/* Copyright (c) 2020-2021, The Tor Project, Inc. */
/* See LICENSE for licensing information */
/**
* @file hs_metrics.c
* @brief Onion service metrics exposed through the MetricsPort
**/
#define HS_METRICS_ENTRY_PRIVATE
#include "orconfig.h"
#include "lib/malloc/malloc.h"
#include "lib/container/smartlist.h"
#include "lib/metrics/metrics_store.h"
#include "lib/log/util_bug.h"
#include "feature/hs/hs_metrics.h"
#include "feature/hs/hs_metrics_entry.h"
#include "feature/hs/hs_service.h"
/** Return a static buffer pointer that contains the port as a string.
*
* Subsequent call to this function invalidates the previous buffer. */
static const char *
port_to_str(const uint16_t port)
{
static char buf[8];
tor_snprintf(buf, sizeof(buf), "%u", port);
return buf;
}
/** Add a new metric to the metrics store of the service.
*
* metric is the index of the metric in the base_metrics array.
*/
static void
add_metric_with_labels(hs_service_t *service, hs_metrics_key_t metric,
bool port_as_label, uint16_t port)
{
metrics_store_t *store;
const char **error_reasons = NULL;
size_t num_error_reasons = 0;
tor_assert(service);
if (BUG(metric >= base_metrics_size))
return;
store = service->metrics.store;
/* Check whether the current metric is an error metric, because error metrics
* require an additional `reason` label. */
switch (metric) {
case HS_METRICS_NUM_REJECTED_INTRO_REQ:
error_reasons = hs_metrics_intro_req_error_reasons;
num_error_reasons = hs_metrics_intro_req_error_reasons_size;
break;
case HS_METRICS_NUM_FAILED_RDV:
error_reasons = hs_metrics_rend_error_reasons;
num_error_reasons = hs_metrics_rend_error_reasons_size;
break;
/* Fall through for all other metrics, as they don't need a
* reason label. */
case HS_METRICS_NUM_INTRODUCTIONS: FALLTHROUGH;
case HS_METRICS_APP_WRITE_BYTES: FALLTHROUGH;
case HS_METRICS_APP_READ_BYTES: FALLTHROUGH;
case HS_METRICS_NUM_ESTABLISHED_RDV: FALLTHROUGH;
case HS_METRICS_NUM_RDV: FALLTHROUGH;
case HS_METRICS_NUM_ESTABLISHED_INTRO: FALLTHROUGH;
case HS_METRICS_POW_NUM_PQUEUE_RDV: FALLTHROUGH;
case HS_METRICS_POW_SUGGESTED_EFFORT: FALLTHROUGH;
case HS_METRICS_INTRO_CIRC_BUILD_TIME: FALLTHROUGH;
case HS_METRICS_REND_CIRC_BUILD_TIME: FALLTHROUGH;
default:
break;
}
/* We don't need a reason label for this metric */
if (!num_error_reasons) {
metrics_store_entry_t *entry = metrics_store_add(
store, base_metrics[metric].type, base_metrics[metric].name,
base_metrics[metric].help, base_metrics[metric].bucket_count,
base_metrics[metric].buckets);
metrics_store_entry_add_label(entry,
metrics_format_label("onion", service->onion_address));
if (port_as_label) {
metrics_store_entry_add_label(entry,
metrics_format_label("port", port_to_str(port)));
}
return;
}
tor_assert(error_reasons);
/* Add entries with reason as label. We need one metric line per
* reason. */
for (size_t i = 0; i < num_error_reasons; ++i) {
metrics_store_entry_t *entry =
metrics_store_add(store, base_metrics[metric].type,
base_metrics[metric].name,
base_metrics[metric].help,
base_metrics[metric].bucket_count,
base_metrics[metric].buckets);
/* Add labels to the entry. */
metrics_store_entry_add_label(entry,
metrics_format_label("onion", service->onion_address));
metrics_store_entry_add_label(entry,
metrics_format_label("reason", error_reasons[i]));
if (port_as_label) {
metrics_store_entry_add_label(entry,
metrics_format_label("port", port_to_str(port)));
}
}
}
/** Initialize a metrics store for the given service.
*
* Essentially, this goes over the base_metrics array and adds them all to the
* store set with their label(s) if any. */
static void
init_store(hs_service_t *service)
{
tor_assert(service);
for (size_t i = 0; i < base_metrics_size; ++i) {
/* Add entries with port as label. We need one metric line per port. */
if (base_metrics[i].port_as_label && service->config.ports) {
SMARTLIST_FOREACH_BEGIN(service->config.ports,
const hs_port_config_t *, p) {
add_metric_with_labels(service, base_metrics[i].key, true,
p->virtual_port);
} SMARTLIST_FOREACH_END(p);
} else {
add_metric_with_labels(service, base_metrics[i].key, false, 0);
}
}
}
/** Update the metrics key entry in the store in the given service. The port,
* if non 0, and the reason label, if non NULL, are used to find the correct
* metrics entry. The value n is the value used to update the entry. */
void
hs_metrics_update_by_service(const hs_metrics_key_t key,
const hs_service_t *service,
uint16_t port, const char *reason,
int64_t n, int64_t obs, bool reset)
{
tor_assert(service);
/* Get the metrics entry in the store. */
smartlist_t *entries = metrics_store_get_all(service->metrics.store,
base_metrics[key].name);
if (BUG(!entries)) {
return;
}
/* We need to find the right metrics entry by finding the port label if any.
*
* XXX: This is not the most optimal due to the string format. Maybe at some
* point turn this into a kvline and a map in a metric entry? */
SMARTLIST_FOREACH_BEGIN(entries, metrics_store_entry_t *, entry) {
if ((port == 0 ||
metrics_store_entry_has_label(
entry, metrics_format_label("port", port_to_str(port)))) &&
((!reason || metrics_store_entry_has_label(
entry, metrics_format_label("reason", reason))))) {
if (reset) {
metrics_store_entry_reset(entry);
}
if (metrics_store_entry_is_histogram(entry)) {
metrics_store_hist_entry_update(entry, n, obs);
} else {
metrics_store_entry_update(entry, n);
}
break;
}
} SMARTLIST_FOREACH_END(entry);
}
/** Update the metrics key entry in the store of a service identified by the
* given identity public key. The port, if non 0, and the reason label, if non
* NULL, are used to find the correct metrics entry. The value n is the value
* used to update the entry.
*
* This is used by callsite that have access to the key but not the service
* object so an extra lookup is done to find the service. */
void
hs_metrics_update_by_ident(const hs_metrics_key_t key,
const ed25519_public_key_t *ident_pk,
const uint16_t port, const char *reason,
int64_t n, int64_t obs, bool reset)
{
hs_service_t *service;
if (!ident_pk) {
/* We can end up here in case this is used from a failure/closing path for
* which we might not have any identity key attacehed to a circuit or
* connection yet. Simply don't assume we have one. */
return;
}
service = hs_service_find(ident_pk);
if (!service) {
/* This is possible because an onion service client can end up here due to
* having an identity key onto a connection _to_ an onion service. We
* can't differentiate that from an actual onion service initiated by a
* service and thus the only way to know is to lookup the service. */
return;
}
hs_metrics_update_by_service(key, service, port, reason, n, obs, reset);
}
/** Return a list of all the onion service metrics stores. This is the
* function attached to the .get_metrics() member of the subsys_t. */
const smartlist_t *
hs_metrics_get_stores(void)
{
/* We can't have the caller to free the returned list so keep it static,
* simply update it. */
static smartlist_t *stores_list = NULL;
smartlist_free(stores_list);
stores_list = hs_service_get_metrics_stores();
return stores_list;
}
/** Initialize the metrics store in the given service. */
void
hs_metrics_service_init(hs_service_t *service)
{
tor_assert(service);
/* This function is called when we register a service and so it could either
* be a new service or a service that was just reloaded through a HUP signal
* for instance. Thus, it is possible that the service has already an
* initialized store. If so, just return. */
if (service->metrics.store) {
return;
}
service->metrics.store = metrics_store_new();
init_store(service);
}
/** Free the metrics store in the given service. */
void
hs_metrics_service_free(hs_service_t *service)
{
tor_assert(service);
metrics_store_free(service->metrics.store);
}