From f94722e1e349a2354a29e482a4b376ae03503e09 Mon Sep 17 00:00:00 2001 From: Yury Molodov Date: Fri, 11 Jul 2025 14:22:50 +0200 Subject: [PATCH] Tempo: Add Victoria Logs support for "traces to logs" (#105985) * feat(trace-to-logs): add VictoriaLogs datasource support Signed-off-by: Yury Molodov * tempo: fix lint errors in createSpanLink.tsx Signed-off-by: Yury Molodov --------- Signed-off-by: Yury Molodov --- .../src/TraceToLogs/TraceToLogsSettings.tsx | 1 + .../explore/TraceView/createSpanLink.test.ts | 85 +++++++++++++++++++ .../explore/TraceView/createSpanLink.tsx | 46 ++++++++++ 3 files changed, 132 insertions(+) diff --git a/packages/grafana-o11y-ds-frontend/src/TraceToLogs/TraceToLogsSettings.tsx b/packages/grafana-o11y-ds-frontend/src/TraceToLogs/TraceToLogsSettings.tsx index b5bca15f317..50870a2d5bc 100644 --- a/packages/grafana-o11y-ds-frontend/src/TraceToLogs/TraceToLogsSettings.tsx +++ b/packages/grafana-o11y-ds-frontend/src/TraceToLogs/TraceToLogsSettings.tsx @@ -80,6 +80,7 @@ export function TraceToLogsSettings({ options, onOptionsChange }: Props) { 'grafana-opensearch-datasource', // external 'grafana-falconlogscale-datasource', // external 'googlecloud-logging-datasource', // external + 'victoriametrics-logs-datasource', // external ]; const traceToLogs = useMemo( diff --git a/public/app/features/explore/TraceView/createSpanLink.test.ts b/public/app/features/explore/TraceView/createSpanLink.test.ts index 03a4651e8a5..7574434861c 100644 --- a/public/app/features/explore/TraceView/createSpanLink.test.ts +++ b/public/app/features/explore/TraceView/createSpanLink.test.ts @@ -1515,6 +1515,91 @@ describe('createSpanLinkFactory', () => { expect(decodeURIComponent(links![0].href)).toContain('spanName=\\"operation\\"'); }); }); + + describe('should return victorialogs link', () => { + const victoriaLogsUID = 'victoriaLogsUID'; + + beforeAll(() => { + setDataSourceSrv({ + getInstanceSettings() { + return { + uid: victoriaLogsUID, + name: 'VictoriaLogs', + type: 'victoriametrics-logs-datasource', + } as unknown as DataSourceInstanceSettings; + }, + } as unknown as DataSourceSrv); + + setLinkSrv(new LinkSrv()); + setTemplateSrv(new TemplateSrv()); + }); + + it('with default keys when tags not configured', () => { + const createLink = setupSpanLinkFactory({}, victoriaLogsUID); + const links = createLink!(createTraceSpan()); + + const linkDef = links?.[0]; + expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Logs); + expect(linkDef!.href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"1602637200000","to":"1602637201000"},"datasource":"victoriaLogsUID","queries":[{"expr":"cluster:=\\"cluster1\\" AND hostname:=\\"hostname1\\" AND service_namespace:=\\"namespace1\\"","refId":""}]}' + )}` + ); + }); + + it('formats query correctly if filterByTraceID and filterBySpanID is true', () => { + const createLink = setupSpanLinkFactory( + { + datasourceUid: victoriaLogsUID, + filterByTraceID: true, + filterBySpanID: true, + }, + victoriaLogsUID + ); + + const links = createLink!(createTraceSpan()); + + const linkDef = links?.[0]; + expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Logs); + expect(linkDef!.href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"1602637200000","to":"1602637201000"},"datasource":"victoriaLogsUID","queries":[{"expr":"span_id:=\\"6605c7b08e715d6c\\" AND trace_id:=\\"7946b05c2e2e4e5a\\" AND cluster:=\\"cluster1\\" AND hostname:=\\"hostname1\\" AND service_namespace:=\\"namespace1\\"","refId":""}]}' + )}` + ); + }); + + it('should format multiple tags correctly', () => { + const createLink = setupSpanLinkFactory( + { + tags: [{ key: 'ip' }, { key: 'hostname' }], + }, + victoriaLogsUID + ); + + const links = createLink!( + createTraceSpan({ + process: { + serviceName: 'service', + tags: [ + { key: 'hostname', value: 'hostname1' }, + { key: 'ip', value: '192.168.0.1' }, + ], + }, + }) + ); + + const linkDef = links?.[0]; + expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Logs); + expect(linkDef!.href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"1602637200000","to":"1602637201000"},"datasource":"victoriaLogsUID","queries":[{"expr":"hostname:=\\"hostname1\\" AND ip:=\\"192.168.0.1\\"","refId":""}]}' + )}` + ); + }); + }); }); describe('dataFrame links', () => { diff --git a/public/app/features/explore/TraceView/createSpanLink.tsx b/public/app/features/explore/TraceView/createSpanLink.tsx index ee689c39920..68b2dba6485 100644 --- a/public/app/features/explore/TraceView/createSpanLink.tsx +++ b/public/app/features/explore/TraceView/createSpanLink.tsx @@ -196,6 +196,13 @@ function legacyCreateSpanLinkFactory( case 'googlecloud-logging-datasource': tags = getFormattedTags(span, tagsToUse, { joinBy: ' AND ' }); query = getQueryForGoogleCloudLogging(span, traceToLogsOptions, tags, customQuery); + break; + case 'victoriametrics-logs-datasource': + // Build tag selector using strict equality (":=") required by LogsQL + // See https://docs.victoriametrics.com/victorialogs/logsql/#exact-filter + tags = getFormattedTags(span, tagsToUse, { labelValueSign: ':=', joinBy: ' AND ' }); + query = getQueryForVictoriaLogs(span, traceToLogsOptions, tags, customQuery); + break; } // query can be false in case the simple UI tag mapping is used but none of them are present in the span. @@ -568,6 +575,45 @@ function getQueryForFalconLogScale(span: TraceSpan, options: TraceToLogsOptionsV }; } +/** + * Builds a LogsQL expression for victoria‑metrics‑logs‑datasource. + * Uses := for exact‑match filters and joins parts with AND. + * See https://docs.victoriametrics.com/victorialogs/logsql/#exact-filter + */ +function getQueryForVictoriaLogs(span: TraceSpan, options: TraceToLogsOptionsV2, tags: string, customQuery?: string) { + const { filterByTraceID, filterBySpanID } = options; + + // Custom user query has priority + if (customQuery) { + return { + expr: customQuery, + refId: '', + }; + } + + const parts: string[] = []; + + if (filterBySpanID && span.spanID) { + parts.push('span_id:="${__span.spanId}"'); + } + if (filterByTraceID && span.traceID) { + parts.push('trace_id:="${__span.traceId}"'); + } + if (tags) { + parts.push('${__tags}'); + } + + // Nothing to match against – do not create the link + if (!parts.length) { + return undefined; + } + + return { + expr: parts.join(' AND '), + refId: '', + }; +} + /** * Creates a string representing all the tags already formatted for use in the query. The tags are filtered so that * only intersection of tags that exist in a span and tags that you want are serialized into the string.