diff options
Diffstat (limited to 'src/pmdas/systemd')
-rw-r--r-- | src/pmdas/systemd/GNUmakefile | 59 | ||||
-rwxr-xr-x | src/pmdas/systemd/Install | 29 | ||||
-rw-r--r-- | src/pmdas/systemd/README | 64 | ||||
-rwxr-xr-x | src/pmdas/systemd/Remove | 24 | ||||
-rw-r--r-- | src/pmdas/systemd/help | 52 | ||||
-rw-r--r-- | src/pmdas/systemd/pmns | 35 | ||||
-rw-r--r-- | src/pmdas/systemd/root | 11 | ||||
-rw-r--r-- | src/pmdas/systemd/systemd.c | 791 |
8 files changed, 1065 insertions, 0 deletions
diff --git a/src/pmdas/systemd/GNUmakefile b/src/pmdas/systemd/GNUmakefile new file mode 100644 index 0000000..9805c1d --- /dev/null +++ b/src/pmdas/systemd/GNUmakefile @@ -0,0 +1,59 @@ +# +# Copyright (c) 2012 Red Hat Inc. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the +# Free Software Foundation; either version 2 of the License, or (at your +# option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# for more details. +# + +TOPDIR = ../../.. +include $(TOPDIR)/src/include/builddefs + +CMDTARGET = pmdasystemd$(EXECSUFFIX) +DFILES = README +CFILES = systemd.c +LCFLAGS = $(SYSTEMD_CFLAGS) +LLDLIBS = $(PCP_PMDALIB) $(SYSTEMD_LIBS) +LSRCFILES = Install Remove pmns help $(DFILES) root + +IAM = systemd +DOMAIN = SYSTEMD +PMDADIR = $(PCP_PMDAS_DIR)/$(IAM) + +LDIRT = domain.h *.o $(IAM).log $(CMDTARGET) + +default: build-me + +include $(BUILDRULES) + +ifneq "$(PMDA_SYSTEMD)" "" +build-me: domain.h $(CMDTARGET) + +install: default + $(INSTALL) -m 755 -d $(PMDADIR) + $(INSTALL) -m 755 Install Remove $(PMDADIR) + $(INSTALL) -m 644 $(DFILES) root help pmns $(PMDADIR) + $(INSTALL) -m 644 domain.h $(PMDADIR)/domain.h + $(INSTALL) -m 755 $(CMDTARGET) $(PMDADIR)/$(CMDTARGET) +else +build-me: +install: +endif + +systemd.o: domain.h + +.NOTPARALLEL: +.ORDER: domain.h $(OBJECTS) + +default_pcp : default + +install_pcp : install + +domain.h: ../../pmns/stdpmid + $(DOMAIN_MAKERULE) diff --git a/src/pmdas/systemd/Install b/src/pmdas/systemd/Install new file mode 100755 index 0000000..2757725 --- /dev/null +++ b/src/pmdas/systemd/Install @@ -0,0 +1,29 @@ +#! /bin/sh +# +# Copyright (c) 2011-2013 Red Hat Inc. +# Copyright (c) 1997 Silicon Graphics, Inc. All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the +# Free Software Foundation; either version 2 of the License, or (at your +# option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# for more details. +# +# Install the systemd PMDA and/or PMNS +# + +. $PCP_DIR/etc/pcp.env +. $PCP_SHARE_DIR/lib/pmdaproc.sh + +iam=systemd +pmda_interface=6 +pipe_opt=true +daemon_opt=true +pmdaSetup +pmdaInstall + +exit 0 diff --git a/src/pmdas/systemd/README b/src/pmdas/systemd/README new file mode 100644 index 0000000..3125da1 --- /dev/null +++ b/src/pmdas/systemd/README @@ -0,0 +1,64 @@ +Systemd PMDA +=========== + +This PMDA exports events from the systemd journal [1] as they occur. +There is no configuration required. In the default (daemon) PMDA +mode, the daemon switches to userid 'adm', so it can report systemwide +journal entries. + +[1] http://www.freedesktop.org/wiki/Software/systemd + + +Metrics +======= + +The file ./help contains descriptions for all of the metrics exported +by this PMDA. + +Once the PMDA has been installed, the following command will list all +the available metrics and their explanatory "help" text: + + $ pminfo -fT systemd + +The interesting metrics are systemd.journal.records and +systemd.journal.records_raw. When a PCP client such as pmevent +monitors them, each new journal entry is reported as a PCP event +tuple. Each field of the journal entry is transcribed as a string or +blob, including the FIELDNAME= prefix. See [2] for more details about +interpretation of the fields. + +[2] http://www.freedesktop.org/software/systemd/man/systemd.journal-fields.html + + +Installation +============ + + + # cd $PCP_PMDAS_DIR/systemd + + + Check that there is no clash in the Performance Metrics Domain + defined in ./domain.h and the other PMDAs currently in use (see + $PCP_PMCDCONF_PATH). If there is, edit ./domain.h to choose another + domain number. + + + Then simply use + + # ./Install + + and choose both the "collector" and "monitor" installation + configuration options -- everything else is automated. + +De-installation +=============== + + + Simply use + + # cd $PCP_PMDAS_DIR/systemd + # ./Remove + +Troubleshooting +=============== + + + After installing or restarting the agent, the PMCD log file + ($PCP_LOG_DIR/pmcd/pmcd.log) and the PMDA log file + ($PCP_LOG_DIR/pmcd/systemd.log) should be checked for any warnings + or errors. diff --git a/src/pmdas/systemd/Remove b/src/pmdas/systemd/Remove new file mode 100755 index 0000000..00e48aa --- /dev/null +++ b/src/pmdas/systemd/Remove @@ -0,0 +1,24 @@ +#! /bin/sh +# +# Copyright (c) 1997 Silicon Graphics, Inc. All Rights Reserved. +# Copyright (c) 2011-2012 Red Hat Inc. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the +# Free Software Foundation; either version 2 of the License, or (at your +# option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# for more details. +# +# Remove the systemd PMDA +# + +. $PCP_DIR/etc/pcp.env +. $PCP_SHARE_DIR/lib/pmdaproc.sh +iam=systemd +pmdaSetup +pmdaRemove +exit 0 diff --git a/src/pmdas/systemd/help b/src/pmdas/systemd/help new file mode 100644 index 0000000..1a40f7a --- /dev/null +++ b/src/pmdas/systemd/help @@ -0,0 +1,52 @@ +# +# Copyright (c) 2012 Red Hat, Inc. All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the +# Free Software Foundation; either version 2 of the License, or (at your +# option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# for more details. +# +# systemd PMDA help file in the ASCII format +# +# lines beginning with a # are ignored +# lines beginning @ introduce a new entry of the form +# @ metric_name oneline-text +# help test goes +# here over multiple lines +# ... +# +# the metric_name is decoded against the default PMNS -- as a special case, +# a name of the form NNN.MM (for numeric NNN and MM) is interpreted as an +# instance domain identification, and the text describes the instance domain +# +# blank lines before the @ line are ignored +# + +@ systemd.numclients The number of attached clients +The number of attached clients. +@ systemd.maxmem Maximum number of queued event bytes. +Maximum number of queued event bytes (apprx. 128 bytes per cursor string). + +@ systemd.journal.field.cursor The cursor, an implicit journald field. +This is the journal entry's permanent, globally unique cursor string. +@ systemd.journal.field.string A journal field that may be a string. +A journal field copied verbatim, as a PM_TYPE_STRING object, presumed as +a valid string (in some encoding), if the field did not contain any \0 characters. +@ systemd.journal.field.blob A journal field copied verbatim. +A journal field copied verbatim, as a PM_TYPE_AGGREGATE object. + +@ systemd.journal.records Journal entries, encoded as strings and blobs. +Each new journald event field is given a systemd.parameters.cursor string +to identify it, and a collection of string and blob fields (as appropriate). + +@ systemd.journal.records_raw Journal entries, encoded as blob parameters only. +Each new journald event field is given a systemd.parameters.cursor string +to identify it, and a blob fields the reproduce the FIELD=value bit-for-bit. + +@ systemd.journal.count Count of journal entries observed +@ systemd.journal.bytes Sum of sizes of all journal entries observed diff --git a/src/pmdas/systemd/pmns b/src/pmdas/systemd/pmns new file mode 100644 index 0000000..b9e6487 --- /dev/null +++ b/src/pmdas/systemd/pmns @@ -0,0 +1,35 @@ +/* + * Metrics for systemd PMDA + * + * Copyright (c) 2012-2013 Red Hat Inc. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +systemd { + numclients SYSTEMD:0:0 + maxmem SYSTEMD:0:1 + journal +} + +systemd.journal { + records SYSTEMD:2:0 + records_raw SYSTEMD:2:1 + field + count SYSTEMD:2:2 + bytes SYSTEMD:2:3 +} + +systemd.journal.field { + cursor SYSTEMD:1:0 + string SYSTEMD:1:1 /* possibly a string */ + blob SYSTEMD:1:2 /* a binary blob */ +} diff --git a/src/pmdas/systemd/root b/src/pmdas/systemd/root new file mode 100644 index 0000000..cac7f3a --- /dev/null +++ b/src/pmdas/systemd/root @@ -0,0 +1,11 @@ +#include <stdpmid> + +root { + systemd +} + +#ifndef SYSTEMD +#define SYSTEMD 114 +#endif + +#include "pmns" diff --git a/src/pmdas/systemd/systemd.c b/src/pmdas/systemd/systemd.c new file mode 100644 index 0000000..c67cb66 --- /dev/null +++ b/src/pmdas/systemd/systemd.c @@ -0,0 +1,791 @@ +/* + * systemd support for the systemd PMDA + * + * Copyright (c) 2012-2014 Red Hat. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + * Structure based upon the logger pmda. + */ + +#define _POSIX_C_SOURCE 200112L /* for strtoull */ + +#include "pmapi.h" +#include "impl.h" +#include "pmda.h" +#include "domain.h" + +#include <systemd/sd-journal.h> + +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> +#include <ctype.h> +#include <stdlib.h> +#include <assert.h> +#include <ctype.h> +#include <grp.h> + +#define DEFAULT_MAXMEM (2 * 1024 * 1024) /* 2 megabytes */ +long maxmem; +int maxfd; +fd_set fds; +static int interval_expired; +static struct timeval interval = { 60, 0 }; +static sd_journal *journald_context; /* Used for monitoring only. */ +static sd_journal *journald_context_seeky; /* Used for event detail extraction, + involving seeks. */ +static int queue_entries = -1; +static char *username = "adm"; + + +/* Track per-context PCP_ATTR_USERID | _GROUPID, so we + can filter event records for that context. */ +static int uid_gid_filter_p = 1; +struct uid_gid_tuple { + char wildcard_p; /* do not filter for this context. */ + char uid_p; char gid_p; /* uid/gid received flags. */ + int uid; int gid; }; /* uid/gid received from PCP_ATTR_* */ +static struct uid_gid_tuple *ctxtab = NULL; +int ctxtab_size = 0; + + +static pmdaMetric metrictab[] = { +/* numclients */ +#define METRICTAB_NUMCLIENTS_PMID metrictab[0].m_desc.pmid + { NULL, + { PMDA_PMID(0,0), PM_TYPE_U32, PM_INDOM_NULL, PM_SEM_INSTANT, + PMDA_PMUNITS(0,0,1,0,0,PM_COUNT_ONE) }, }, +/* maxmem */ +#define METRICTAB_MAXMEM_PMID metrictab[1].m_desc.pmid + { NULL, + { PMDA_PMID(0,1), PM_TYPE_U32, PM_INDOM_NULL, PM_SEM_INSTANT, + PMDA_PMUNITS(1,0,0,PM_SPACE_BYTE,0,0) }, }, +/* journal.field.cursor */ +#define METRICTAB_JOURNAL_CURSOR_PMID metrictab[2].m_desc.pmid + { NULL, + { PMDA_PMID(1,0), PM_TYPE_STRING, PM_INDOM_NULL, PM_SEM_INSTANT, + PMDA_PMUNITS(0,0,0,0,0,0) }, }, +/* journal.field.string */ +#define METRICTAB_JOURNAL_STRING_PMID metrictab[3].m_desc.pmid + { NULL, + { PMDA_PMID(1,1), PM_TYPE_STRING, PM_INDOM_NULL, PM_SEM_INSTANT, + PMDA_PMUNITS(0,0,0,0,0,0) }, }, +/* journal.field.blob */ +#define METRICTAB_JOURNAL_BLOB_PMID metrictab[4].m_desc.pmid + { NULL, + { PMDA_PMID(1,2), PM_TYPE_AGGREGATE, PM_INDOM_NULL, PM_SEM_INSTANT, + PMDA_PMUNITS(0,0,0,0,0,0) }, }, +/* journal.records */ +#define METRICTAB_JOURNAL_RECORDS_PMID metrictab[5].m_desc.pmid + { NULL, + { PMDA_PMID(2,0), PM_TYPE_EVENT, PM_INDOM_NULL, PM_SEM_INSTANT, + PMDA_PMUNITS(0,0,0,0,0,0) }, }, +/* journal.records_raw */ +#define METRICTAB_JOURNAL_RECORDS_RAW_PMID metrictab[6].m_desc.pmid + { NULL, + { PMDA_PMID(2,1), PM_TYPE_EVENT, PM_INDOM_NULL, PM_SEM_INSTANT, + PMDA_PMUNITS(0,0,0,0,0,0) }, }, +/* journal.count */ +#define METRICTAB_JOURNAL_COUNT_PMID metrictab[7].m_desc.pmid + { NULL, + { PMDA_PMID(2,2), PM_TYPE_U32, PM_INDOM_NULL, PM_SEM_COUNTER, + PMDA_PMUNITS(0,0,1,0,0,PM_COUNT_ONE) }, }, +/* journal.bytes */ +#define METRICTAB_JOURNAL_BYTES_PMID metrictab[8].m_desc.pmid + { NULL, + { PMDA_PMID(2,3), PM_TYPE_U64, PM_INDOM_NULL, PM_SEM_COUNTER, + PMDA_PMUNITS(1,0,0,PM_SPACE_BYTE,0,0) }, }, +}; + + +void systemd_shutdown(void) +{ + if (journald_context != 0) + sd_journal_close (journald_context); + + if (journald_context_seeky != 0) + sd_journal_close (journald_context_seeky); + + /* XXX: pmdaEvent zap queues? */ +} + + +/* Return a strndup (or NULL) of a field of the current journal entry, + since sd_journal_get_data returns data that is not + \0-terminated. */ +char * +my_sd_journal_get_data(sd_journal *j, const char *field) +{ + int rc; + const char* str; + size_t str_len; + + assert (j != NULL); + assert (field != NULL); + + rc = sd_journal_get_data(j, field, + (const void**) & str, & str_len); + if (rc < 0) + return NULL; + + return strndup (str, str_len); +} + + +void systemd_refresh(void) +{ + /* Absorb any changes such as inotify() messages. */ + (void) sd_journal_process(journald_context); + (void) sd_journal_process(journald_context_seeky); + + while (1) { + char *cursor = NULL; + char *timestamp_str = NULL; + struct timeval timestamp; + + int rc = sd_journal_next(journald_context); + + if (rc == 0) /* No recent entries. */ + break; + + if (rc < 0) { + __pmNotifyErr(LOG_ERR, "sd_journal_next failure: %s", strerror(-rc)); + break; + } + + /* NB: we enqueue the journal cursor string, rather than the + actual journal records. */ + rc = sd_journal_get_cursor(journald_context, &cursor); + if (rc < 0) { + __pmNotifyErr(LOG_ERR, "sd_journal_get_cursor failure: %s", + strerror(-rc)); + break; + } + + /* Extract a timestamp from the journald event fields. */ + timestamp_str = my_sd_journal_get_data(journald_context, + "_SOURCE_REALTIME_TIMESTAMP"); + if (timestamp_str == NULL) + timestamp_str = my_sd_journal_get_data(journald_context, + "__REALTIME_TIMESTAMP"); + if (timestamp_str == NULL) + rc = -ENOMEM; + else { + const char* curse; + unsigned long long epoch_us; + /* defined in systemd.journal-fields(7) as + FIELD_NAME=NNNN, where NNNN is decimal us since epoch. */ + curse = strchr (timestamp_str, '='); + if (curse == NULL) + rc = -EINVAL; + else { + curse ++; + epoch_us = strtoull (curse, NULL, 10); + timestamp.tv_sec = epoch_us / 1000000; + timestamp.tv_usec = epoch_us % 1000000; + } + free (timestamp_str); + } + /* Improvise. */ + if (rc < 0) + gettimeofday (& timestamp, NULL); + + /* Enqueue it to fresh visitors. */ + rc = pmdaEventQueueAppend(queue_entries, + cursor, strlen(cursor)+1 /* \0 */, ×tamp); + free(cursor); /* Already copied. */ + if (rc < 0) { + __pmNotifyErr(LOG_ERR, "pmdaEventQueueAppend failure: %s", pmErrStr(rc)); + break; + } + } +} + + + +enum journald_field_encoding { + JFE_STRING_BLOB_AUTO, + JFE_BLOB_ONLY +}; + + + +int +systemd_journal_event_filter (void *rp, void *data, size_t size) +{ + int rc; + struct uid_gid_tuple* ugt = rp; + + assert (ugt == & ctxtab[pmdaGetContext()]); + if (pmDebug & DBG_TRACE_APPL0) + __pmNotifyErr(LOG_DEBUG, "filter (%d) uid=%d gid=%d data=%p bytes=%u\n", + pmdaGetContext(), ugt->uid, ugt->gid, data, (unsigned)size); + + /* The data/size pair gives the object in the event queue, i.e., + the systemd journal cursor string. It has not yet been turned + into a PM_TYPE_EVENT tuple yet, and if we have our way, it won't + be (for non-participating clients). */ + + /* The general filtering idea is to only feed journal records to clients + if their uid matches the _UID=NNN field -or- gid matches _GID=MMM + -or- the client is highly authenticated (wildcard_p) -or- per-uid filtering + was turned off at the pmda level. */ + + /* Reminder: function rc == 0 passes the filter. */ + + /* Unfiltered? Everyone gets egg soup! */ + if (! uid_gid_filter_p) + return 0; + + if (pmDebug & DBG_TRACE_APPL0) + __pmNotifyErr(LOG_DEBUG, "filter (%d) uid%s%d gid%s%d wildcard=%d\n", + pmdaGetContext(), + ugt->uid_p?"=":"?", ugt->uid, + ugt->gid_p?"=":"?", ugt->gid, + ugt->wildcard_p); + + /* Superuser? May we offer some goulash? */ + if (ugt->wildcard_p) + return 0; + + /* Unauthenticated context? No soup for you! */ + if (! ugt->uid_p && ! ugt->gid_p) + return 1; + + /* OK, we need to take a look at the journal record in question. */ + + if (pmDebug & DBG_TRACE_APPL0) + __pmNotifyErr(LOG_DEBUG, "filter cursor=%s\n", (const char*) data); + + (void) size; /* already known \0-terminated */ + rc = sd_journal_seek_cursor(journald_context_seeky, (char*) data); + if (rc < 0) { + __pmNotifyErr(LOG_ERR, "filter cannot seek to cursor=%s\n", + (const char*) data); + return 1; /* No point trying again in systemd_journal_decoder. */ + } + + rc = sd_journal_next(journald_context_seeky); + if (rc < 0) { + __pmNotifyErr(LOG_ERR, "filter cannot advance to next\n"); + return 1; /* No point trying again in systemd_journal_decoder. */ + } + + if (ugt->uid_p) { + char *uid_str = my_sd_journal_get_data(journald_context_seeky, "_UID"); + if (uid_str) { + int uid = atoi (& uid_str[5]); /* skip over _UID= */ + free (uid_str); + if (uid == ugt->uid) + return 0; /* You're a somebody. Here's a bowl of stew. */ + } + } + + if (ugt->gid_p) { + char *gid_str = my_sd_journal_get_data(journald_context_seeky, "_GID"); + if (gid_str) { + int gid = atoi (& gid_str[5]); /* skip over _GID= */ + free (gid_str); + if (gid == ugt->gid) + return 0; /* You're with pals. Here's a bowl of miso. */ + } + } + + /* No soup for you! */ + return 1; +} + + +void +systemd_journal_event_filter_release (void *rp) +{ + /* NB: We have nothing to release, as we don't do memory allocation + for the filter per se - we clean up during end-context time. + We can't send a NULL to pmdaEventSetFilter for release purposes + (since it'll blindly call it), so need this dummy function. */ + + (void) rp; +} + + +int +systemd_journal_decoder(int eventarray, void *buffer, size_t size, + struct timeval *timestamp, void *data) +{ + int sts; + pmAtomValue atom; + enum journald_field_encoding jfe = * (enum journald_field_encoding *) data; + + sts = pmdaEventAddRecord(eventarray, timestamp, PM_EVENT_FLAG_POINT); + if (sts < 0) + return sts; + + /* Go to the cursor point enqueued for this client. The buffer is already + \0-terminated. */ + sts = sd_journal_seek_cursor(journald_context_seeky, (char*) buffer); + if (sts < 0) { + /* But see RHBZ #876654. */ + return /* sts */ 0; + } + + sts = sd_journal_next(journald_context_seeky); + if (sts < 0) + return sts; + if (sts == 0) + return -ENODATA; /* event got lost between cursor-recording and now */ + + /* Add the _CURSOR implicit journal field. */ + atom.cp = buffer; + sts = pmdaEventAddParam(eventarray, METRICTAB_JOURNAL_CURSOR_PMID, + PM_TYPE_STRING, &atom); + + /* Add all the explicit journal fields. */ + while (1) { + const void *data; + size_t data_len; + + if (sts < 0) + break; + sts = sd_journal_enumerate_data(journald_context_seeky, &data, &data_len); + if (sts <= 0) + break; + + /* Infer string upon absence of embedded \0's. */ + if (jfe == JFE_STRING_BLOB_AUTO && (memchr (data, '\0', data_len) == NULL)) { + /* Unfortunately, data may not be \0-terminated, so we can't simply pass + it to atom.cp. We need to copy the bad boy first. */ + atom.cp = strndup(data, data_len); + if (atom.cp == NULL) + sts = -ENOMEM; + else { + sts = pmdaEventAddParam(eventarray, METRICTAB_JOURNAL_STRING_PMID, + PM_TYPE_STRING, &atom); + free (atom.cp); + } + /* NB: we assume libpcp_pmda will not free() the field. */ + } else { + pmValueBlock *aggr = (pmValueBlock *)malloc(PM_VAL_HDR_SIZE + data_len); + if (aggr == NULL) + sts = -ENOMEM; + else { + aggr->vtype = PM_TYPE_AGGREGATE; + if (PM_VAL_HDR_SIZE + data_len >= 1<<24) + aggr->vlen = (1U<<24) - 1; /* vlen is a :24 bit field */ + else + aggr->vlen = PM_VAL_HDR_SIZE + data_len; + memcpy (aggr->vbuf, data, data_len); + atom.vbp = aggr; + sts = pmdaEventAddParam(eventarray, METRICTAB_JOURNAL_BLOB_PMID, + PM_TYPE_AGGREGATE, &atom); + /* NB: we assume libpcp_pmda will free() aggr. */ + } + } + } + + return sts < 0 ? sts : 1; /* added one event array */ +} + + +void enlarge_ctxtab(int context) +{ + /* Grow the context table if necessary. */ + if (ctxtab_size /* cardinal */ <= context /* ordinal */) { + size_t need = (context + 1) * sizeof(struct uid_gid_tuple); + ctxtab = realloc (ctxtab, need); + if (ctxtab == NULL) + __pmNoMem("systemd ctx table", need, PM_FATAL_ERR); + /* Blank out new entries. */ + while (ctxtab_size <= context) + memset (& ctxtab[ctxtab_size++], 0, sizeof(struct uid_gid_tuple)); + } +} + + +static int +systemd_fetch(int numpmid, pmID pmidlist[], pmResult **resp, pmdaExt *pmda) +{ + int sts; + (void) pmdaEventNewClient(pmda->e_context); + enlarge_ctxtab(pmda->e_context); + sts = pmdaEventSetFilter(pmda->e_context, queue_entries, + & ctxtab[pmda->e_context], /* any non-NULL value */ + systemd_journal_event_filter, + systemd_journal_event_filter_release /* NULL */); + if (sts < 0) + return sts; + return pmdaFetch(numpmid, pmidlist, resp, pmda); +} + + +static int +systemd_fetchCallBack(pmdaMetric *mdesc, unsigned int inst, pmAtomValue *atom) +{ + pmID id = mdesc->m_desc.pmid; + int sts; + + if (id == METRICTAB_NUMCLIENTS_PMID) { + sts = pmdaEventClients(atom); + } else if (id == METRICTAB_MAXMEM_PMID) { + atom->ul = (unsigned long)maxmem; + sts = PMDA_FETCH_STATIC; + } else if (id == METRICTAB_JOURNAL_CURSOR_PMID) { + sts = PMDA_FETCH_NOVALUES; + } else if (id == METRICTAB_JOURNAL_STRING_PMID) { + sts = PMDA_FETCH_NOVALUES; + } else if (id == METRICTAB_JOURNAL_BLOB_PMID) { + sts = PMDA_FETCH_NOVALUES; + } else if (id == METRICTAB_JOURNAL_COUNT_PMID) { + sts = pmdaEventQueueCounter(queue_entries, atom); + } else if (id == METRICTAB_JOURNAL_BYTES_PMID) { + sts = pmdaEventQueueBytes(queue_entries, atom); + } else if (id == METRICTAB_JOURNAL_RECORDS_PMID) { + enum journald_field_encoding jfe = JFE_STRING_BLOB_AUTO; + sts = pmdaEventSetAccess(pmdaGetContext(), queue_entries, 1); + if (sts == 0) + sts = pmdaEventQueueRecords(queue_entries, atom, pmdaGetContext(), + systemd_journal_decoder, & jfe); + } else if (id == METRICTAB_JOURNAL_RECORDS_RAW_PMID) { + enum journald_field_encoding jfe = JFE_BLOB_ONLY; + sts = pmdaEventSetAccess(pmdaGetContext(), queue_entries, 1); + if (sts == 0) + sts = pmdaEventQueueRecords(queue_entries, atom, pmdaGetContext(), + systemd_journal_decoder, & jfe); + } else { + sts = PM_ERR_PMID; + } + return sts; +} + + +static int +systemd_contextAttributeCallBack(int context, + int attr, const char *value, int length, pmdaExt *pmda) +{ + static int rootlike_gids_found = 0; + static int adm_gid = -1; + static int wheel_gid = -1; + static int systemd_journal_gid = -1; + int id; + + /* Look up root-like gids if needed. A later PCP client that + matches any of these group-id's is treated as if root/adm, + i.e., journal records are not filtered for them (wildcard_p). + XXX: we could examine group-membership lists and check against + uid to also set wildcard_p. */ + if (! rootlike_gids_found) { + struct group *grp; + grp = getgrnam("adm"); + if (grp) adm_gid = grp->gr_gid; + grp = getgrnam("wheel"); + if (grp) wheel_gid = grp->gr_gid; + grp = getgrnam("systemd-journal"); + if (grp) systemd_journal_gid = grp->gr_gid; + rootlike_gids_found = 1; + } + + enlarge_ctxtab(context); + assert (ctxtab != NULL && context < ctxtab_size); + + /* NB: we maintain separate uid_p and gid_p for filtering + purposes; it's possible that a pcp client might send only + PCP_ATTR_USERID, leaving gid=0, possibly leading us to + misinterpret that as GROUPID=0 (root) and sending back _GID=0 + records. */ + switch (attr) { + case PCP_ATTR_USERID: + ctxtab[context].uid_p = 1; + id = atoi(value); + ctxtab[context].uid = id; + if (id == 0) /* root */ + ctxtab[context].wildcard_p = 1; + break; + + case PCP_ATTR_GROUPID: + ctxtab[context].gid_p = 1; + id = atoi(value); + ctxtab[context].gid = id; + if (id == adm_gid || + id == wheel_gid || + id == systemd_journal_gid) + ctxtab[context].wildcard_p = 1; + break; + } + + if (pmDebug & DBG_TRACE_APPL0) + __pmNotifyErr(LOG_DEBUG, "attrib (%d) uid%s%d gid%s%d wildcard=%d\n", + context, + ctxtab[context].uid_p?"=":"?", ctxtab[context].uid, + ctxtab[context].gid_p?"=":"?", ctxtab[context].gid, + ctxtab[context].wildcard_p); + + return 0; +} + + +static void +systemd_end_contextCallBack(int context) +{ + pmdaEventEndClient(context); + + /* assert (ctxtab != NULL && context < ctxtab_size); */ + + /* NB: don't do that; this callback may be hit without any fetch + calls having been performed, this ctxtab not stretching all the + way to [context]. */ + + if (context < ctxtab_size) + memset (& ctxtab[context], 0, sizeof(struct uid_gid_tuple)); +} + + +static int +systemd_desc(pmID pmid, pmDesc *desc, pmdaExt *pmda) +{ + return pmdaDesc(pmid, desc, pmda); +} + + +static int +systemd_text(int ident, int type, char **buffer, pmdaExt *pmda) +{ + return pmdaText(ident, type, buffer, pmda); +} + + +void +systemd_init(pmdaInterface *dp) +{ + int sts; + int journal_fd; + + dp->version.six.desc = systemd_desc; + dp->version.six.fetch = systemd_fetch; + dp->version.six.text = systemd_text; + dp->version.six.attribute = systemd_contextAttributeCallBack; + pmdaSetFetchCallBack(dp, systemd_fetchCallBack); + pmdaSetEndContextCallBack(dp, systemd_end_contextCallBack); + pmdaInit(dp, NULL, 0, metrictab, sizeof(metrictab)/sizeof(metrictab[0])); + + /* Initialize the systemd side. This is failure-tolerant. */ + /* XXX: SD_JOURNAL_{LOCAL|RUNTIME|SYSTEM}_ONLY */ + sts = sd_journal_open(& journald_context, 0); + if (sts < 0) { + __pmNotifyErr(LOG_ERR, "sd_journal_open failure: %s", + strerror(-sts)); + dp->status = sts; + return; + } + + sts = sd_journal_open(& journald_context_seeky, 0); + if (sts < 0) { + __pmNotifyErr(LOG_ERR, "sd_journal_open #2 failure: %s", + strerror(-sts)); + dp->status = sts; + return; + } + + sts = sd_journal_seek_tail(journald_context); + if (sts < 0) { + __pmNotifyErr(LOG_ERR, "sd_journal_seek_tail failure: %s", + strerror(-sts)); + } + + /* Work around RHBZ979487. */ + sts = sd_journal_previous_skip(journald_context, 1); + if (sts < 0) { + __pmNotifyErr(LOG_ERR, "sd_journal_previous_skip failure: %s", + strerror(-sts)); + } + + /* Arrange to wake up for journal events. */ + journal_fd = sd_journal_get_fd(journald_context); + if (journal_fd < 0) { + __pmNotifyErr(LOG_ERR, "sd_journal_get_fd failure: %s", + strerror(-journal_fd)); + /* NB: not a fatal error; the select() loop will still time out and + periodically poll. This makes it ok for sd_journal_reliable_fd() + to be 0. */ + } else { + FD_SET(journal_fd, &fds); + if (journal_fd > maxfd) maxfd = journal_fd; + } + + /* NB: One queue is used for both .records and .records_raw; they + just use different decoder callbacks. */ + queue_entries = pmdaEventNewQueue("systemd", maxmem); + if (queue_entries < 0) + __pmNotifyErr(LOG_ERR, "pmdaEventNewQueue failure: %s", + pmErrStr(queue_entries)); +} + + +void +systemdMain(pmdaInterface *dispatch) +{ + int pmcdfd; + + pmcdfd = __pmdaInFd(dispatch); + if (pmcdfd > maxfd) + maxfd = pmcdfd; + + FD_SET(pmcdfd, &fds); + + for (;;) { + fd_set readyfds; + int nready; + struct timeval select_timeout = interval; + + memcpy(&readyfds, &fds, sizeof(readyfds)); + nready = select(maxfd+1, &readyfds, NULL, NULL, & select_timeout); + if (pmDebug & DBG_TRACE_APPL2) + __pmNotifyErr(LOG_DEBUG, "select: nready=%d interval=%d", + nready, interval_expired); + if (nready < 0) { + if (neterror() != EINTR) { + __pmNotifyErr(LOG_ERR, "select failure: %s", netstrerror()); + exit(1); + } else if (!interval_expired) { + continue; + } + } + + if (nready > 0 && FD_ISSET(pmcdfd, &readyfds)) { + if (pmDebug & DBG_TRACE_APPL0) + __pmNotifyErr(LOG_DEBUG, "processing pmcd PDU [fd=%d]", pmcdfd); + if (__pmdaMainPDU(dispatch) < 0) { + exit(1); /* fatal if we lose pmcd */ + } + if (pmDebug & DBG_TRACE_APPL0) + __pmNotifyErr(LOG_DEBUG, "completed pmcd PDU [fd=%d]", pmcdfd); + } + systemd_refresh(); + } +} + + +static void +convertUnits(char **endnum, long *maxmem) +{ + switch ((int) **endnum) { + case 'b': + case 'B': + break; + case 'k': + case 'K': + *maxmem *= 1024; + break; + case 'm': + case 'M': + *maxmem *= 1024 * 1024; + break; + case 'g': + case 'G': + *maxmem *= 1024 * 1024 * 1024; + break; + } + (*endnum)++; +} + + +static void +usage(void) +{ + fprintf(stderr, + "Usage: %s [options]\n\n" + "Options:\n" + " -d domain use domain (numeric) for metrics domain of PMDA\n" + " -l logfile write log into logfile rather than using default log name\n" + " -m memory maximum memory used per queue (default %ld bytes)\n" + " -s interval default delay between iterations (default %d sec)\n" + " -U username user account to run under (default \"adm\")\n" + " -f disable per-uid/gid record filtering (default on)\n", + pmProgname, maxmem, (int)interval.tv_sec); + exit(1); +} + + +int +main(int argc, char **argv) +{ + static char helppath[MAXPATHLEN]; + char *endnum; + pmdaInterface desc; + long minmem; + int c, err = 0, sep = __pmPathSeparator(); + + minmem = getpagesize(); + maxmem = (minmem > DEFAULT_MAXMEM) ? minmem : DEFAULT_MAXMEM; + __pmSetProgname(argv[0]); + snprintf(helppath, sizeof(helppath), "%s%c" "systemd" "%c" "help", + pmGetConfig("PCP_PMDAS_DIR"), sep, sep); + pmdaDaemon(&desc, PMDA_INTERFACE_6, pmProgname, SYSTEMD, + "systemd.log", helppath); + + while ((c = pmdaGetOpt(argc, argv, "D:d:l:m:s:U:f?", &desc, &err)) != EOF) { + switch (c) { + case 'm': + maxmem = strtol(optarg, &endnum, 10); + if (*endnum != '\0') + convertUnits(&endnum, &maxmem); + if (*endnum != '\0' || maxmem < minmem) { + fprintf(stderr, "%s: invalid max memory '%s' (min=%ld)\n", + pmProgname, optarg, minmem); + err++; + } + break; + + case 's': + if (pmParseInterval(optarg, &interval, &endnum) < 0) { + fprintf(stderr, "%s: -s requires a time interval: %s\n", + pmProgname, endnum); + free(endnum); + err++; + } + break; + + case 'U': + username = optarg; + break; + + case 'f': + uid_gid_filter_p = 0; + break; + + default: + err++; + break; + } + } + + if (err) + usage(); + + FD_ZERO (&fds); + pmdaOpenLog(&desc); + + /* The systemwide journal may be accessed by the adm user (group); + root access is not necessary. */ + __pmSetProcessIdentity(username); + desc.comm.flags |= PDU_FLAG_AUTH; + pmdaConnect(&desc); + // After this point, systemd_init is allowed to take some extra time. + systemd_init(&desc); // sets some fds + systemdMain(&desc); // sets some more fds + systemd_shutdown(); + exit(0); +} + +/* + Local Variables: + c-basic-offset: 4 + End: +*/ |