From e49b2eae59398482dc282db460cd625832b57465 Mon Sep 17 00:00:00 2001 From: Vincent Miszczak Date: Thu, 26 Feb 2026 14:54:07 +0100 Subject: [PATCH] feat: add opt-in dataplane format --- CHANGELOG.md | 2 + src/README.md | 15 +++ src/configuration/LogsSettings.tsx | 22 ++++- src/datasource.ts | 5 +- .../frameProcessors/streamFrameProcessor.ts | 35 +++++-- .../transformBackendResult.test.ts | 91 ++++++++++++++++++- src/transformers/transformBackendResult.ts | 3 +- src/types.ts | 1 + 8 files changed, 161 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5323067..67ea009d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## tip +* FEATURE: add opt-in Grafana dataplane log format support. When enabled in datasource settings, log frames use standard field names (`timestamp`, `body`) and `DataFrameType.LogLines` metadata, enabling automatic `${labelKey}` variables in correlations. + ## v0.26.3 * BUGFIX: fix time range provided in `field_names` and `field_values` requests. Instead of being rounded to a 24-hour time range, the selected time range is now provided. See [pr #581](https://github.com/VictoriaMetrics/victorialogs-datasource/pull/581). diff --git a/src/README.md b/src/README.md index 36e2c8e9..711eefe1 100644 --- a/src/README.md +++ b/src/README.md @@ -46,6 +46,8 @@ datasources: url: http://victorialogs:9428 isDefault: true jsonData: + # Enable Grafana dataplane log format for correlations support + #useDataplaneFormat: true # Multitenancy settings, see https://docs.victoriametrics.com/victorialogs/#multitenancy # to use the multitenancy, uncomment lines below: AccountID and ProjectID multitenancyHeaders: @@ -165,6 +167,19 @@ Where: * `operator` is the comparison operator to use, such as `equals`, `notEquals`, `regex`, `lessThan`, `greaterThan`. * `enabled` is a boolean flag to enable or disable the rule. Defaults to `true` if omitted. +### Dataplane format + +The **Use dataplane format** toggle in the datasource settings enables the [Grafana dataplane log format](https://grafana.com/developers/dataplane/). When enabled, log frames use standard field names (`timestamp`, `body` instead of `Time`, `Line`) and set `DataFrameType.LogLines` metadata. This makes all label keys automatically available as `${labelKey}` variables in [correlations](https://grafana.com/docs/grafana/latest/administration/correlations/), similar to how Loki works with its dataplane support. + +**Note:** Enabling this setting may break existing client-side transformations that reference the `Time` or `Line` field names. + +To enable via provisioning: + +```yaml +jsonData: + useDataplaneFormat: true +``` + ### Variables VictoriaLogs datasource supports [variables](https://grafana.com/docs/grafana/latest/variables/) in queries. diff --git a/src/configuration/LogsSettings.tsx b/src/configuration/LogsSettings.tsx index 4054cba7..31303297 100644 --- a/src/configuration/LogsSettings.tsx +++ b/src/configuration/LogsSettings.tsx @@ -1,7 +1,7 @@ import React, { SyntheticEvent, useMemo } from 'react'; import { SelectableValue } from '@grafana/data'; -import { InlineField, Input, Stack, Text } from '@grafana/ui'; +import { InlineField, InlineSwitch, Input, Stack, Text } from '@grafana/ui'; import { Options } from '../types'; @@ -53,6 +53,26 @@ export const LogsSettings = (props: PropsConfigEditor) => { /> +
+ + { + onOptionsChange({ + ...options, + jsonData: { + ...options.jsonData, + useDataplaneFormat: e.currentTarget.checked, + }, + }); + }} + /> + +
); diff --git a/src/datasource.ts b/src/datasource.ts index b28fceed..bd21c1e2 100644 --- a/src/datasource.ts +++ b/src/datasource.ts @@ -94,6 +94,7 @@ export class VictoriaLogsDatasource queryBuilderLimits?: QueryBuilderLimits; logLevelRules: LogLevelRule[]; multitenancyHeaders?: MultitenancyHeaders; + useDataplaneFormat: boolean; constructor( instanceSettings: DataSourceInstanceSettings, @@ -120,6 +121,7 @@ export class VictoriaLogsDatasource this.queryBuilderLimits = settingsData.queryBuilderLimits; this.logLevelRules = settingsData.logLevelRules || []; this.multitenancyHeaders = this.parseMultitenancyHeaders(settingsData.multitenancyHeaders); + this.useDataplaneFormat = settingsData.useDataplaneFormat ?? false; } query(request: DataQueryRequest): Observable { @@ -158,7 +160,8 @@ export class VictoriaLogsDatasource response, fixedRequest, this.derivedFields ?? [], - this.getActiveLevelRules() + this.getActiveLevelRules(), + this.useDataplaneFormat ) ) ); diff --git a/src/transformers/frameProcessors/streamFrameProcessor.ts b/src/transformers/frameProcessors/streamFrameProcessor.ts index 01374d4d..65f2a705 100644 --- a/src/transformers/frameProcessors/streamFrameProcessor.ts +++ b/src/transformers/frameProcessors/streamFrameProcessor.ts @@ -1,4 +1,4 @@ -import { DataFrame, QueryResultMeta } from '@grafana/data'; +import { DataFrame, DataFrameType, QueryResultMeta } from '@grafana/data'; import { LogLevelRule } from '../../configuration/LogLevelRules/types'; import { getHighlighterExpressionsFromQuery } from '../../queryUtils'; @@ -7,19 +7,20 @@ import { getDerivedFields } from '../fields/derivedField'; import { getStreamFields } from '../fields/labelField'; import { addLevelField } from '../fields/levelField'; import { getStreamIds } from '../fields/streamUtils'; -import { ANNOTATIONS_REF_ID } from '../types'; +import { ANNOTATIONS_REF_ID, FrameField } from '../types'; import { dataFrameHasError, setFrameMeta } from '../utils/frame/frameUtils'; export function processStreamsFrames( frames: DataFrame[], queryMap: Map, derivedFieldConfigs: DerivedFieldConfig[], - logLevelRules: LogLevelRule[] + logLevelRules: LogLevelRule[], + useDataplaneFormat = false, ): DataFrame[] { return frames.map((frame) => { const query = frame.refId !== undefined ? queryMap.get(frame.refId) : undefined; const isAnnotations = query?.refId === ANNOTATIONS_REF_ID; - return processStreamFrame(frame, query, derivedFieldConfigs, logLevelRules, isAnnotations); + return processStreamFrame(frame, query, derivedFieldConfigs, logLevelRules, isAnnotations, useDataplaneFormat); }); } @@ -28,7 +29,8 @@ function processStreamFrame( query: Query | undefined, derivedFieldConfigs: DerivedFieldConfig[], logLevelRules: LogLevelRule[], - transformLabels = false + transformLabels = false, + useDataplaneFormat = false, ): DataFrame { const custom: Record = { ...frame.meta?.custom, // keep the original meta.custom @@ -55,11 +57,26 @@ function processStreamFrame( const derivedFields = getDerivedFields(frameWithLevel, derivedFieldConfigs); const baseFields = getStreamFields(frameWithLevel.fields, transformLabels); + const fields = [...baseFields, ...derivedFields]; + + if (useDataplaneFormat) { + return { + ...frameWithLevel, + fields: fields.map((f) => { + if (f.name === 'Time') { return { ...f, name: 'timestamp' }; } + if (f.name === FrameField.Line) { return { ...f, name: 'body' }; } + return f; + }), + meta: { + ...frameWithLevel.meta, + type: DataFrameType.LogLines, + typeVersion: [0, 0], + }, + }; + } + return { ...frameWithLevel, - fields: [ - ...baseFields, - ...derivedFields - ] + fields, }; } diff --git a/src/transformers/transformBackendResult.test.ts b/src/transformers/transformBackendResult.test.ts index d5257bdc..5a0d50c2 100644 --- a/src/transformers/transformBackendResult.test.ts +++ b/src/transformers/transformBackendResult.test.ts @@ -1,4 +1,4 @@ -import { DataQueryRequest, DataQueryResponse, dateTime, LogLevel } from '@grafana/data'; +import { DataFrameType, DataQueryRequest, DataQueryResponse, dateTime, LogLevel } from '@grafana/data'; import { LogLevelRule, LogLevelRuleType } from '../configuration/LogLevelRules/types'; import { DerivedFieldConfig, Query, QueryType } from '../types'; @@ -187,6 +187,95 @@ describe('transformBackendResult', () => { ]); }); + it('should use dataplane format when useDataplaneFormat is true', () => { + const response = { + 'data': [ + { + 'refId': 'A', + 'meta': { + 'typeVersion': [0, 0] + }, + 'fields': [ + { + 'name': 'Time', + 'type': 'time', + 'typeInfo': { 'frame': 'time.Time' }, + 'config': {}, + 'values': [1760598702731], + 'entities': {}, + 'nanos': [713000] + }, + { + 'name': 'Line', + 'type': 'string', + 'typeInfo': { 'frame': 'string' }, + 'config': {}, + 'values': ['starting application'], + 'entities': {} + }, + { + 'name': 'labels', + 'type': 'other', + 'typeInfo': { 'frame': 'json.RawMessage' }, + 'config': {}, + 'values': [labels[0]], + 'entities': {} + } + ], + 'length': 1 + } + ], + 'state': 'Done' + } as DataQueryResponse; + const request = { + 'app': 'dashboard', + 'requestId': 'SQR100', + 'timezone': 'browser', + 'range': { + 'to': '2025-10-16T07:28:02.475Z', + 'from': '2025-10-16T01:28:02.475Z', + 'raw': { 'from': 'now-6h', 'to': 'now' } + }, + 'interval': '20s', + 'intervalMs': 20000, + 'targets': [ + { + 'datasource': { 'type': 'victoriametrics-logs-datasource', 'uid': 'bexw8wod6s4jke' }, + 'editorMode': 'code', + 'expr': '*', + 'queryType': 'instant', + 'refId': 'A', + 'maxLines': 1000 + } + ], + 'maxDataPoints': 913, + 'scopedVars': { + '__sceneObject': { 'text': '__sceneObject' }, + '__interval': { 'text': '20s', 'value': '20s' }, + '__interval_ms': { 'text': '20000', 'value': 20000 } + }, + 'startTime': 1760599682628, + 'rangeRaw': { 'from': 'now-6h', 'to': 'now' }, + 'dashboardUID': '886b7b9f-97a7-47ee-93b6-9ec7342f6d3e', + 'panelId': 1, + 'panelName': 'New panel', + 'panelPluginId': 'table', + 'dashboardTitle': 'double label info' + } as unknown as DataQueryRequest; + const result = transformBackendResult(response, request, [], [], true); + const frame = result.data[0]; + + // Field names should be renamed to dataplane format + expect(frame.fields[0].name).toBe('timestamp'); + expect(frame.fields[1].name).toBe('body'); + expect(frame.fields[2].name).toBe('labels'); + expect(frame.fields[3].name).toBe('detected_level'); + + // Meta should have dataplane type + expect(frame.meta?.type).toBe(DataFrameType.LogLines); + expect(frame.meta?.typeVersion).toStrictEqual([0, 0]); + }); + it('should parse level according to rules, apply the origin level labels then rule labels', () => { const extendedLabels = labels.map((l, index) => { if (index > 2) { diff --git a/src/transformers/transformBackendResult.ts b/src/transformers/transformBackendResult.ts index 70c6e82b..824d8557 100644 --- a/src/transformers/transformBackendResult.ts +++ b/src/transformers/transformBackendResult.ts @@ -17,6 +17,7 @@ export function transformBackendResult( request: DataQueryRequest, derivedFieldConfigs: DerivedFieldConfig[], logLevelRules: LogLevelRule[], + useDataplaneFormat = false, ): DataQueryResponse { const { data, errors, ...rest } = response; const queries = request.targets; @@ -44,7 +45,7 @@ export function transformBackendResult( data: [ ...processMetricRangeFrames(metricRangeFrames, request.targets, request.range.from.valueOf(), request.range.to.valueOf()), ...processMetricInstantFrames(metricInstantFrames), - ...processStreamsFrames(streamsFrames, queryMap, derivedFieldConfigs, logLevelRules), + ...processStreamsFrames(streamsFrames, queryMap, derivedFieldConfigs, logLevelRules, useDataplaneFormat), ...processHistogramFrames(histogramFrames, request.panelPluginId), ], }; diff --git a/src/types.ts b/src/types.ts index 051386a9..ffe7b147 100644 --- a/src/types.ts +++ b/src/types.ts @@ -18,6 +18,7 @@ export interface Options extends DataSourceJsonData { logLevelRules?: LogLevelRule[]; multitenancyHeaders?: Partial>; vmuiUrl?: string; + useDataplaneFormat?: boolean; } export const QUERY_DIRECTION = {