Compare commits

...

4 Commits

4 changed files with 90 additions and 7 deletions

View File

@ -96,6 +96,10 @@ async fn main() -> anyhow::Result<()> {
"/embed/dash/:dash_idx/graph/:graph_idx", "/embed/dash/:dash_idx/graph/:graph_idx",
get(routes::graph_embed).with_state(State(config.clone())), get(routes::graph_embed).with_state(State(config.clone())),
) )
.route(
"/embed/dash/:dash_idx/log/:graph_idx",
get(routes::log_embed).with_state(State(config.clone())),
)
.route("/dash/:dash_idx", get(routes::dashboard_direct)) .route("/dash/:dash_idx", get(routes::dashboard_direct))
.route("/", get(routes::index).with_state(State(config.clone()))) .route("/", get(routes::index).with_state(State(config.clone())))
.layer(TraceLayer::new_for_http()) .layer(TraceLayer::new_for_http())

View File

@ -163,6 +163,21 @@ pub async fn graph_ui(
graph_component(dash_idx, graph_idx, graph) graph_component(dash_idx, graph_idx, graph)
} }
pub async fn log_ui(
State(config): State<Config>,
Path((dash_idx, log_idx)): Path<(usize, usize)>,
) -> Markup {
let log = config
.get(dash_idx)
.expect(&format!("No such dashboard {}", dash_idx))
.logs
.as_ref()
.expect("No graphs in this dashboard")
.get(log_idx)
.expect("No such graph");
log_component(dash_idx, log_idx, log)
}
pub async fn dash_ui(State(config): State<Config>, Path(dash_idx): Path<usize>) -> Markup { pub async fn dash_ui(State(config): State<Config>, Path(dash_idx): Path<usize>) -> Markup {
// TODO(zaphar): Should do better http error reporting here. // TODO(zaphar): Should do better http error reporting here.
dash_elements(config, dash_idx) dash_elements(config, dash_idx)
@ -241,6 +256,23 @@ pub async fn graph_embed(
} }
} }
pub async fn log_embed(
State(config): State<Config>,
Path((dash_idx, log_idx)): Path<(usize, usize)>,
) -> Markup {
html! {
html {
head {
title { ("Heracles - Prometheus Unshackled") }
}
body {
(graph_lib_prelude())
(log_ui(State(config.clone()), Path((dash_idx, log_idx))).await)
}
}
}
}
async fn index_html(config: Config, dash_idx: Option<usize>) -> Markup { async fn index_html(config: Config, dash_idx: Option<usize>) -> Markup {
html! { html! {
html { html {

View File

@ -36,6 +36,7 @@
* @property {{color: string}=} fill * @property {{color: string}=} fill
* @property {{width: number, color: string}=} line * @property {{width: number, color: string}=} line
* @property {{family: string, size: number, color: string }=} font * @property {{family: string, size: number, color: string }=} font
* @property {Array<number>=} columnwidth
*/ */
/** /**
@ -68,6 +69,41 @@
* @type {(TableTrace|GraphTrace)} * @type {(TableTrace|GraphTrace)}
*/ */
/**
* Map ansi terminal codes to html color codes.
* @param {string} line
*/
function ansiToHtml(line) {
const ansiToHtmlMap = {
// Map ANSI color codes to HTML color names or hex values
// We don't necessarily handle all the colors but this is enough to start.
"30": "black",
"31": "red",
"32": "green",
"33": "yellow",
"34": "blue",
"35": "magenta",
"36": "cyan",
"37": "white",
"39": "initial"
};
// NOTE(zaphar): Yes this is gross and I should really do a better parser but I'm lazy.
// Replace ANSI codes with HTML span elements styled with the corresponding color
return line.replace(/\x1b\[([0-9;]*)m/g, (match, p1) => {
const parts = p1.split(';'); // ANSI codes can be compounded, e.g., "1;31" for bold red
let styles = '';
for (let part of parts) {
if (ansiToHtmlMap[part]) {
// If the code is a color, map it to a CSS color
styles += `color: ${ansiToHtmlMap[part]};`;
}
// TODO(zaphar): Add more conditions here to handle other styles like bold or underline?
}
return styles ? `<span style="${styles}">` : '</span>';
}) + '</span>';
}
/** /**
* Get's a css variable's value from the document. * Get's a css variable's value from the document.
* @param {string} variableName - Name of the variable to get `--var-name` * @param {string} variableName - Name of the variable to get `--var-name`
@ -395,7 +431,7 @@ export class GraphPlot extends HTMLElement {
var layout = { var layout = {
displayModeBar: false, displayModeBar: false,
responsive: true, responsive: true,
plot_bgcolor: getCssVariableValue('--paper-background-color').trim(), plot_bgcolor: getCssVariableValue('--plot-background-color').trim(),
paper_bgcolor: getCssVariableValue('--paper-background-color').trim(), paper_bgcolor: getCssVariableValue('--paper-background-color').trim(),
font: { font: {
color: getCssVariableValue('--text-color').trim() color: getCssVariableValue('--text-color').trim()
@ -480,41 +516,50 @@ export class GraphPlot extends HTMLElement {
traces.push(trace); traces.push(trace);
} }
} else if (subplot.Stream) { } else if (subplot.Stream) {
// TODO(zaphar): subplot.Stream // log lines!!! // TODO(jwall): It's possible that this should actually be a separate custom
// element.
const trace = /** @type TableTrace */({ const trace = /** @type TableTrace */({
type: "table", type: "table",
// TODO(zaphar): Column width? columnwidth: [15, 20, 70],
headers: { headers: {
align: "left", align: "left",
values: ["Timestamp", "Log"], values: ["Timestamp","Label", "Log"],
fill: { color: layout.xaxis.gridColor } fill: { color: layout.xaxis.gridColor }
}, },
cells: { cells: {
align: "left", align: "left",
values: [], values: [],
fill: { color: layout.paper_bgcolor } fill: { color: layout.plot_bgcolor }
}, },
}); });
const dateColumn = []; const dateColumn = [];
const metaColumn = [];
const logColumn = []; const logColumn = [];
loopStream: for (const pair of subplot.Stream) { loopStream: for (const pair of subplot.Stream) {
const labels = pair[0]; const labels = pair[0];
var labelList = [];
for (var label in labels) { for (var label in labels) {
var show = this.#filteredLabelSets[label]; var show = this.#filteredLabelSets[label];
if (show && !show.includes(labels[label])) { if (show && !show.includes(labels[label])) {
continue loopStream; continue loopStream;
} }
labelList.push(`${label}:${labels[label]}`);
} }
const labelsName = labelList.join("<br>");
const lines = pair[1]; const lines = pair[1];
// TODO(jwall): Headers // TODO(jwall): Headers
for (const line of lines) { for (const line of lines) {
// For streams the timestamps are in nanoseconds // For streams the timestamps are in nanoseconds
dateColumn.push(new Date(line.timestamp / 1000000)); // TODO(zaphar): We should improve the timstamp formatting a bit
logColumn.push(line.line); let timestamp = new Date(line.timestamp / 1000000);
dateColumn.push(timestamp.toISOString());
metaColumn.push(labelsName);
logColumn.push(ansiToHtml(line.line));
} }
} }
trace.cells.values.push(dateColumn); trace.cells.values.push(dateColumn);
trace.cells.values.push(metaColumn);
trace.cells.values.push(logColumn); trace.cells.values.push(logColumn);
traces.push(trace); traces.push(trace);
} }

View File

@ -3,6 +3,7 @@
--background-color: #FFFFFF; /* Light background */ --background-color: #FFFFFF; /* Light background */
--text-color: #333333; /* Dark text for contrast */ --text-color: #333333; /* Dark text for contrast */
--paper-background-color: #F0F0F0; --paper-background-color: #F0F0F0;
--plot-background-color: #F0F0F0;
--accent-color: #6200EE; /* For buttons and interactive elements */ --accent-color: #6200EE; /* For buttons and interactive elements */
/* Graph colors */ /* Graph colors */
@ -27,6 +28,7 @@
/* Solarized Dark Base Colors */ /* Solarized Dark Base Colors */
--background-color: #002b36; /* base03 */ --background-color: #002b36; /* base03 */
--paper-background-color: #003c4a; --paper-background-color: #003c4a;
--plot-background-color: rgb(24, 34, 21);
--text-color: #839496; /* base0 */ --text-color: #839496; /* base0 */
--accent-color: #268bd2; /* blue */ --accent-color: #268bd2; /* blue */