From f69ea6d6faaf45ec183521902dd573a046b6cb87 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Mon, 4 Mar 2024 20:52:36 -0500 Subject: [PATCH] ui: Show log results in our dashboards --- src/query/loki.rs | 6 ++-- src/routes.rs | 44 ++++++++++++++++++++----- static/lib.js | 84 ++++++++++++++++++++++++++++++++++++++++------- 3 files changed, 113 insertions(+), 21 deletions(-) diff --git a/src/query/loki.rs b/src/query/loki.rs index 8152593..43e4c87 100644 --- a/src/query/loki.rs +++ b/src/query/loki.rs @@ -22,7 +22,7 @@ use tracing::{debug, error}; use super::{LogLine, QueryResult, QueryType, TimeSpan}; // TODO(jwall): Should I allow non stream returns? -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, PartialEq)] pub enum ResultType { /// Returned by query endpoints #[serde(rename = "vector")] @@ -85,8 +85,10 @@ pub fn loki_to_sample(data: LokiData) -> QueryResult { } QueryResult::StreamInstant(values) } + // Stream types are nanoseconds. // Matrix types are seconds ResultType::Matrix | ResultType::Streams => { let mut values = Vec::with_capacity(data.result.len()); + let multiple = (if data.result_type == ResultType::Matrix { 1000000 } else { 1 }) as f64; for result in data.result { if let Some(value) = result.values { values.push(( @@ -94,7 +96,7 @@ pub fn loki_to_sample(data: LokiData) -> QueryResult { value .into_iter() .map(|(timestamp, line)| LogLine { - timestamp: timestamp.parse::().expect("Invalid f64 type"), + timestamp: multiple * timestamp.parse::().expect("Invalid f64 type"), line, }) .collect(), diff --git a/src/routes.rs b/src/routes.rs index 097b6f9..f5106dd 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -26,7 +26,7 @@ use serde::{Deserialize, Serialize}; use tracing::debug; use crate::dashboard::{ - loki_query_data, prom_query_data, AxisDefinition, Dashboard, Graph, GraphSpan, Orientation, + loki_query_data, prom_query_data, AxisDefinition, Dashboard, Graph, GraphSpan, Orientation, LogStream, }; use crate::query::QueryResult; @@ -120,6 +120,18 @@ pub fn mk_api_routes(config: Arc>) -> Router { ) } +pub fn log_component(dash_idx: usize, log_idx: usize, log: &LogStream) -> Markup { + let log_id = format!("log-{}-{}", dash_idx, log_idx); + let log_data_uri = format!("/api/dash/{}/log/{}", dash_idx, log_idx); + let log_embed_uri = format!("/embed/dash/{}/log/{}", dash_idx, log_idx); + html! { + div { + h2 { (log.title) " - " a href=(log_embed_uri) { "embed url" } } + graph-plot uri=(log_data_uri) id=(log_id) { } + } + } +} + pub fn graph_component(dash_idx: usize, graph_idx: usize, graph: &Graph) -> Markup { let graph_id = format!("graph-{}-{}", dash_idx, graph_idx); let graph_data_uri = format!("/api/dash/{}/graph/{}", dash_idx, graph_idx); @@ -160,19 +172,35 @@ fn dash_elements(config: State>>, dash_idx: usize) -> maud::P let dash = config .get(dash_idx) .expect(&format!("No such dashboard {}", dash_idx)); - let graph_iter = dash + let graph_components = if let Some(graphs) = dash .graphs - .as_ref() - .expect("No graphs in this dashboard") - .iter() + .as_ref() { + let graph_iter = graphs.iter() .enumerate() .collect::>(); + Some(html! { + @for (idx, graph) in &graph_iter { + (graph_component(dash_idx, *idx, *graph)) + } + }) + } else { + None + }; + let log_components = if let Some(logs) = dash.logs.as_ref() { + let log_iter = logs.iter().enumerate().collect::>(); + Some(html! { + @for (idx, log) in &log_iter { + (log_component(dash_idx, *idx, *log)) + } + }) + } else { + None + }; html!( h1 { (dash.title) } span-selector class="row-flex" {} - @for (idx, graph) in &graph_iter { - (graph_component(dash_idx, *idx, *graph)) - } + @if graph_components.is_some() { (graph_components.unwrap()) } + @if log_components.is_some() { (log_components.unwrap()) } ) } diff --git a/static/lib.js b/static/lib.js index ec2d447..80dd4d1 100644 --- a/static/lib.js +++ b/static/lib.js @@ -15,10 +15,10 @@ /** * @typedef PlotList * @type {object} - * @property {?Array} Series - * @property {?Array} Scalar - * @property {?Array} StreamInstant - * @property {?Array} Stream + * @property {Array=} Series + * @property {Array=} Scalar + * @property {Array<{timestamp: string, line: string}>=} StreamInstant - Timestamps are in seconds + * @property {Array<{timestamp: string, line: string}>=} Stream - Timestamps are in nanoseconds */ /** @@ -29,16 +29,43 @@ * @property {Array} plots */ +/** + * @typedef HeaderOrCell + * @type {object} + * @property {array} values + * @property {string=} fill + * @property {{width: number, color: string}=} line + * @property {{family: string, size: number, color: string }=} font + */ + /** - * @typedef PlotTrace + * @typedef TableTrace * @type {object} * @property {string=} name * @property type {string} * @property {string=} mode + * @property {HeaderOrCell} headers + * @property {HeaderOrCell} cells - An Array of columns for the table. + * @property {string=} xaxis + * @property {string=} yaxis +*/ + +/** + * @typedef GraphTrace + * @type {object} + * @property {string=} name + * @property {string=} fill + * @property type {string} + * @property {string=} mode * @property {Array} x * @property {Array} y - * @peroperty {string=} xaxis - * @peroperty {string=} yaxis + * @property {string=} xaxis + * @property {string=} yaxis +*/ + +/** + * @typedef PlotTrace + * @type {(TableTrace|GraphTrace)} */ /** @@ -401,7 +428,7 @@ export class GraphPlot extends HTMLElement { var yaxis = meta.yaxis || "y"; // https://plotly.com/javascript/reference/layout/yaxis/ const series = triple[2]; - const trace = { + const trace = /** @type GraphTrace */({ type: "scatter", mode: "lines+text", x: [], @@ -410,7 +437,7 @@ export class GraphPlot extends HTMLElement { xaxis: "x", yaxis: yaxis, //yhoverformat: yaxis.tickformat, - }; + }); if (meta.fill) { trace.fill = meta.fill; } @@ -434,7 +461,7 @@ export class GraphPlot extends HTMLElement { } const meta = triple[1]; const series = triple[2]; - const trace = /** @type PlotTrace */({ + const trace = /** @type GraphTrace */({ type: "bar", x: [], y: [], @@ -446,7 +473,42 @@ export class GraphPlot extends HTMLElement { trace.x.push(trace.name); traces.push(trace); } - } // TODO(zaphar): subplot.Stream // log lines!!! + } else if (subplot.Stream) { + // TODO(zaphar): subplot.Stream // log lines!!! + const trace = /** @type TableTrace */({ + type: "table", + headers: { + align: "left", + values: ["Timestamp", "Log"] + }, + cells: { + align: "left", + values: [] + }, + }); + const dateColumn = []; + const logColumn = []; + + loopStream: for (const pair of subplot.Stream) { + const labels = pair[0]; + for (var label in labels) { + var show = this.#filteredLabelSets[label]; + if (show && !show.includes(labels[label])) { + continue loopStream; + } + } + const lines = pair[1]; + // TODO(jwall): Headers + for (const line of lines) { + // For streams the timestamps are in nanoseconds + dateColumn.push(new Date(line.timestamp / 1000000)); + logColumn.push(line.line); + } + } + trace.cells.values.push(dateColumn); + trace.cells.values.push(logColumn); + traces.push(trace); + } } // https://plotly.com/javascript/plotlyjs-function-reference/#plotlyreact // @ts-ignore