ui: Show log results in our dashboards

This commit is contained in:
Jeremy Wall 2024-03-04 20:52:36 -05:00
parent 17e6ae81a0
commit f69ea6d6fa
3 changed files with 113 additions and 21 deletions

View File

@ -22,7 +22,7 @@ use tracing::{debug, error};
use super::{LogLine, QueryResult, QueryType, TimeSpan}; use super::{LogLine, QueryResult, QueryType, TimeSpan};
// TODO(jwall): Should I allow non stream returns? // TODO(jwall): Should I allow non stream returns?
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug, PartialEq)]
pub enum ResultType { pub enum ResultType {
/// Returned by query endpoints /// Returned by query endpoints
#[serde(rename = "vector")] #[serde(rename = "vector")]
@ -85,8 +85,10 @@ pub fn loki_to_sample(data: LokiData) -> QueryResult {
} }
QueryResult::StreamInstant(values) QueryResult::StreamInstant(values)
} }
// Stream types are nanoseconds. // Matrix types are seconds
ResultType::Matrix | ResultType::Streams => { ResultType::Matrix | ResultType::Streams => {
let mut values = Vec::with_capacity(data.result.len()); 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 { for result in data.result {
if let Some(value) = result.values { if let Some(value) = result.values {
values.push(( values.push((
@ -94,7 +96,7 @@ pub fn loki_to_sample(data: LokiData) -> QueryResult {
value value
.into_iter() .into_iter()
.map(|(timestamp, line)| LogLine { .map(|(timestamp, line)| LogLine {
timestamp: timestamp.parse::<f64>().expect("Invalid f64 type"), timestamp: multiple * timestamp.parse::<f64>().expect("Invalid f64 type"),
line, line,
}) })
.collect(), .collect(),

View File

@ -26,7 +26,7 @@ use serde::{Deserialize, Serialize};
use tracing::debug; use tracing::debug;
use crate::dashboard::{ 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; use crate::query::QueryResult;
@ -120,6 +120,18 @@ pub fn mk_api_routes(config: Arc<Vec<Dashboard>>) -> Router<Config> {
) )
} }
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 { pub fn graph_component(dash_idx: usize, graph_idx: usize, graph: &Graph) -> Markup {
let graph_id = format!("graph-{}-{}", dash_idx, graph_idx); let graph_id = format!("graph-{}-{}", dash_idx, graph_idx);
let graph_data_uri = format!("/api/dash/{}/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<Arc<Vec<Dashboard>>>, dash_idx: usize) -> maud::P
let dash = config let dash = config
.get(dash_idx) .get(dash_idx)
.expect(&format!("No such dashboard {}", dash_idx)); .expect(&format!("No such dashboard {}", dash_idx));
let graph_iter = dash let graph_components = if let Some(graphs) = dash
.graphs .graphs
.as_ref() .as_ref() {
.expect("No graphs in this dashboard") let graph_iter = graphs.iter()
.iter()
.enumerate() .enumerate()
.collect::<Vec<(usize, &Graph)>>(); .collect::<Vec<(usize, &Graph)>>();
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::<Vec<(usize, &LogStream)>>();
Some(html! {
@for (idx, log) in &log_iter {
(log_component(dash_idx, *idx, *log))
}
})
} else {
None
};
html!( html!(
h1 { (dash.title) } h1 { (dash.title) }
span-selector class="row-flex" {} span-selector class="row-flex" {}
@for (idx, graph) in &graph_iter { @if graph_components.is_some() { (graph_components.unwrap()) }
(graph_component(dash_idx, *idx, *graph)) @if log_components.is_some() { (log_components.unwrap()) }
}
) )
} }

View File

@ -15,10 +15,10 @@
/** /**
* @typedef PlotList * @typedef PlotList
* @type {object} * @type {object}
* @property {?Array} Series * @property {Array=} Series
* @property {?Array} Scalar * @property {Array=} Scalar
* @property {?Array} StreamInstant * @property {Array<{timestamp: string, line: string}>=} StreamInstant - Timestamps are in seconds
* @property {?Array} Stream * @property {Array<{timestamp: string, line: string}>=} Stream - Timestamps are in nanoseconds
*/ */
/** /**
@ -29,16 +29,43 @@
* @property {Array<PlotList>} plots * @property {Array<PlotList>} 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} * @type {object}
* @property {string=} name * @property {string=} name
* @property type {string} * @property type {string}
* @property {string=} mode * @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} x
* @property {Array} y * @property {Array} y
* @peroperty {string=} xaxis * @property {string=} xaxis
* @peroperty {string=} yaxis * @property {string=} yaxis
*/
/**
* @typedef PlotTrace
* @type {(TableTrace|GraphTrace)}
*/ */
/** /**
@ -401,7 +428,7 @@ export class GraphPlot extends HTMLElement {
var yaxis = meta.yaxis || "y"; var yaxis = meta.yaxis || "y";
// https://plotly.com/javascript/reference/layout/yaxis/ // https://plotly.com/javascript/reference/layout/yaxis/
const series = triple[2]; const series = triple[2];
const trace = { const trace = /** @type GraphTrace */({
type: "scatter", type: "scatter",
mode: "lines+text", mode: "lines+text",
x: [], x: [],
@ -410,7 +437,7 @@ export class GraphPlot extends HTMLElement {
xaxis: "x", xaxis: "x",
yaxis: yaxis, yaxis: yaxis,
//yhoverformat: yaxis.tickformat, //yhoverformat: yaxis.tickformat,
}; });
if (meta.fill) { if (meta.fill) {
trace.fill = meta.fill; trace.fill = meta.fill;
} }
@ -434,7 +461,7 @@ export class GraphPlot extends HTMLElement {
} }
const meta = triple[1]; const meta = triple[1];
const series = triple[2]; const series = triple[2];
const trace = /** @type PlotTrace */({ const trace = /** @type GraphTrace */({
type: "bar", type: "bar",
x: [], x: [],
y: [], y: [],
@ -446,7 +473,42 @@ export class GraphPlot extends HTMLElement {
trace.x.push(trace.name); trace.x.push(trace.name);
traces.push(trace); 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 // https://plotly.com/javascript/plotlyjs-function-reference/#plotlyreact
// @ts-ignore // @ts-ignore