diff --git a/Cargo.lock b/Cargo.lock index 709aaca3f2..f9fcce624f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1440,7 +1440,7 @@ dependencies = [ [[package]] name = "clickward" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/clickward?rev=a1b342c2558e835d09e6e39a40d3de798a29c2f#a1b342c2558e835d09e6e39a40d3de798a29c2f5" +source = "git+https://github.com/oxidecomputer/clickward?rev=242fd812aaeafec99ba01b5505ffbb2bd2370917#242fd812aaeafec99ba01b5505ffbb2bd2370917" dependencies = [ "anyhow", "camino", diff --git a/Cargo.toml b/Cargo.toml index 8f4ba04ad1..4832a99e99 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -336,7 +336,7 @@ clickhouse-admin-server-client = { path = "clients/clickhouse-admin-server-clien clickhouse-admin-single-client = { path = "clients/clickhouse-admin-single-client" } clickhouse-admin-types = { path = "clickhouse-admin/types" } clickhouse-admin-test-utils = { path = "clickhouse-admin/test-utils" } -clickward = { git = "https://github.com/oxidecomputer/clickward", rev = "a1b342c2558e835d09e6e39a40d3de798a29c2f" } +clickward = { git = "https://github.com/oxidecomputer/clickward", rev = "242fd812aaeafec99ba01b5505ffbb2bd2370917" } cockroach-admin-api = { path = "cockroach-admin/api" } cockroach-admin-client = { path = "clients/cockroach-admin-client" } cockroach-admin-types = { path = "cockroach-admin/types" } diff --git a/clickhouse-admin/api/src/lib.rs b/clickhouse-admin/api/src/lib.rs index 398fb30f06..d9e742d764 100644 --- a/clickhouse-admin/api/src/lib.rs +++ b/clickhouse-admin/api/src/lib.rs @@ -4,12 +4,13 @@ use clickhouse_admin_types::{ ClickhouseKeeperClusterMembership, DistributedDdlQueue, KeeperConf, - KeeperConfig, KeeperConfigurableSettings, Lgif, RaftConfig, ReplicaConfig, - ServerConfigurableSettings, + KeeperConfig, KeeperConfigurableSettings, Lgif, MetricInfoPath, RaftConfig, + ReplicaConfig, ServerConfigurableSettings, SystemTimeSeries, + TimeSeriesSettingsQuery, }; use dropshot::{ HttpError, HttpResponseCreated, HttpResponseOk, - HttpResponseUpdatedNoContent, RequestContext, TypedBody, + HttpResponseUpdatedNoContent, Path, Query, RequestContext, TypedBody, }; /// API interface for our clickhouse-admin-keeper server @@ -116,6 +117,21 @@ pub trait ClickhouseAdminServerApi { async fn distributed_ddl_queue( rqctx: RequestContext, ) -> Result>, HttpError>; + + /// Retrieve time series from the system database. + /// + /// The value of each data point is the average of all stored data points + /// within the interval. + /// These are internal ClickHouse metrics. + #[endpoint { + method = GET, + path = "/timeseries/{table}/{metric}/avg" + }] + async fn system_timeseries_avg( + rqctx: RequestContext, + path_params: Path, + query_params: Query, + ) -> Result>, HttpError>; } /// API interface for our clickhouse-admin-single server @@ -136,4 +152,19 @@ pub trait ClickhouseAdminSingleApi { async fn init_db( rqctx: RequestContext, ) -> Result; + + /// Retrieve time series from the system database. + /// + /// The value of each data point is the average of all stored data points + /// within the interval. + /// These are internal ClickHouse metrics. + #[endpoint { + method = GET, + path = "/timeseries/{table}/{metric}/avg" + }] + async fn system_timeseries_avg( + rqctx: RequestContext, + path_params: Path, + query_params: Query, + ) -> Result>, HttpError>; } diff --git a/clickhouse-admin/src/clickhouse_cli.rs b/clickhouse-admin/src/clickhouse_cli.rs index fbdbe46e5f..3d8a1b076e 100644 --- a/clickhouse-admin/src/clickhouse_cli.rs +++ b/clickhouse-admin/src/clickhouse_cli.rs @@ -6,19 +6,23 @@ use anyhow::Result; use camino::Utf8PathBuf; use clickhouse_admin_types::{ ClickhouseKeeperClusterMembership, DistributedDdlQueue, KeeperConf, - KeeperId, Lgif, RaftConfig, OXIMETER_CLUSTER, + KeeperId, Lgif, RaftConfig, SystemTimeSeries, SystemTimeSeriesSettings, + OXIMETER_CLUSTER, }; use dropshot::HttpError; use illumos_utils::{output_to_exec_error, ExecutionError}; -use slog::Logger; +use slog::{debug, Logger}; use slog_error_chain::{InlineErrorChain, SlogInlineError}; use std::collections::BTreeSet; use std::ffi::OsStr; use std::fmt::Display; use std::io; use std::net::SocketAddrV6; +use std::time::Duration; use tokio::process::Command; +const DEFAULT_COMMAND_TIMEOUT: Duration = Duration::from_secs(30); + #[derive(Debug, thiserror::Error, SlogInlineError)] pub enum ClickhouseCliError { #[error("failed to run `clickhouse {subcommand}`")] @@ -38,6 +42,8 @@ pub enum ClickhouseCliError { #[source] err: anyhow::Error, }, + #[error("clickhouse server unavailable: {0}")] + ServerUnavailable(String), } impl From for HttpError { @@ -45,6 +51,7 @@ impl From for HttpError { match err { ClickhouseCliError::Run { .. } | ClickhouseCliError::Parse { .. } + | ClickhouseCliError::ServerUnavailable { .. } | ClickhouseCliError::ExecutionError(_) => { let message = InlineErrorChain::new(&err).to_string(); HttpError { @@ -161,6 +168,25 @@ impl ClickhouseCli { .await } + pub async fn system_timeseries_avg( + &self, + settings: SystemTimeSeriesSettings, + ) -> Result, ClickhouseCliError> { + let log = self.log.clone().unwrap(); + let query = settings.query_avg(); + + debug!(&log, "Querying system database"; "query" => &query); + + self.client_non_interactive( + ClickhouseClientType::Server, + &query, + "Retrieve time series from the system database", + SystemTimeSeries::parse, + log, + ) + .await + } + async fn client_non_interactive( &self, client: ClickhouseClientType, @@ -182,19 +208,33 @@ impl ClickhouseCli { .arg("--query") .arg(query); - let output = command.output().await.map_err(|err| { - let err_args: Vec<&OsStr> = command.as_std().get_args().collect(); - let err_args_parsed: Vec = err_args - .iter() - .map(|&os_str| os_str.to_string_lossy().into_owned()) - .collect(); - let err_args_str = err_args_parsed.join(" "); - ClickhouseCliError::Run { - description: subcommand_description, - subcommand: err_args_str, - err, + let now = tokio::time::Instant::now(); + let result = + tokio::time::timeout(DEFAULT_COMMAND_TIMEOUT, command.output()) + .await; + + let elapsed = now.elapsed(); + let output = match result { + Ok(result) => result.map_err(|err| { + let err_args: Vec<&OsStr> = + command.as_std().get_args().collect(); + let err_args_parsed: Vec = err_args + .iter() + .map(|&os_str| os_str.to_string_lossy().into_owned()) + .collect(); + let err_args_str = err_args_parsed.join(" "); + ClickhouseCliError::Run { + description: subcommand_description, + subcommand: err_args_str, + err, + } + })?, + Err(e) => { + return Err(ClickhouseCliError::ServerUnavailable(format!( + "command timed out after {elapsed:?}: {e}" + ))) } - })?; + }; if !output.status.success() { return Err(output_to_exec_error(command.as_std(), &output).into()); diff --git a/clickhouse-admin/src/http_entrypoints.rs b/clickhouse-admin/src/http_entrypoints.rs index 9eb987da72..9379e8102f 100644 --- a/clickhouse-admin/src/http_entrypoints.rs +++ b/clickhouse-admin/src/http_entrypoints.rs @@ -6,12 +6,13 @@ use crate::context::{ServerContext, SingleServerContext}; use clickhouse_admin_api::*; use clickhouse_admin_types::{ ClickhouseKeeperClusterMembership, DistributedDdlQueue, KeeperConf, - KeeperConfig, KeeperConfigurableSettings, Lgif, RaftConfig, ReplicaConfig, - ServerConfigurableSettings, + KeeperConfig, KeeperConfigurableSettings, Lgif, MetricInfoPath, RaftConfig, + ReplicaConfig, ServerConfigurableSettings, SystemTimeSeries, + SystemTimeSeriesSettings, TimeSeriesSettingsQuery, }; use dropshot::{ ApiDescription, HttpError, HttpResponseCreated, HttpResponseOk, - HttpResponseUpdatedNoContent, RequestContext, TypedBody, + HttpResponseUpdatedNoContent, Path, Query, RequestContext, TypedBody, }; use illumos_utils::svcadm::Svcadm; use omicron_common::address::CLICKHOUSE_TCP_PORT; @@ -64,6 +65,21 @@ impl ClickhouseAdminServerApi for ClickhouseAdminServerImpl { let output = ctx.clickhouse_cli().distributed_ddl_queue().await?; Ok(HttpResponseOk(output)) } + + async fn system_timeseries_avg( + rqctx: RequestContext, + path_params: Path, + query_params: Query, + ) -> Result>, HttpError> { + let ctx = rqctx.context(); + let retrieval_settings = query_params.into_inner(); + let metric_info = path_params.into_inner(); + let settings = + SystemTimeSeriesSettings { retrieval_settings, metric_info }; + let output = + ctx.clickhouse_cli().system_timeseries_avg(settings).await?; + Ok(HttpResponseOk(output)) + } } enum ClickhouseAdminKeeperImpl {} @@ -155,4 +171,19 @@ impl ClickhouseAdminSingleApi for ClickhouseAdminSingleImpl { Ok(HttpResponseUpdatedNoContent()) } + + async fn system_timeseries_avg( + rqctx: RequestContext, + path_params: Path, + query_params: Query, + ) -> Result>, HttpError> { + let ctx = rqctx.context(); + let retrieval_settings = query_params.into_inner(); + let metric_info = path_params.into_inner(); + let settings = + SystemTimeSeriesSettings { retrieval_settings, metric_info }; + let output = + ctx.clickhouse_cli().system_timeseries_avg(settings).await?; + Ok(HttpResponseOk(output)) + } } diff --git a/clickhouse-admin/types/src/lib.rs b/clickhouse-admin/types/src/lib.rs index e563f6da75..3b8696438c 100644 --- a/clickhouse-admin/types/src/lib.rs +++ b/clickhouse-admin/types/src/lib.rs @@ -17,6 +17,7 @@ use schemars::{ use serde::{Deserialize, Serialize}; use slog::{info, Logger}; use std::collections::{BTreeMap, BTreeSet}; +use std::fmt; use std::fs::create_dir; use std::io::{ErrorKind, Write}; use std::net::Ipv6Addr; @@ -1033,6 +1034,204 @@ impl DistributedDdlQueue { } } +#[inline] +fn default_interval() -> u64 { + 60 +} + +#[inline] +fn default_time_range() -> u64 { + 86400 +} + +#[inline] +fn default_timestamp_format() -> TimestampFormat { + TimestampFormat::Utc +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +/// Available metrics tables in the `system` database +pub enum SystemTable { + AsynchronousMetricLog, + MetricLog, +} + +impl fmt::Display for SystemTable { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let table = match self { + SystemTable::MetricLog => "metric_log", + SystemTable::AsynchronousMetricLog => "asynchronous_metric_log", + }; + write!(f, "{}", table) + } +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +/// Which format should the timestamp be in. +pub enum TimestampFormat { + Utc, + UnixEpoch, +} + +impl fmt::Display for TimestampFormat { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let table = match self { + TimestampFormat::Utc => "iso", + TimestampFormat::UnixEpoch => "unix_timestamp", + }; + write!(f, "{}", table) + } +} + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct MetricInfoPath { + /// Table to query in the `system` database + pub table: SystemTable, + /// Name of the metric to retrieve. + pub metric: String, +} + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct TimeSeriesSettingsQuery { + /// The interval to collect monitoring metrics in seconds. + /// Default is 60 seconds. + #[serde(default = "default_interval")] + pub interval: u64, + /// Range of time to collect monitoring metrics in seconds. + /// Default is 86400 seconds (24 hrs). + #[serde(default = "default_time_range")] + pub time_range: u64, + /// Format in which each timeseries timestamp will be in. + /// Default is UTC + #[serde(default = "default_timestamp_format")] + pub timestamp_format: TimestampFormat, +} + +/// Settings to specify which time series to retrieve. +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct SystemTimeSeriesSettings { + /// Time series retrieval settings (time range and interval) + pub retrieval_settings: TimeSeriesSettingsQuery, + /// Database table and name of the metric to retrieve + pub metric_info: MetricInfoPath, +} + +impl SystemTimeSeriesSettings { + fn interval(&self) -> u64 { + self.retrieval_settings.interval + } + + fn time_range(&self) -> u64 { + self.retrieval_settings.time_range + } + + fn timestamp_format(&self) -> TimestampFormat { + self.retrieval_settings.timestamp_format + } + + fn metric_name(&self) -> &str { + &self.metric_info.metric + } + + fn table(&self) -> SystemTable { + self.metric_info.table + } + + // TODO: Use more aggregate functions than just avg? + pub fn query_avg(&self) -> String { + let interval = self.interval(); + let time_range = self.time_range(); + let metric_name = self.metric_name(); + let table = self.table(); + let ts_fmt = self.timestamp_format(); + + let avg_value = match table { + SystemTable::MetricLog => metric_name, + SystemTable::AsynchronousMetricLog => "value", + }; + + let mut query = format!( + "SELECT toStartOfInterval(event_time, INTERVAL {interval} SECOND) AS time, avg({avg_value}) AS value + FROM system.{table} + WHERE event_date >= toDate(now() - {time_range}) AND event_time >= now() - {time_range} + " + ); + + match table { + SystemTable::MetricLog => (), + SystemTable::AsynchronousMetricLog => query.push_str( + format!( + "AND metric = '{metric_name}' + " + ) + .as_str(), + ), + }; + + query.push_str( + format!( + "GROUP BY time + ORDER BY time WITH FILL STEP {interval} + FORMAT JSONEachRow + SETTINGS date_time_output_format = '{ts_fmt}'" + ) + .as_str(), + ); + query + } +} + +// Our OpenAPI generator does not allow for enums to be of different +// primitive types. Because Utc is a "string" in json, Unix cannot be an int. +// This is why we set it as a `String`. +#[derive(Debug, Display, Serialize, Deserialize, JsonSchema, PartialEq)] +#[serde(untagged)] +pub enum Timestamp { + Utc(DateTime), + Unix(String), +} + +/// Retrieved time series from the internal `system` database. +#[derive(Debug, Serialize, Deserialize, JsonSchema, PartialEq)] +#[serde(rename_all = "snake_case")] +pub struct SystemTimeSeries { + pub time: Timestamp, + pub value: f64, + // TODO: Would be really nice to have an enum with possible units (s, ms, bytes) + // Not sure if I can even add this, the system tables don't mention units at all. +} + +impl SystemTimeSeries { + pub fn parse(log: &Logger, data: &[u8]) -> Result> { + let s = String::from_utf8_lossy(data); + info!( + log, + "Retrieved data from `system` database"; + "output" => ?s + ); + + let mut m = vec![]; + + for line in s.lines() { + // serde_json deserialises f64 types with loss of precision at times. + // For example, in our tests some of the values to serialize have a + // fractional value of `.33333`, but once parsed, they become `.33331`. + // + // We do not require this level of precision, so we'll leave as is. + // Just noting that we are aware of this slight inaccuracy. + let item: SystemTimeSeries = serde_json::from_str(line)?; + m.push(item); + } + + Ok(m) + } +} + #[cfg(test)] mod tests { use camino::Utf8PathBuf; @@ -1048,6 +1247,7 @@ mod tests { ClickhouseHost, DistributedDdlQueue, KeeperConf, KeeperId, KeeperServerInfo, KeeperServerType, KeeperSettings, Lgif, LogLevel, RaftConfig, RaftServerSettings, ServerId, ServerSettings, + SystemTimeSeries, }; fn log() -> slog::Logger { @@ -1887,4 +2087,96 @@ snapshot_storage_disk=LocalSnapshotDisk "missing field `entry_version` at line 1 column 454", ); } + + #[test] + fn test_unix_epoch_system_timeseries_parse_success() { + let log = log(); + let data = "{\"time\":\"1732494720\",\"value\":110220450825.75238} +{\"time\":\"1732494840\",\"value\":110339992917.33333} +{\"time\":\"1732494960\",\"value\":110421854037.33333}\n" + .as_bytes(); + let timeseries = SystemTimeSeries::parse(&log, data).unwrap(); + + let expected = vec![ + SystemTimeSeries { + time: crate::Timestamp::Unix("1732494720".to_string()), + value: 110220450825.75238, + }, + SystemTimeSeries { + time: crate::Timestamp::Unix("1732494840".to_string()), + value: 110339992917.33331, + }, + SystemTimeSeries { + time: crate::Timestamp::Unix("1732494960".to_string()), + value: 110421854037.33331, + }, + ]; + + assert_eq!(timeseries, expected); + } + + #[test] + fn test_utc_system_timeseries_parse_success() { + let log = log(); + let data = + "{\"time\":\"2024-11-25T00:34:00Z\",\"value\":110220450825.75238} +{\"time\":\"2024-11-25T00:35:00Z\",\"value\":110339992917.33333} +{\"time\":\"2024-11-25T00:36:00Z\",\"value\":110421854037.33333}\n" + .as_bytes(); + let timeseries = SystemTimeSeries::parse(&log, data).unwrap(); + + let expected = vec![ + SystemTimeSeries { + time: crate::Timestamp::Utc( + "2024-11-25T00:34:00Z".parse::>().unwrap(), + ), + value: 110220450825.75238, + }, + SystemTimeSeries { + time: crate::Timestamp::Utc( + "2024-11-25T00:35:00Z".parse::>().unwrap(), + ), + value: 110339992917.33331, + }, + SystemTimeSeries { + time: crate::Timestamp::Utc( + "2024-11-25T00:36:00Z".parse::>().unwrap(), + ), + value: 110421854037.33331, + }, + ]; + + assert_eq!(timeseries, expected); + } + + #[test] + fn test_misshapen_system_timeseries_parse_fail() { + let log = log(); + let data = "{\"bob\":\"1732494720\",\"value\":110220450825.75238}\n" + .as_bytes(); + let result = SystemTimeSeries::parse(&log, data); + + let error = result.unwrap_err(); + let root_cause = error.root_cause(); + + assert_eq!( + format!("{}", root_cause), + "missing field `time` at line 1 column 47", + ); + } + + #[test] + fn test_time_format_system_timeseries_parse_fail() { + let log = log(); + let data = "{\"time\":2024,\"value\":110220450825.75238}\n".as_bytes(); + let result = SystemTimeSeries::parse(&log, data); + + let error = result.unwrap_err(); + let root_cause = error.root_cause(); + + assert_eq!( + format!("{}", root_cause), + "data did not match any variant of untagged enum Timestamp at line 1 column 12", + ); + } } diff --git a/openapi/clickhouse-admin-server.json b/openapi/clickhouse-admin-server.json index a42ee25bbb..c82c7c0d8e 100644 --- a/openapi/clickhouse-admin-server.json +++ b/openapi/clickhouse-admin-server.json @@ -73,6 +73,83 @@ } } } + }, + "/timeseries/{table}/{metric}/avg": { + "get": { + "summary": "Retrieve time series from the system database.", + "description": "The value of each data point is the average of all stored data points within the interval. These are internal ClickHouse metrics.", + "operationId": "system_timeseries_avg", + "parameters": [ + { + "in": "path", + "name": "metric", + "description": "Name of the metric to retrieve.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "table", + "description": "Table to query in the `system` database", + "required": true, + "schema": { + "$ref": "#/components/schemas/SystemTable" + } + }, + { + "in": "query", + "name": "interval", + "description": "The interval to collect monitoring metrics in seconds. Default is 60 seconds.", + "schema": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + { + "in": "query", + "name": "time_range", + "description": "Range of time to collect monitoring metrics in seconds. Default is 86400 seconds (24 hrs).", + "schema": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + { + "in": "query", + "name": "timestamp_format", + "description": "Format in which each timeseries timestamp will be in. Default is UTC", + "schema": { + "$ref": "#/components/schemas/TimestampFormat" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_SystemTimeSeries", + "type": "array", + "items": { + "$ref": "#/components/schemas/SystemTimeSeries" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } } }, "components": { @@ -533,6 +610,50 @@ "listen_addr", "remote_servers" ] + }, + "SystemTimeSeries": { + "description": "Retrieved time series from the internal `system` database.", + "type": "object", + "properties": { + "time": { + "$ref": "#/components/schemas/Timestamp" + }, + "value": { + "type": "number", + "format": "double" + } + }, + "required": [ + "time", + "value" + ] + }, + "Timestamp": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "string" + } + ] + }, + "SystemTable": { + "description": "Available metrics tables in the `system` database", + "type": "string", + "enum": [ + "asynchronous_metric_log", + "metric_log" + ] + }, + "TimestampFormat": { + "description": "Which format should the timestamp be in.", + "type": "string", + "enum": [ + "utc", + "unix_epoch" + ] } }, "responses": { diff --git a/openapi/clickhouse-admin-single.json b/openapi/clickhouse-admin-single.json index 74763957ca..b00bf56314 100644 --- a/openapi/clickhouse-admin-single.json +++ b/openapi/clickhouse-admin-single.json @@ -26,6 +26,83 @@ } } } + }, + "/timeseries/{table}/{metric}/avg": { + "get": { + "summary": "Retrieve time series from the system database.", + "description": "The value of each data point is the average of all stored data points within the interval. These are internal ClickHouse metrics.", + "operationId": "system_timeseries_avg", + "parameters": [ + { + "in": "path", + "name": "metric", + "description": "Name of the metric to retrieve.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "table", + "description": "Table to query in the `system` database", + "required": true, + "schema": { + "$ref": "#/components/schemas/SystemTable" + } + }, + { + "in": "query", + "name": "interval", + "description": "The interval to collect monitoring metrics in seconds. Default is 60 seconds.", + "schema": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + { + "in": "query", + "name": "time_range", + "description": "Range of time to collect monitoring metrics in seconds. Default is 86400 seconds (24 hrs).", + "schema": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + { + "in": "query", + "name": "timestamp_format", + "description": "Format in which each timeseries timestamp will be in. Default is UTC", + "schema": { + "$ref": "#/components/schemas/TimestampFormat" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_SystemTimeSeries", + "type": "array", + "items": { + "$ref": "#/components/schemas/SystemTimeSeries" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } } }, "components": { @@ -48,6 +125,50 @@ "message", "request_id" ] + }, + "SystemTimeSeries": { + "description": "Retrieved time series from the internal `system` database.", + "type": "object", + "properties": { + "time": { + "$ref": "#/components/schemas/Timestamp" + }, + "value": { + "type": "number", + "format": "double" + } + }, + "required": [ + "time", + "value" + ] + }, + "Timestamp": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "string" + } + ] + }, + "SystemTable": { + "description": "Available metrics tables in the `system` database", + "type": "string", + "enum": [ + "asynchronous_metric_log", + "metric_log" + ] + }, + "TimestampFormat": { + "description": "Which format should the timestamp be in.", + "type": "string", + "enum": [ + "utc", + "unix_epoch" + ] } }, "responses": {