grafana: tell the logs datasource that severity_text is the level

Grafana's log-level buttons filter on a field called `level`; no row in
the store has one. The store's name for it is `severity_text`, chosen by
VictoriaLogs' OTLP ingester rather than by us — v1.52.0's
`app/vlinsert/opentelemetry/pb.go` writes it unconditionally and the
ingest parameters have no `_level_field` to rename it with. So the
mapping is made on the reader: the VictoriaLogs datasource, which was
provisioned with no `jsonData` at all, now carries a `logLevelRules`
entry per severity the journald parser can emit.

`logLevelRules` is the datasource plugin's only level-related jsonData
key — there is no field-name setting and no OpenTelemetry preset to
switch on. It is read off `instanceSettings.jsonData` in the plugin's
`datasource.ts` and typed in its `configuration/LogLevelRules/types.ts`,
both recovered from the sourcemap shipped in the pinned artifact
(`grafanaPlugins.victoriametrics-logs-datasource` 0.26.3). Each enabled
rule appends an `OR severity_text:="INFO"`-shaped term to the query a
level button emits, next to the `level:…` term that matches nothing.

A wrong rule here fails silently: Grafana provisions unknown jsonData
without complaint and the buttons go on returning zero rows. The three
ways to get one wrong — a non-literal `enabled`, a non-canonical `level`
spelling, a value that is not the stored text — are recorded at the
binding, and a module-eval arm pins them along with the real failure
mode, a severity added to `nix/journald-severity.nix` and not here.

`Unspecified` is left unmapped on purpose: it is the store's own
rendering of an absent severity and the thing the logs dashboard's "no
severity" panel counts.

Refs #4560
This commit is contained in:
atlas 2026-09-20 16:13:43 +02:00 committed by mara
commit 8e9ca3ee6c
3 changed files with 119 additions and 0 deletions

View file

@ -40,6 +40,65 @@ let
# free to drift into a panel that renders empty rather than erroring.
logsDatasourceUid = "swarm-victorialogs";
# Which field carries the log level, told to the READER because the writer
# cannot be told. Grafana's log-level buttons filter on a field called
# `level`, and no row in this store has one: VictoriaLogs' OTLP ingester
# names the stored field `severity_text` unconditionally (v1.52.0,
# `app/vlinsert/opentelemetry/pb.go`) and its ingest parameters have no
# `_level_field` sibling to rename it with. So the mapping is made here.
#
# `logLevelRules` is the datasource plugin's own jsonData key and the only
# level-related one it has — there is no "the level lives in field X"
# string to set. A rule names its own field, so saying it takes one rule
# per level.
#
# 🔑 Each enabled rule appends an `OR <field>:<op>"<value>"` term to the
# query a level button emits, beside the `level:…` term that matches
# nothing here. Clicking "info" goes from
#
# level:contains_common_case("info","information","informational","notice")
#
# to that OR `severity_text:="INFO"`, which our rows do match.
#
# ⚠️ Three ways to get a rule wrong, all of them SILENT — Grafana accepts
# any jsonData it does not recognise, so a bad rule provisions cleanly and
# the buttons go on returning zero:
#
# - `enabled` must be literally `true`, not merely not-false. The query
# builder keeps rules on `rule.enabled` being truthy while the row
# colouring path keeps them on `!== false`, so an omitted flag colours
# rows correctly and leaves the buttons broken — working in the half
# nobody is looking at.
# - `level` must be a CANONICAL Grafana `LogLevel` value. The builder
# groups rules by it and only ever looks up `critical`, `error`,
# `warning`, `info`, `debug`, `trace`; `warn` and `fatal` are enum
# aliases that resolve to other spellings and match no group.
# - `value` is compared with `===`, so it is the severity text exactly as
# STORED: the uppercase OpenTelemetry short names `overwrite_text`
# writes in ../journald-severity.nix, not that table's lowercase keys.
#
# `Unspecified` is deliberately unmapped. It is VictoriaLogs' own rendering
# of an absent severity, and it is what the logs dashboard's "no severity"
# panel counts — giving it a level would dress the missing data up as a
# colour and retire the instrument that measures it.
logLevelRules =
let
fromSeverityText = value: level: {
field = "severity_text";
operator = "equals";
enabled = true;
inherit value level;
};
in
[
(fromSeverityText "FATAL" "critical")
(fromSeverityText "ERROR" "error")
(fromSeverityText "WARN" "warning")
(fromSeverityText "INFO" "info")
(fromSeverityText "DEBUG" "debug")
(fromSeverityText "TRACE" "trace")
];
# The shipped dashboards carry `@datasourceUid@` / `@logsDatasourceUid@` where
# a real deployment needs the uids above. They are substituted here rather
# than committed with the literals so the single binding stays single.
@ -944,6 +1003,11 @@ in
# two defaults is a coin toss over which one an untyped panel
# gets.
isDefault = false;
# See `logLevelRules` above for why the level lives here and
# not in the pipeline.
jsonData = {
inherit logLevelRules;
};
}
];
};