/* * This file is part of Cockpit. * * Copyright (C) 2014 Red Hat, Inc. * * Cockpit is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 2.1 of the License, or * (at your option) any later version. * * Cockpit 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Cockpit; If not, see . */ #define _GNU_SOURCE #include "config.h" #include "cockpitmetrics.h" #include "cockpitpcpmetrics.h" #include "common/cockpitjson.h" #include #include /** * CockpitPcpMetrics: * * A #CockpitMetrics channel that pulls data from PCP */ static int my_pmParseUnitsStr(const char *, pmUnits *, double *); #define COCKPIT_PCP_METRICS(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), COCKPIT_TYPE_PCP_METRICS, CockpitPcpMetrics)) typedef struct { const gchar *name; const gchar *derive; pmID id; pmDesc desc; pmUnits *units; gdouble factor; pmUnits units_buf; } MetricInfo; typedef struct { int context; gint64 start; } ArchiveInfo; typedef struct { CockpitMetrics parent; const gchar *name; int direct_context; int numpmid; pmID *pmidlist; MetricInfo *metrics; gint64 interval; gint64 limit; guint idler; GList *archives; /* of ArchiveInfo */ GList *cur_archive; /* The previous samples sent */ pmResult *last; } CockpitPcpMetrics; typedef struct { CockpitMetricsClass parent_class; } CockpitPcpMetricsClass; G_DEFINE_TYPE (CockpitPcpMetrics, cockpit_pcp_metrics, COCKPIT_TYPE_METRICS); static void cockpit_pcp_metrics_init (CockpitPcpMetrics *self) { self->direct_context = -1; } static gboolean result_meta_equal (CockpitPcpMetrics *self, pmResult *r1, pmResult *r2) { pmValueSet *vs1; pmValueSet *vs2; int i, j; /* PCP guarantees that the result ids are same as requested */ for (i = 0; i < r1->numpmid; i++) { /* We only care about instanced metrics. */ if (self->metrics[i].desc.indom == PM_INDOM_NULL) continue; vs1 = r1->vset[i]; vs2 = r2->vset[i]; g_assert (vs1 && vs2); if (vs1->numval != vs2->numval) return FALSE; for (j = 0; j < vs1->numval; j++) { if (vs1->vlist[j].inst != vs2->vlist[j].inst) return FALSE; } } return TRUE; } static gint64 timestamp_from_timeval (struct timeval *tv) { return tv->tv_sec * 1000 + tv->tv_usec / 1000; } static JsonObject * build_meta (CockpitPcpMetrics *self, pmResult *result) { JsonArray *metrics; JsonObject *metric; JsonArray *instances; JsonObject *root; pmValueSet *vs; struct timeval now_timeval; gint64 timestamp, now; char *instance; int i, j; int rc; gettimeofday (&now_timeval, NULL); timestamp = timestamp_from_timeval (&result->timestamp); now = timestamp_from_timeval (&now_timeval); root = json_object_new (); json_object_set_int_member (root, "timestamp", timestamp); json_object_set_int_member (root, "now", now); json_object_set_int_member (root, "interval", self->interval); metrics = json_array_new (); for (i = 0; i < result->numpmid; i++) { metric = json_object_new (); /* Name and derivation mode */ json_object_set_string_member (metric, "name", self->metrics[i].name); if (self->metrics[i].derive) json_object_set_string_member (metric, "derive", self->metrics[i].derive); /* Instances */ vs = result->vset[i]; if (vs->numval < 0 || self->metrics[i].desc.indom == PM_INDOM_NULL) { /* When negative numval is an error code ... we don't care */ } else { instances = json_array_new (); for (j = 0; j < vs->numval; j++) { /* PCP guarantees that the result is in the same order as requested */ rc = pmNameInDom (self->metrics[i].desc.indom, vs->vlist[j].inst, &instance); if (rc != 0) { g_warning ("%s: instance name lookup failed: %s", self->name, pmErrStr (rc)); instance = NULL; } /* HACK: We can't use json_builder_add_string_value here since it turns empty strings into 'null' values inside arrays. https://bugzilla.gnome.org/show_bug.cgi?id=730803 */ { JsonNode *string_element = json_node_alloc (); json_node_init_string (string_element, instance? instance : ""); json_array_add_element (instances, string_element); } if (instance) free (instance); } json_object_set_array_member (metric, "instances", instances); } /* Units */ if (self->metrics[i].factor == 1.0) { json_object_set_string_member (metric, "units", pmUnitsStr(self->metrics[i].units)); } else { gchar *name = g_strdup_printf ("%s*%g", pmUnitsStr(self->metrics[i].units), 1.0/self->metrics[i].factor); json_object_set_string_member (metric, "units", name); g_free (name); } /* Semantics */ switch (self->metrics[i].desc.sem) { case PM_SEM_COUNTER: json_object_set_string_member (metric, "semantics", "counter"); break; case PM_SEM_INSTANT: json_object_set_string_member (metric, "semantics", "instant"); break; case PM_SEM_DISCRETE: json_object_set_string_member (metric, "semantics", "discrete"); break; default: break; } json_array_add_object_element (metrics, metric); } json_object_set_array_member (root, "metrics", metrics); return root; } static JsonObject * build_meta_if_necessary (CockpitPcpMetrics *self, pmResult *result) { if (self->last) { /* * If we've already sent the first meta message, then only send * another when the set of instances in the results change. */ if (result_meta_equal (self, self->last, result)) return NULL; } return build_meta (self, result); } static void build_sample (CockpitPcpMetrics *self, double **buffer, pmResult *result, int metric, int instance) { MetricInfo *info = &self->metrics[metric]; int valfmt = result->vset[metric]->valfmt; pmValue *value = &result->vset[metric]->vlist[instance]; pmAtomValue sample; buffer[metric][instance] = NAN; if (info->desc.type == PM_TYPE_AGGREGATE || info->desc.type == PM_TYPE_EVENT) return; if (result->vset[metric]->numval <= instance) return; /* Make sure we keep the least 48 significant bits of 64 bit numbers since "delta" and "rate" derivation works on those, and the whole 64 don't fit into a double. */ if (info->desc.type == PM_TYPE_64) { if (pmExtractValue (valfmt, value, PM_TYPE_64, &sample, PM_TYPE_64) < 0) return; sample.d = (sample.ll << 16) >> 16; } else if (info->desc.type == PM_TYPE_U64) { if (pmExtractValue (valfmt, value, PM_TYPE_U64, &sample, PM_TYPE_U64) < 0) return; sample.d = (sample.ull << 16) >> 16; } else { if (pmExtractValue (valfmt, value, info->desc.type, &sample, PM_TYPE_DOUBLE) < 0) return; } if (info->units != &info->desc.units) { if (pmConvScale (PM_TYPE_DOUBLE, &sample, &info->desc.units, &sample, info->units) < 0) return; sample.d *= info->factor; } buffer[metric][instance] = sample.d; } static void build_samples (CockpitPcpMetrics *self, pmResult *result) { double **buffer; pmValueSet *vs; int i, j; buffer = cockpit_metrics_get_data_buffer (COCKPIT_METRICS (self)); for (i = 0; i < result->numpmid; i++) { vs = result->vset[i]; /* When negative numval is an error code ... we don't care */ if (vs->numval < 0) { ; } else if (self->metrics[i].desc.indom == PM_INDOM_NULL) { build_sample (self, buffer, result, i, 0); } else { for (j = 0; j < vs->numval; j++) build_sample (self, buffer, result, i, j); } } } static void cockpit_pcp_metrics_tick (CockpitMetrics *metrics, gint64 timestamp) { CockpitPcpMetrics *self = (CockpitPcpMetrics *)metrics; JsonObject *meta; pmResult *result; int rc; if (pmUseContext (self->direct_context) < 0) g_return_if_reached (); rc = pmFetch (self->numpmid, self->pmidlist, &result); if (rc < 0) { cockpit_channel_fail (COCKPIT_CHANNEL (self), "internal-error", "%s: couldn't fetch metrics: %s", self->name, pmErrStr (rc)); return; } meta = build_meta_if_necessary (self, result); if (meta) { cockpit_metrics_send_meta (metrics, meta, FALSE); json_object_unref (meta); } /* Send one set of samples */ build_samples (self, result); cockpit_metrics_send_data (metrics, timestamp_from_timeval (&result->timestamp)); cockpit_metrics_flush_data (metrics); if (self->last) pmFreeResult (self->last); self->last = result; } static void next_archive (CockpitPcpMetrics *self); static gboolean on_idle_batch (gpointer user_data) { const int archive_batch = 60; CockpitPcpMetrics *self = user_data; ArchiveInfo *info; JsonObject *meta; pmResult *result; gint i; int rc; info = (ArchiveInfo *)(self->cur_archive->data); if (pmUseContext (info->context) < 0) { self->idler = 0; return FALSE; } for (i = 0; i < archive_batch; i++) { /* Sent enough samples? */ self->limit--; if (self->limit < 0) { cockpit_metrics_flush_data (COCKPIT_METRICS (self)); cockpit_channel_close (COCKPIT_CHANNEL (self), NULL); self->idler = 0; return FALSE; } rc = pmFetch (self->numpmid, self->pmidlist, &result); if (rc < 0) { self->idler = 0; if (rc == PM_ERR_EOL) { cockpit_metrics_flush_data (COCKPIT_METRICS (self)); next_archive (self); } else { cockpit_channel_fail (COCKPIT_CHANNEL (self), "internal-error", "%s: couldn't read from archive: %s", self->name, pmErrStr (rc)); } return FALSE; } meta = build_meta_if_necessary (self, result); if (meta) { cockpit_metrics_send_meta (COCKPIT_METRICS (self), meta, self->last == NULL); json_object_unref (meta); } build_samples (self, result); cockpit_metrics_send_data (COCKPIT_METRICS (self), timestamp_from_timeval (&result->timestamp)); if (self->last) pmFreeResult (self->last); self->last = result; } cockpit_metrics_flush_data (COCKPIT_METRICS (self)); return TRUE; } static gboolean units_equal (pmUnits *a, pmUnits *b) { return (a->scaleCount == b->scaleCount && a->scaleTime == b->scaleTime && a->scaleSpace == b->scaleSpace && a->dimCount == b->dimCount && a->dimTime == b->dimTime && a->dimSpace == b->dimSpace); } static gboolean units_convertible (pmUnits *a, pmUnits *b) { pmAtomValue dummy; dummy.d = 0; return pmConvScale (PM_TYPE_DOUBLE, &dummy, a, &dummy, b) >= 0; } static gboolean convert_metric_description (CockpitPcpMetrics *self, JsonNode *node, MetricInfo *info, int index, gboolean *not_found) { CockpitChannel *channel = COCKPIT_CHANNEL (self); const gchar *units; int rc; if (json_node_get_node_type (node) == JSON_NODE_OBJECT) { if (!cockpit_json_get_string (json_node_get_object (node), "name", NULL, &info->name) || info->name == NULL) { cockpit_channel_fail (channel, "protocol-error", "%s: invalid \"metrics\" option was specified (no name for metric %d)", self->name, index); return FALSE; } if (!cockpit_json_get_string (json_node_get_object (node), "units", NULL, &units)) { cockpit_channel_fail (channel, "protocol-error", "%s: invalid units for metric %s (not a string)", self->name, info->name); return FALSE; } if (!cockpit_json_get_string (json_node_get_object (node), "derive", NULL, &info->derive)) { cockpit_channel_fail (channel, "protocol-error", "%s: invalid derivation mode for metric %s (not a string)", self->name, info->name); return FALSE; } } else { cockpit_channel_fail (channel, "protocol-error", "%s: invalid \"metrics\" option was specified (not an object for metric %d)", self->name, index); return FALSE; } rc = pmLookupName (1, (char **)&info->name, &info->id); if (rc < 0) { if (not_found) { *not_found = TRUE; g_message ("%s: no such metric: %s: %s", self->name, info->name, pmErrStr (rc)); } else { cockpit_channel_fail (channel, "not-found", "%s: no such metric: %s: %s", self->name, info->name, pmErrStr (rc)); } return FALSE; } rc = pmLookupDesc (info->id, &info->desc); if (rc < 0) { if (not_found) { *not_found = TRUE; } else { cockpit_channel_fail (channel, "not-found", "%s: no such metric: %s: %s", self->name, info->name, pmErrStr (rc)); } return FALSE; } if (units) { if (my_pmParseUnitsStr (units, &info->units_buf, &info->factor) < 0) { cockpit_channel_fail (channel, "protocol-error", "%s: failed to parse units: %s", self->name, units); return FALSE; } if (!units_convertible (&info->desc.units, &info->units_buf)) { cockpit_channel_fail (channel, "protocol-error", "%s: can't convert metric %s to units %s", self->name, info->name, units); return FALSE; } if (info->factor != 1.0 || !units_equal (&info->desc.units, &info->units_buf)) info->units = &info->units_buf; } if (!info->units) { info->units = &info->desc.units; info->factor = 1.0; } return TRUE; } static gboolean prepare_current_context (CockpitPcpMetrics *self, gboolean *not_found) { CockpitChannel *channel = COCKPIT_CHANNEL (self); JsonObject *options; gchar **instances = NULL; gchar **omit_instances = NULL; JsonArray *metrics; gboolean ret = FALSE; int i; g_free (self->metrics); g_free (self->pmidlist); self->numpmid = 0; self->metrics = NULL; self->pmidlist = NULL; options = cockpit_channel_get_options (channel); /* "instances" option */ if (!cockpit_json_get_strv (options, "instances", NULL, (gchar ***)&instances)) { cockpit_channel_fail (channel, "protocol-error", "%s: invalid \"instances\" option (not an array of strings)", self->name); goto out; } /* "omit-instances" option */ if (!cockpit_json_get_strv (options, "omit-instances", NULL, (gchar ***)&omit_instances)) { cockpit_channel_fail (channel, "protocol-error", "%s: invalid \"omit-instances\" option (not an array of strings)", self->name); goto out; } /* "metrics" option */ if (!cockpit_json_get_array (options, "metrics", NULL, &metrics)) { cockpit_channel_fail (channel, "protocol-error", "%s: invalid \"metrics\" option was specified (not an array)", self->name); goto out; } if (metrics) self->numpmid = json_array_get_length (metrics); self->pmidlist = g_new0 (pmID, self->numpmid); self->metrics = g_new0 (MetricInfo, self->numpmid); for (i = 0; i < self->numpmid; i++) { MetricInfo *info = &self->metrics[i]; if (!convert_metric_description (self, json_array_get_element (metrics, i), info, i, not_found)) goto out; self->pmidlist[i] = info->id; if (info->desc.indom != PM_INDOM_NULL) { if (instances) { pmDelProfile (info->desc.indom, 0, NULL); for (int i = 0; instances[i]; i++) { int instid = pmLookupInDom (info->desc.indom, instances[i]); if (instid >= 0) pmAddProfile (info->desc.indom, 1, &instid); } } else if (omit_instances) { pmAddProfile (info->desc.indom, 0, NULL); for (int i = 0; omit_instances[i]; i++) { int instid = pmLookupInDom (info->desc.indom, omit_instances[i]); if (instid >= 0) pmDelProfile (info->desc.indom, 1, &instid); } } } } ret = TRUE; out: g_free (instances); g_free (omit_instances); return ret; } static void start_archive (CockpitPcpMetrics *self, gint64 timestamp); static gboolean add_archive (CockpitPcpMetrics *self, const gchar *name) { ArchiveInfo *info; pmLogLabel label; int rc; info = g_new0 (ArchiveInfo, 1); info->context = pmNewContext (PM_CONTEXT_ARCHIVE, name); if (info->context < 0) { if (info->context == -ENOENT) { g_debug ("%s: couldn't find pcp archive for %s", self->name, name); cockpit_channel_close (COCKPIT_CHANNEL (self), "not-found"); } else { cockpit_channel_fail (COCKPIT_CHANNEL (self), "internal-error", "%s: couldn't create pcp archive context for %s: %s", self->name, name, pmErrStr (info->context)); } g_free (info); return FALSE; } rc = pmGetArchiveLabel (&label); if (rc < 0) { cockpit_channel_fail (COCKPIT_CHANNEL (self), "internal-error", "%s: couldn't read archive label of %s: %s", self->name, name, pmErrStr (rc)); pmDestroyContext (info->context); g_free (info); return FALSE; } info->start = label.ll_start.tv_sec * 1000 + label.ll_start.tv_usec / 1000; self->archives = g_list_prepend (self->archives, info); return TRUE; } static gint cmp_archive_start (gconstpointer a, gconstpointer b) { const ArchiveInfo *a_info = a; const ArchiveInfo *b_info = b; if (a_info->start > b_info->start) return 1; else if (a_info->start < b_info->start) return -1; else return 0; } static gboolean prepare_archives (CockpitPcpMetrics *self, const gchar *name, gint64 timestamp) { gboolean ret = TRUE; GDir *dir; int count; GError *error = NULL; dir = g_dir_open (name, 0, &error); if (dir) { const gchar *entry; count = 0; while ((entry = g_dir_read_name (dir)) && count < 200) { if (g_str_has_suffix (entry, ".meta")) { gchar *path = g_build_filename (name, entry, NULL); path[strlen(path)-strlen(".meta")] = '\0'; if (!add_archive (self, path)) ret = FALSE; g_free (path); count += 1; } } g_dir_close (dir); } else if (g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_NOENT)) { if (!add_archive (self, name)) ret = FALSE; } else { cockpit_channel_fail (COCKPIT_CHANNEL (self), "internal-error", "%s: %s", name, error->message); ret = FALSE; } g_clear_error (&error); if (self->archives == NULL) { if (ret) cockpit_channel_close (COCKPIT_CHANNEL (self), "not-found"); return FALSE; } self->archives = g_list_sort (self->archives, cmp_archive_start); self->cur_archive = self->archives; start_archive (self, timestamp); return TRUE; } static void start_archive (CockpitPcpMetrics *self, gint64 timestamp) { CockpitChannel *channel = COCKPIT_CHANNEL (self); ArchiveInfo *info; struct timeval stamp; gboolean not_found; int rc; while (self->cur_archive && self->cur_archive->next && ((ArchiveInfo *)(self->cur_archive->next->data))->start < timestamp) self->cur_archive = self->cur_archive->next; again: if (self->cur_archive == NULL) { cockpit_channel_close (channel, NULL); return; } info = self->cur_archive->data; if (timestamp < info->start) timestamp = info->start; stamp.tv_sec = (timestamp / 1000); stamp.tv_usec = (timestamp % 1000) * 1000; rc = pmUseContext (info->context); if (rc < 0) { cockpit_channel_fail (channel, "internal-error", "%s: couldn't switch pcp context: %s", self->name, pmErrStr (rc)); return; } rc = pmSetMode (PM_MODE_INTERP | PM_XTB_SET(PM_TIME_MSEC), &stamp, self->interval); if (rc < 0) { cockpit_channel_fail (channel, "internal-error", "%s: couldn't set pcp mode: %s", self->name, pmErrStr (rc)); return; } not_found = TRUE; if (!prepare_current_context (self, ¬_found)) { if (not_found) { self->cur_archive = self->cur_archive->next; goto again; } return; } /* Make sure we send a meta message. */ if (self->last) pmFreeResult (self->last); self->last = NULL; g_assert (self->idler == 0); self->idler = g_idle_add (on_idle_batch, self); } static void next_archive (CockpitPcpMetrics *self) { self->cur_archive = self->cur_archive->next; start_archive (self, 0); } static gboolean ensure_pcp_conf (CockpitChannel *channel) { gboolean res = TRUE; gchar *prefix; gchar *conf; gchar *confpath = NULL; FILE *fp = NULL; /* Libpcp is prone to call exit(1) behind our backs when it can't find its config file, so we check here first. */ prefix = getenv("PCP_DIR"); conf = getenv("PCP_CONF"); if (conf == NULL) { if (prefix == NULL) confpath = g_strdup ("/etc/pcp.conf"); else confpath = g_strdup_printf ("%s/etc/pcp.conf", prefix); conf = confpath; } if (access((const char *)conf, R_OK) < 0 || (fp = fopen(conf, "r")) == NULL) { cockpit_channel_fail (channel, "internal-error", "could not access %s: %m", conf); res = FALSE; } if (fp) fclose (fp); g_free (confpath); return res; } static void cockpit_pcp_metrics_prepare (CockpitChannel *channel) { CockpitPcpMetrics *self = COCKPIT_PCP_METRICS (channel); JsonObject *options; const gchar *source; int type; char *name = NULL; gint64 timestamp; COCKPIT_CHANNEL_CLASS (cockpit_pcp_metrics_parent_class)->prepare (channel); options = cockpit_channel_get_options (channel); if (!ensure_pcp_conf (channel)) goto out; /* "source" option */ if (!cockpit_json_get_string (options, "source", NULL, &source)) { cockpit_channel_fail (channel, "protocol-error", "invalid \"source\" option for metrics channel"); goto out; } else if (!source) { cockpit_channel_fail (channel, "protocol-error", "no \"source\" option specified for metrics channel"); goto out; } else if (g_str_has_prefix (source, "/")) { type = PM_CONTEXT_ARCHIVE; name = g_strdup (source); } else if (g_str_has_prefix (source, "pcp-archive")) { gchar *dir = pmGetConfig("PCP_LOG_DIR"); gchar hostname[HOST_NAME_MAX + 1]; if (gethostname (hostname, HOST_NAME_MAX) < 0) { cockpit_channel_fail (channel, "internal-error", "error getting hostname: %m"); goto out; } hostname[HOST_NAME_MAX] = '\0'; type = PM_CONTEXT_ARCHIVE; name = g_strdup_printf ("%s/pmlogger/%s", dir, hostname); } else if (g_str_equal (source, "direct")) { type = PM_CONTEXT_LOCAL; name = NULL; } else if (g_str_equal (source, "pmcd")) { type = PM_CONTEXT_HOST; name = g_strdup ("local:"); } else { cockpit_channel_fail (channel, "not-supported", "unsupported \"source\" option specified for metrics: %s", source); goto out; } self->name = source; /* "timestamp" option */ if (!cockpit_json_get_int (options, "timestamp", 0, ×tamp)) { cockpit_channel_fail (channel, "protocol-error", "%s: invalid \"timestamp\" option", self->name); goto out; } if (timestamp / 1000 < G_MINLONG || timestamp / 1000 > G_MAXLONG) { cockpit_channel_fail (channel, "protocol-error", "%s: invalid \"timestamp\" value: %" G_GINT64_FORMAT, self->name, timestamp); goto out; } if (timestamp < 0) { struct timeval now; gettimeofday (&now, NULL); timestamp = (now.tv_sec * 1000 + now.tv_usec / 1000) + timestamp; } /* "limit" option */ if (!cockpit_json_get_int (options, "limit", G_MAXINT64, &self->limit)) { cockpit_channel_fail (channel, "protocol-error", "%s: invalid \"limit\" option", self->name); goto out; } else if (self->limit <= 0) { cockpit_channel_fail (channel, "protocol-error", "%s: invalid \"limit\" option value: %" G_GINT64_FORMAT, self->name, self->limit); goto out; } /* "interval" option */ if (!cockpit_json_get_int (options, "interval", 1000, &self->interval)) { cockpit_channel_fail (channel, "protocol-error", "%s: invalid \"interval\" option", self->name); goto out; } else if (self->interval <= 0 || self->interval > G_MAXINT) { cockpit_channel_fail (channel, "protocol-error", "%s: invalid \"interval\" value: %" G_GINT64_FORMAT, self->name, self->interval); goto out; } if (type == PM_CONTEXT_ARCHIVE) { if (!prepare_archives (self, name, timestamp)) goto out; } else { self->direct_context = pmNewContext(type, name); if (self->direct_context < 0) { if (self->direct_context == -ENOENT) { g_debug ("%s: couldn't create PCP context: %s", self->name, pmErrStr (self->direct_context)); cockpit_channel_close (channel, "not-supported"); } else { cockpit_channel_fail (channel, "internal-error", "%s: couldn't create PCP context: %s", self->name, pmErrStr (self->direct_context)); } goto out; } if (!prepare_current_context (self, NULL)) goto out; } if (type != PM_CONTEXT_ARCHIVE) cockpit_metrics_metronome (COCKPIT_METRICS (self), self->interval); cockpit_channel_ready (channel, NULL); out: g_free (name); } static void cockpit_pcp_metrics_dispose (GObject *object) { CockpitPcpMetrics *self = COCKPIT_PCP_METRICS (object); if (self->idler) { g_source_remove (self->idler); self->idler = 0; } if (self->last) { pmFreeResult (self->last); self->last = NULL; } for (GList *a = self->archives; a; a = a->next) { ArchiveInfo *info = a->data; if (info->context >= 0) pmDestroyContext (info->context); g_free (info); } g_list_free (self->archives); self->archives = NULL; if (self->direct_context >= 0) { pmDestroyContext (self->direct_context); self->direct_context = -1; } G_OBJECT_CLASS (cockpit_pcp_metrics_parent_class)->dispose (object); } static void cockpit_pcp_metrics_finalize (GObject *object) { CockpitPcpMetrics *self = COCKPIT_PCP_METRICS (object); g_free (self->metrics); g_free (self->pmidlist); G_OBJECT_CLASS (cockpit_pcp_metrics_parent_class)->finalize (object); } static void cockpit_pcp_metrics_class_init (CockpitPcpMetricsClass *klass) { GObjectClass *gobject_class = G_OBJECT_CLASS (klass); CockpitMetricsClass *metrics_class = COCKPIT_METRICS_CLASS (klass); CockpitChannelClass *channel_class = COCKPIT_CHANNEL_CLASS (klass); gobject_class->dispose = cockpit_pcp_metrics_dispose; gobject_class->finalize = cockpit_pcp_metrics_finalize; channel_class->prepare = cockpit_pcp_metrics_prepare; metrics_class->tick = cockpit_pcp_metrics_tick; } /* This is the version of pmParseUnitsStr as proposed here: fche/units-parse @ git://sourceware.org/git/pcpfans.git */ /* * Copyright (c) 2014 Red Hat. * Copyright (c) 1995 Silicon Graphics, Inc. All Rights Reserved. * * This library is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published * by the Free Software Foundation; either version 2.1 of the License, or * (at your option) any later version. * * This library 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 Lesser General Public * License for more details. */ #include #include #include #include // Parse a general "N $units" string into a pmUnits tuple and a multiplier. // $units can be a series of SCALE-UNIT^EXPONENT, each unit dimension appearing // at most once. // An internal variant of pmUnits, but without the narrow bitfields. // That way, we can tolerate intermediate arithmetic that goes out of // range of the 4-bit bitfields. typedef struct pmUnitsBig { int dimSpace; /* space dimension */ int dimTime; /* time dimension */ int dimCount; /* event dimension */ unsigned scaleSpace; /* one of PM_SPACE_* below */ unsigned scaleTime; /* one of PM_TIME_* below */ int scaleCount; /* one of PM_COUNT_* below */ } pmUnitsBig; static int __pmParseUnitsStrPart(const char *str, const char *str_end, pmUnitsBig *out, double *multiplier) { int sts = 0; unsigned i; const char *ptr; // scanning along str enum dimension_t {d_none,d_space,d_time,d_count} dimension; struct unit_keyword_t { const char *keyword; int scale; }; static const struct unit_keyword_t time_keywords[] = { { "nanoseconds", PM_TIME_NSEC }, { "nanosecond", PM_TIME_NSEC }, { "nanosec", PM_TIME_NSEC }, { "ns", PM_TIME_NSEC }, { "microseconds", PM_TIME_USEC }, { "microsecond", PM_TIME_USEC }, { "microsec", PM_TIME_USEC }, { "us", PM_TIME_USEC }, { "milliseconds", PM_TIME_MSEC }, { "millisecond", PM_TIME_MSEC }, { "millisec", PM_TIME_MSEC }, { "ms", PM_TIME_MSEC }, { "seconds", PM_TIME_SEC }, { "second", PM_TIME_SEC }, { "sec", PM_TIME_SEC }, { "s", PM_TIME_SEC }, { "minutes", PM_TIME_MIN }, { "minute", PM_TIME_MIN }, { "min", PM_TIME_MIN }, { "hours", PM_TIME_HOUR }, { "hour", PM_TIME_HOUR }, { "hr", PM_TIME_HOUR }, { "time-0", 0 }, /* { "time-1", 1 }, */ { "time-2", 2 }, { "time-3", 3 }, { "time-4", 4 }, { "time-5", 5 }, { "time-6", 6 }, { "time-7", 7 }, { "time-8", 8 }, { "time-9", 9 }, { "time-10", 10 }, { "time-11", 11 }, { "time-12", 12 }, { "time-13", 13 }, { "time-14", 14 }, { "time-15", 15 }, { "time-1", 1 }, }; const size_t num_time_keywords = sizeof(time_keywords) / sizeof(time_keywords[0]); static const struct unit_keyword_t space_keywords[] = { { "bytes", PM_SPACE_BYTE }, { "byte", PM_SPACE_BYTE }, { "Kbytes", PM_SPACE_KBYTE }, { "Kbyte", PM_SPACE_KBYTE }, { "Kilobytes", PM_SPACE_KBYTE }, { "Kilobyte", PM_SPACE_KBYTE }, { "KB", PM_SPACE_KBYTE }, { "Mbytes", PM_SPACE_MBYTE }, { "Mbyte", PM_SPACE_MBYTE }, { "Megabytes", PM_SPACE_MBYTE }, { "Megabyte", PM_SPACE_MBYTE }, { "MB", PM_SPACE_MBYTE }, { "Gbytes", PM_SPACE_GBYTE }, { "Gbyte", PM_SPACE_GBYTE }, { "Gigabytes", PM_SPACE_GBYTE }, { "Gigabyte", PM_SPACE_GBYTE }, { "GB", PM_SPACE_GBYTE }, { "Tbytes", PM_SPACE_TBYTE }, { "Tbyte", PM_SPACE_TBYTE }, { "Terabytes", PM_SPACE_TBYTE }, { "Terabyte", PM_SPACE_TBYTE }, { "TB", PM_SPACE_TBYTE }, { "Pbytes", PM_SPACE_PBYTE }, { "Pbyte", PM_SPACE_PBYTE }, { "Petabytes", PM_SPACE_PBYTE }, { "Petabyte", PM_SPACE_PBYTE }, { "PB", PM_SPACE_PBYTE }, { "Ebytes", PM_SPACE_EBYTE }, { "Ebyte", PM_SPACE_EBYTE }, { "Exabytes", PM_SPACE_EBYTE }, { "Exabyte", PM_SPACE_EBYTE }, { "EB", PM_SPACE_EBYTE }, { "space-0", 0 }, /* { "space-1", 1 }, */ { "space-2", 2 }, { "space-3", 3 }, { "space-4", 4 }, { "space-5", 5 }, { "space-6", 6 }, { "space-7", 7 }, { "space-8", 8 }, { "space-9", 9 }, { "space-10", 10 }, { "space-11", 11 }, { "space-12", 12 }, { "space-13", 13 }, { "space-14", 14 }, { "space-15", 15 }, { "space-1", 1 }, }; const size_t num_space_keywords = sizeof(space_keywords) / sizeof(space_keywords[0]); static const struct unit_keyword_t count_keywords[] = { { "count x 10^-8", -8 }, { "count x 10^-7", -7 }, { "count x 10^-6", -6 }, { "count x 10^-5", -5 }, { "count x 10^-4", -4 }, { "count x 10^-3", -3 }, { "count x 10^-2", -2 }, { "count x 10^-1", -1 }, /* { "count", 0 }, { "counts", 0 }, */ /* { "count x 10", 1 },*/ { "count x 10^2", 2 }, { "count x 10^3", 3 }, { "count x 10^4", 4 }, { "count x 10^5", 5 }, { "count x 10^6", 6 }, { "count x 10^7", 7 }, { "count x 10", 1 }, { "counts", 0 }, { "count", 0 }, // NB: we don't support the anomalous "x 10^SCALE" syntax for the dimCount=0 case. }; const size_t num_count_keywords = sizeof(count_keywords) / sizeof(count_keywords[0]); static const struct unit_keyword_t exponent_keywords[] = { { "^-8", -8 }, { "^-7", -7 }, { "^-6", -6 }, { "^-5", -5 }, { "^-4", -4 }, { "^-3", -3 }, { "^-2", -2 }, { "^-1", -1 }, { "^0", 0 }, /*{ "^1", 1 }, */ { "^2", 2 }, { "^3", 3 }, { "^4", 4 }, { "^5", 5 }, { "^6", 6 }, { "^7", 7 }, // NB: the following larger exponents are enabled by use of pmUnitsBig above. // They happen to be necessary because pmUnitsStr emits foo-dim=-8 as "/ foo^8", // so the denominator could encounter wider-than-bitfield exponents. { "^8", 8 }, { "^9", 9 }, { "^10", 10 }, { "^11", 11 }, { "^12", 12 }, { "^13", 13 }, { "^14", 14 }, { "^15", 15 }, { "^1", 1 }, }; const size_t num_exponent_keywords = sizeof(exponent_keywords) / sizeof(exponent_keywords[0]); *multiplier = 1.0; memset (out, 0, sizeof (*out)); ptr = str; while (ptr != str_end) { // parse whole string assert (*ptr != '\0'); if (isspace (*ptr)) { // skip whitespace ptr ++; continue; } if (*ptr == '-' || *ptr == '.' || isdigit(*ptr)) { // possible floating-point number // parse it with strtod(3). char *newptr; errno = 0; double m = strtod(ptr, &newptr); if (errno || newptr == ptr || newptr > str_end) { sts = PM_ERR_CONV; goto out; } ptr = newptr; *multiplier *= m; continue; } dimension = d_none; // classify dimension of base unit // match & skip over keyword (followed by space, ^, or EOL) #define streqskip(q) (((ptr+strlen(q) <= str_end) && \ (strncasecmp(ptr,q,strlen(q))==0) && \ ((isspace(*(ptr+strlen(q)))) || \ (*(ptr+strlen(q))=='^') || \ (ptr+strlen(q)==str_end))) \ ? (ptr += strlen(q), 1) : 0) // parse base unit, only once per input string. We don't support // "microsec millisec", as that would require arithmetic on the scales. // We could support "sec sec" (and turn that into sec^2) in the future. for (i=0; idimTime == 0 && streqskip (time_keywords[i].keyword)) { out->scaleTime = time_keywords[i].scale; dimension = d_time; } for (i=0; idimSpace == 0 && streqskip (space_keywords[i].keyword)) { out->scaleSpace = space_keywords[i].scale; dimension = d_space; } for (i=0; idimCount == 0 && streqskip (count_keywords[i].keyword)) { out->scaleCount = count_keywords[i].scale; dimension = d_count; } // parse optional dimension exponent switch (dimension) { case d_none: // unrecognized base unit, punt! sts = PM_ERR_CONV; goto out; case d_time: if (ptr == str_end || isspace(*ptr)) { out->dimTime = 1; } else { for (i=0; idimTime = exponent_keywords[i].scale; break; } } break; case d_space: if (ptr == str_end || isspace(*ptr)) { out->dimSpace = 1; } else { for (i=0; idimSpace = exponent_keywords[i].scale; break; } } break; case d_count: if (ptr == str_end || isspace(*ptr)) { out->dimCount = 1; } else { for (i=0; idimCount = exponent_keywords[i].scale; break; } } break; } // fall through to next unit^exponent bit, if any } out: return sts; } // Parse a general "N $units / M $units" string into a pmUnits tuple and a multiplier. static int my_pmParseUnitsStr(const char *str, pmUnits *out, double *multiplier) { const char *slash; double dividend_mult, divisor_mult; pmUnitsBig dividend, divisor; int sts; int dim; assert (str); assert (out); assert (multiplier); memset (out, 0, sizeof (*out)); // Parse the dividend and divisor separately slash = strchr (str, '/'); if (slash == NULL) { sts = __pmParseUnitsStrPart(str, str+strlen(str), & dividend, & dividend_mult); if (sts < 0) goto out; // empty string for nonexistent denominator; will just return (0,0,0,0,0,0)*1.0 sts = __pmParseUnitsStrPart(str+strlen(str), str+strlen(str), & divisor, & divisor_mult); if (sts < 0) goto out; } else { sts = __pmParseUnitsStrPart(str, slash, & dividend, & dividend_mult); if (sts < 0) goto out; sts = __pmParseUnitsStrPart(slash+1, str+strlen(str), & divisor, & divisor_mult); if (sts < 0) goto out; } // Compute the quotient dimensionality, check for possible bitfield overflow. dim = dividend.dimSpace - divisor.dimSpace; if (dim < -8 || dim > 7) { sts = PM_ERR_CONV; goto out; } else { out->dimSpace = dim; } dim = dividend.dimTime - divisor.dimTime; if (dim < -8 || dim > 7) { sts = PM_ERR_CONV; goto out; } else { out->dimTime = dim; } dim = dividend.dimCount - divisor.dimCount; if (dim < -8 || dim > 7) { sts = PM_ERR_CONV; goto out; } else { out->dimCount = dim; } // Compute the individual scales. In theory, we have considerable // freedom here, because we are also outputting a multiplier. We // could just set all out->scale* to 0, and accumulate the // compensating scaling multipliers there. But in order to // fulfill the testing-oriented invariant: // // for all valid pmUnits u: // pmParseUnitsStr(pmUnitsStr(u), out_u, out_multiplier) succeeds, and // out_u == u, and // out_multiplier == 1.0 // // we need to propagate scales to some extent. It is helpful to // realize that pmUnitsStr() never generates multiplier literals, // nor the same dimension in the dividend and divisor. *multiplier = divisor_mult / dividend_mult; // NB: note reciprocation if (dividend.dimSpace == 0 && divisor.dimSpace != 0) out->scaleSpace = divisor.scaleSpace; else if (divisor.dimSpace == 0 && dividend.dimSpace != 0) out->scaleSpace = dividend.scaleSpace; else { // both have space dimension; must compute a scale/multiplier out->scaleSpace = PM_SPACE_BYTE; *multiplier *= pow (pow (1024.0, (double) dividend.scaleSpace), -(double)dividend.dimSpace); *multiplier *= pow (pow (1024.0, (double) divisor.scaleSpace), (double)divisor.dimSpace); if (out->dimSpace == 0) // became dimensionless? out->scaleSpace = 0; } if (dividend.dimCount == 0 && divisor.dimCount != 0) out->scaleCount = divisor.scaleCount; else if (divisor.dimCount == 0 && dividend.dimCount != 0) out->scaleCount = dividend.scaleCount; else { // both have count dimension; must compute a scale/multiplier out->scaleCount = PM_COUNT_ONE; *multiplier *= pow (pow (10.0, (double) dividend.scaleCount), -(double)dividend.dimCount); *multiplier *= pow (pow (10.0, (double) divisor.scaleCount), (double)divisor.dimCount); if (out->dimCount == 0) // became dimensionless? out->scaleCount = 0; } if (dividend.dimTime == 0 && divisor.dimTime != 0) out->scaleTime = divisor.scaleTime; else if (divisor.dimTime == 0 && dividend.dimTime != 0) out->scaleTime = dividend.scaleTime; else { // both have time dimension; must compute a scale/multiplier out->scaleTime = PM_TIME_SEC; static const double time_scales [] = {[PM_TIME_NSEC] = 0.000000001, [PM_TIME_USEC] = 0.000001, [PM_TIME_MSEC] = 0.001, [PM_TIME_SEC] = 1, [PM_TIME_MIN] = 60, [PM_TIME_HOUR] = 3600 }; // guaranteed by __pmParseUnitsStrPart; ensure in-range array access assert (dividend.scaleTime >= PM_TIME_NSEC && dividend.scaleTime <= PM_TIME_HOUR); assert (divisor.scaleTime >= PM_TIME_NSEC && divisor.scaleTime <= PM_TIME_HOUR); *multiplier *= pow (time_scales[dividend.scaleTime], -(double)dividend.dimTime); *multiplier *= pow (time_scales[divisor.scaleTime], (double)divisor.dimTime); if (out->dimTime == 0) // became dimensionless? out->scaleTime = 0; } out: if (sts < 0) { memset (out, 0, sizeof (*out)); // clear partially filled in pmUnits *multiplier = 1.0; } return sts; }