/*
* 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 .
*/
#include "config.h"
#include
#include
#include "cockpitmetrics.h"
#include "cockpitinternalmetrics.h"
#include "cockpitsamples.h"
#include "cockpitcpusamples.h"
#include "cockpitmemorysamples.h"
#include "cockpitblocksamples.h"
#include "cockpitnetworksamples.h"
#include "cockpitmountsamples.h"
#include "cockpitcgroupsamples.h"
#include "cockpitdisksamples.h"
#include "common/cockpitjson.h"
/**
* CockpitInternalMetrics:
*
* A #CockpitMetrics channel that pulls data from internal sources
*/
static void cockpit_samples_interface_init (CockpitSamplesIface *iface);
#define COCKPIT_INTERNAL_METRICS(o) \
(G_TYPE_CHECK_INSTANCE_CAST ((o), COCKPIT_TYPE_INTERNAL_METRICS, CockpitInternalMetrics))
typedef enum {
CPU_SAMPLER = 1 << 0,
MEMORY_SAMPLER = 1 << 1,
BLOCK_SAMPLER = 1 << 2,
NETWORK_SAMPLER = 1 << 3,
MOUNT_SAMPLER = 1 << 4,
CGROUP_SAMPLER = 1 << 5,
DISK_SAMPLER = 1 << 6
} SamplerSet;
typedef struct {
const gchar *name;
const gchar *units;
const gchar *semantics;
gboolean instanced;
SamplerSet sampler;
} MetricDescription;
static MetricDescription metric_descriptions[] = {
{ "cpu.basic.nice", "millisec", "counter", FALSE, CPU_SAMPLER },
{ "cpu.basic.user", "millisec", "counter", FALSE, CPU_SAMPLER },
{ "cpu.basic.system", "millisec", "counter", FALSE, CPU_SAMPLER },
{ "cpu.basic.iowait", "millisec", "counter", FALSE, CPU_SAMPLER },
{ "memory.free", "bytes", "instant", FALSE, MEMORY_SAMPLER },
{ "memory.used", "bytes", "instant", FALSE, MEMORY_SAMPLER },
{ "memory.cached", "bytes", "instant", FALSE, MEMORY_SAMPLER },
{ "memory.swap-used", "bytes", "instant", FALSE, MEMORY_SAMPLER },
{ "block.device.read", "bytes", "counter", TRUE, BLOCK_SAMPLER },
{ "block.device.written", "bytes", "counter", TRUE, BLOCK_SAMPLER },
{ "disk.all.read", "bytes", "counter", FALSE, DISK_SAMPLER },
{ "disk.all.written", "bytes", "counter", FALSE, DISK_SAMPLER },
{ "network.all.rx", "bytes", "counter", FALSE, NETWORK_SAMPLER },
{ "network.all.tx", "bytes", "counter", FALSE, NETWORK_SAMPLER },
{ "network.interface.rx", "bytes", "counter", TRUE, NETWORK_SAMPLER },
{ "network.interface.tx", "bytes", "counter", TRUE, NETWORK_SAMPLER },
{ "mount.total", "bytes", "instant", TRUE, MOUNT_SAMPLER },
{ "mount.used", "bytes", "instant", TRUE, MOUNT_SAMPLER },
{ "cgroup.memory.usage", "bytes", "instant", TRUE, CGROUP_SAMPLER },
{ "cgroup.memory.limit", "bytes", "instant", TRUE, CGROUP_SAMPLER },
{ "cgroup.memory.sw-usage", "bytes", "instant", TRUE, CGROUP_SAMPLER },
{ "cgroup.memory.sw-limit", "bytes", "instant", TRUE, CGROUP_SAMPLER },
{ "cgroup.cpu.usage", "millisec", "counter", TRUE, CGROUP_SAMPLER },
{ "cgroup.cpu.shares", "count", "instant", TRUE, CGROUP_SAMPLER },
{ NULL }
};
static MetricDescription *
find_metric_description (const gchar *name)
{
for (MetricDescription *d = metric_descriptions; d->name; d++)
{
if (g_strcmp0 (d->name, name) == 0)
return d;
}
return NULL;
}
typedef struct {
gboolean seen;
int index;
double value;
} InstanceInfo;
typedef struct {
MetricDescription *desc;
const gchar *derive;
GHashTable *instances;
double value;
} MetricInfo;
typedef struct {
CockpitMetrics parent;
const gchar *name;
gint64 interval;
int n_metrics;
MetricInfo *metrics;
const gchar **instances;
const gchar **omit_instances;
SamplerSet samplers;
gboolean need_meta;
} CockpitInternalMetrics;
typedef struct {
CockpitMetricsClass parent_class;
} CockpitInternalMetricsClass;
G_DEFINE_TYPE_WITH_CODE (CockpitInternalMetrics, cockpit_internal_metrics, COCKPIT_TYPE_METRICS,
G_IMPLEMENT_INTERFACE (COCKPIT_TYPE_SAMPLES,
cockpit_samples_interface_init))
static void
cockpit_internal_metrics_init (CockpitInternalMetrics *self)
{
}
static gint64
timestamp_from_timeval (struct timeval *tv)
{
return tv->tv_sec * 1000 + tv->tv_usec / 1000;
}
static void
send_meta (CockpitInternalMetrics *self)
{
JsonArray *metrics;
JsonObject *metric;
JsonObject *root;
struct timeval now_timeval;
gint64 now;
gettimeofday (&now_timeval, NULL);
now = timestamp_from_timeval (&now_timeval);
root = json_object_new ();
json_object_set_int_member (root, "timestamp", now);
json_object_set_int_member (root, "now", now);
json_object_set_int_member (root, "interval", self->interval);
metrics = json_array_new ();
for (int i = 0; i < self->n_metrics; i++)
{
MetricInfo *info = &self->metrics[i];
metric = json_object_new ();
/* Name and derivation mode
*/
json_object_set_string_member (metric, "name", info->desc->name);
if (info->derive)
json_object_set_string_member (metric, "derive", info->derive);
/* Instances
*/
if (info->desc->instanced)
{
GHashTableIter iter;
gpointer key, value;
int index;
JsonArray *instances = json_array_new ();
g_hash_table_iter_init (&iter, info->instances);
index = 0;
while (g_hash_table_iter_next (&iter, &key, &value))
{
const gchar *name = key;
InstanceInfo *inst = value;
/* 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, name);
json_array_add_element (instances, string_element);
}
inst->index = index++;
}
json_object_set_array_member (metric, "instances", instances);
}
/* Units and semantics
*/
json_object_set_string_member (metric, "units", info->desc->units);
json_object_set_string_member (metric, "semantics", info->desc->semantics);
json_array_add_object_element (metrics, metric);
}
json_object_set_array_member (root, "metrics", metrics);
cockpit_metrics_send_meta (COCKPIT_METRICS (self), root, FALSE);
json_object_unref (root);
}
static void
cockpit_internal_metrics_sample (CockpitSamples *samples,
const gchar *metric,
const gchar *instance,
gint64 value)
{
CockpitInternalMetrics *self = COCKPIT_INTERNAL_METRICS (samples);
for (int i = 0; i < self->n_metrics; i++)
{
MetricInfo *info = &self->metrics[i];
if (g_strcmp0 (metric, info->desc->name) != 0)
continue;
if (info->desc->instanced)
{
InstanceInfo *inst = g_hash_table_lookup (info->instances, instance);
if (inst == NULL)
{
g_debug ("%s + %s", metric, instance);
inst = g_new0 (InstanceInfo, 1);
g_hash_table_insert (info->instances, g_strdup (instance), inst);
self->need_meta = TRUE;
}
inst->seen = TRUE;
inst->value = value;
}
else
info->value = value;
}
}
static void
instance_reset (gpointer key,
gpointer value,
gpointer user_data)
{
InstanceInfo *info = value;
info->seen = FALSE;
}
static gboolean
instance_unseen (gpointer key,
gpointer value,
gpointer user_data)
{
InstanceInfo *info = value;
return !info->seen;
}
static void
cockpit_internal_metrics_tick (CockpitMetrics *metrics,
gint64 timestamp)
{
CockpitInternalMetrics *self = (CockpitInternalMetrics *)metrics;
struct timeval now_timeval;
gint64 now;
gettimeofday (&now_timeval, NULL);
now = timestamp_from_timeval (&now_timeval);
/* Reset samples
*/
for (int i = 0; i < self->n_metrics; i++)
{
MetricInfo *info = &self->metrics[i];
if (info->desc->instanced)
g_hash_table_foreach (info->instances, instance_reset, NULL);
else
info->value = NAN;
}
/* Sample
*/
if (self->samplers & CPU_SAMPLER)
cockpit_cpu_samples (COCKPIT_SAMPLES (self));
if (self->samplers & MEMORY_SAMPLER)
cockpit_memory_samples (COCKPIT_SAMPLES (self));
if (self->samplers & BLOCK_SAMPLER)
cockpit_block_samples (COCKPIT_SAMPLES (self));
if (self->samplers & NETWORK_SAMPLER)
cockpit_network_samples (COCKPIT_SAMPLES (self));
if (self->samplers & MOUNT_SAMPLER)
cockpit_mount_samples (COCKPIT_SAMPLES (self));
if (self->samplers & CGROUP_SAMPLER)
cockpit_cgroup_samples (COCKPIT_SAMPLES (self));
if (self->samplers & DISK_SAMPLER)
cockpit_disk_samples (COCKPIT_SAMPLES (self));
/* Check for disappeared instances
*/
for (int i = 0; i < self->n_metrics; i++)
{
MetricInfo *info = &self->metrics[i];
if (info->desc->instanced)
if (g_hash_table_foreach_remove (info->instances, instance_unseen, NULL) > 0)
self->need_meta = TRUE;
}
/* Send a meta message if necessary. This will also allocate a new
buffer and setup the instance indices.
*/
if (self->need_meta)
{
send_meta (self);
self->need_meta = FALSE;
}
/* Ship them out
*/
double **buffer = cockpit_metrics_get_data_buffer (COCKPIT_METRICS (self));
for (int i = 0; i < self->n_metrics; i++)
{
MetricInfo *info = &self->metrics[i];
if (info->desc->instanced)
{
GHashTableIter iter;
gpointer key, value;
g_hash_table_iter_init (&iter, info->instances);
while (g_hash_table_iter_next (&iter, &key, &value))
{
InstanceInfo *inst = value;
buffer[i][inst->index] = inst->value;
}
}
else
buffer[i][0] = info->value;
}
cockpit_metrics_send_data (COCKPIT_METRICS (self), now);
cockpit_metrics_flush_data (COCKPIT_METRICS (self));
}
static gboolean
convert_metric_description (CockpitInternalMetrics *self,
JsonNode *node,
MetricInfo *info,
int index)
{
CockpitChannel *channel = COCKPIT_CHANNEL (self);
const gchar *name;
const gchar *units;
if (json_node_get_node_type (node) == JSON_NODE_OBJECT)
{
if (!cockpit_json_get_string (json_node_get_object (node), "name", NULL, &name)
|| name == NULL)
{
cockpit_channel_fail (channel, "protocol-error",
"invalid \"metrics\" option was specified (no name for metric %d)", index);
return FALSE;
}
if (!cockpit_json_get_string (json_node_get_object (node), "units", NULL, &units))
{
cockpit_channel_fail (channel, "protocol-error",
"invalid units for metric %s (not a string)", name);
return FALSE;
}
if (!cockpit_json_get_string (json_node_get_object (node), "derive", NULL, &info->derive))
{
cockpit_channel_fail (channel, "protocol-error",
"invalid derivation mode for metric %s (not a string)", name);
return FALSE;
}
}
else
{
cockpit_channel_fail (channel, "protocol-error",
"invalid \"metrics\" option was specified (not an object for metric %d)", index);
return FALSE;
}
MetricDescription *desc = find_metric_description (name);
if (desc == NULL)
{
g_message ("unknown internal metric %s", name);
}
else
{
if (units && g_strcmp0 (desc->units, units) != 0)
{
cockpit_channel_fail (channel, "protocol-error",
"%s has units %s, not %s", name, desc->units, units);
return FALSE;
}
if (desc->instanced)
info->instances = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
info->desc = desc;
self->samplers |= desc->sampler;
}
return TRUE;
}
static void
cockpit_internal_metrics_prepare (CockpitChannel *channel)
{
CockpitInternalMetrics *self = COCKPIT_INTERNAL_METRICS (channel);
JsonObject *options;
JsonArray *metrics;
int i;
COCKPIT_CHANNEL_CLASS (cockpit_internal_metrics_parent_class)->prepare (channel);
options = cockpit_channel_get_options (channel);
/* "instances" option */
if (!cockpit_json_get_strv (options, "instances", NULL, (gchar ***)&self->instances))
{
cockpit_channel_fail (channel, "protocol-error",
"invalid \"instances\" option (not an array of strings)");
return;
}
/* "omit-instances" option */
if (!cockpit_json_get_strv (options, "omit-instances", NULL, (gchar ***)&self->omit_instances))
{
cockpit_channel_fail (channel, "protocol-error",
"invalid \"omit-instances\" option (not an array of strings)");
return;
}
/* "metrics" option */
self->n_metrics = 0;
if (!cockpit_json_get_array (options, "metrics", NULL, &metrics))
{
cockpit_channel_fail (channel, "protocol-error",
"invalid \"metrics\" option was specified (not an array)");
return;
}
if (metrics)
self->n_metrics = json_array_get_length (metrics);
self->metrics = g_new0 (MetricInfo, self->n_metrics);
for (i = 0; i < self->n_metrics; i++)
{
MetricInfo *info = &self->metrics[i];
if (!convert_metric_description (self, json_array_get_element (metrics, i), info, i))
return;
if (!info->desc)
{
cockpit_channel_close (channel, "not-supported");
return;
}
}
/* "interval" option */
if (!cockpit_json_get_int (options, "interval", 1000, &self->interval))
{
cockpit_channel_fail (channel, "protocol-error", "invalid \"interval\" option");
return;
}
else if (self->interval <= 0 || self->interval > G_MAXINT)
{
cockpit_channel_fail (channel, "protocol-error",
"invalid \"interval\" value: %" G_GINT64_FORMAT, self->interval);
return;
}
self->need_meta = TRUE;
cockpit_metrics_metronome (COCKPIT_METRICS (self), self->interval);
cockpit_channel_ready (channel, NULL);
}
static void
cockpit_internal_metrics_dispose (GObject *object)
{
#if 0
CockpitInternalMetrics *self = COCKPIT_INTERNAL_METRICS (object);
#endif
G_OBJECT_CLASS (cockpit_internal_metrics_parent_class)->dispose (object);
}
static void
cockpit_internal_metrics_finalize (GObject *object)
{
CockpitInternalMetrics *self = COCKPIT_INTERNAL_METRICS (object);
g_free (self->instances);
g_free (self->omit_instances);
g_free (self->metrics);
G_OBJECT_CLASS (cockpit_internal_metrics_parent_class)->finalize (object);
}
static void
cockpit_internal_metrics_class_init (CockpitInternalMetricsClass *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_internal_metrics_dispose;
gobject_class->finalize = cockpit_internal_metrics_finalize;
channel_class->prepare = cockpit_internal_metrics_prepare;
metrics_class->tick = cockpit_internal_metrics_tick;
}
static void
cockpit_samples_interface_init (CockpitSamplesIface *iface)
{
iface->sample = cockpit_internal_metrics_sample;
}