From 686ac97ca424c02cbaf7b4e60498c3e309395f65 Mon Sep 17 00:00:00 2001 From: Adam Kinniburgh Date: Thu, 17 Sep 2026 10:34:36 +0100 Subject: [PATCH 1/4] fix: dependency import misses aggregates --- .../jaeger-bridge-service-to-service.json | 30 ++++++++ .../Jaeger/v1/dataStreams/dependencies.json | 29 +++----- .../v1/dataStreams/serviceDependencies.json | 68 +++++++++++++++++++ .../Jaeger/v1/indexDefinitions/default.json | 2 +- plugins/Jaeger/v1/ui.json | 20 ++++++ 5 files changed, 129 insertions(+), 20 deletions(-) create mode 100644 plugins/Jaeger/v1/correlationRules/jaeger-bridge-service-to-service.json create mode 100644 plugins/Jaeger/v1/dataStreams/serviceDependencies.json diff --git a/plugins/Jaeger/v1/correlationRules/jaeger-bridge-service-to-service.json b/plugins/Jaeger/v1/correlationRules/jaeger-bridge-service-to-service.json new file mode 100644 index 00000000..e448bb76 --- /dev/null +++ b/plugins/Jaeger/v1/correlationRules/jaeger-bridge-service-to-service.json @@ -0,0 +1,30 @@ +{ + "bridge": { + "types": "Dependency" + }, + "displayName": "Service depends on Service", + "labels": { + "forward": "depends on", + "reverse": "dependency of" + }, + "source": { + "types": "Service" + }, + "sourceConditions": [ + { + "operator": "equals", + "sourceProperty": "rawId", + "targetProperty": "parent" + } + ], + "target": { + "types": "Service" + }, + "targetConditions": [ + { + "operator": "equals", + "sourceProperty": "child", + "targetProperty": "rawId" + } + ] +} diff --git a/plugins/Jaeger/v1/dataStreams/dependencies.json b/plugins/Jaeger/v1/dataStreams/dependencies.json index ab9cb116..d517e3c6 100644 --- a/plugins/Jaeger/v1/dataStreams/dependencies.json +++ b/plugins/Jaeger/v1/dataStreams/dependencies.json @@ -7,27 +7,23 @@ ], "baseDataSourceName": "httpRequestUnscoped", "matches": "none", - "timeframes": [ - "last1hour", - "last12hours", - "last24hours", - "last7days" - ], + "timeframes": false, "config": { "httpMethod": "get", "endpointPath": "/api/dependencies", "pathToData": "data", "getArgs": [ - { - "key": "endTs", - "value": "{{timeframe.unixEnd * 1000}}" - }, { "key": "lookback", - "value": "{{(timeframe.unixEnd - timeframe.unixStart) * 1000}}" + "value": "{{dataSource.dependencyStorage === 'spans' ? 3600000 : 172800000}}" } ], - "headers": [] + "headers": [], + "errorHandling": { + "type": "path", + "realm": "payload", + "path": "errors.0.msg" + } }, "metadata": [ { @@ -56,13 +52,8 @@ { "thousandsSeparator": true } - ] - }, - { - "name": "source", - "displayName": "Source", - "shape": "string", - "visible": false + ], + "role": "value" } ] } diff --git a/plugins/Jaeger/v1/dataStreams/serviceDependencies.json b/plugins/Jaeger/v1/dataStreams/serviceDependencies.json new file mode 100644 index 00000000..6fc54740 --- /dev/null +++ b/plugins/Jaeger/v1/dataStreams/serviceDependencies.json @@ -0,0 +1,68 @@ +{ + "name": "serviceDependencies", + "displayName": "Service Dependencies", + "description": "Call dependencies into and out of a service, one row per parent-child pair", + "tags": [ + "Services" + ], + "baseDataSourceName": "httpRequestScopedSingle", + "matches": { + "sourceType": { + "type": "equals", + "value": "Service" + } + }, + "timeframes": false, + "config": { + "httpMethod": "get", + "endpointPath": "/api/dependencies", + "pathToData": "data", + "getArgs": [ + { + "key": "service", + "value": "{{object.rawId}}" + }, + { + "key": "lookback", + "value": "{{dataSource.dependencyStorage === 'spans' ? 3600000 : 172800000}}" + } + ], + "headers": [], + "errorHandling": { + "type": "path", + "realm": "payload", + "path": "errors.0.msg" + } + }, + "metadata": [ + { + "name": "key", + "displayName": "Dependency", + "computed": true, + "valueExpression": "{{ $['parent'] + ' -> ' + $['child'] }}", + "shape": "string", + "role": "label" + }, + { + "name": "parent", + "displayName": "Parent Service", + "shape": "string" + }, + { + "name": "child", + "displayName": "Child Service", + "shape": "string" + }, + { + "name": "callCount", + "displayName": "Call Count", + "shape": [ + "number", + { + "thousandsSeparator": true + } + ], + "role": "value" + } + ] +} diff --git a/plugins/Jaeger/v1/indexDefinitions/default.json b/plugins/Jaeger/v1/indexDefinitions/default.json index aabecbdc..c4130f11 100644 --- a/plugins/Jaeger/v1/indexDefinitions/default.json +++ b/plugins/Jaeger/v1/indexDefinitions/default.json @@ -19,7 +19,7 @@ "dataStream": { "name": "dependencies" }, - "timeframe": "last1hour", + "timeframe": "none", "objectMapping": { "id": "key", "name": "key", diff --git a/plugins/Jaeger/v1/ui.json b/plugins/Jaeger/v1/ui.json index 41e003df..0d0905ee 100644 --- a/plugins/Jaeger/v1/ui.json +++ b/plugins/Jaeger/v1/ui.json @@ -13,6 +13,26 @@ "type": "checkbox", "name": "ignoreCertificateErrors", "label": "Ignore certificate errors", + "defaultValue": false, "help": "Enable when connecting to a Jaeger instance that uses a self-signed certificate." + }, + { + "type": "radio", + "name": "dependencyStorage", + "label": "Dependency storage", + "defaultValue": "aggregated", + "help": "Controls the window the Dependencies data stream asks Jaeger for. The `spark-dependencies` job stamps every batch it writes with midnight UTC rather than the time it ran, so a query matching the selected timeframe finds nothing for 23 hours of the day — pre-aggregated storage needs the window widened to whole UTC days instead.", + "options": [ + { + "value": "aggregated", + "label": "Pre-aggregated daily", + "description": "Elasticsearch, OpenSearch or Cassandra, where the dependency graph is written by the spark-dependencies job." + }, + { + "value": "spans", + "label": "Derived from spans", + "description": "Badger or in-memory storage, where Jaeger derives the dependency graph from the spans in the requested window." + } + ] } ] From d2dfc790f73f006c478211ca24df2b99275bf889 Mon Sep 17 00:00:00 2001 From: Adam Kinniburgh Date: Thu, 17 Sep 2026 10:34:39 +0100 Subject: [PATCH 2/4] fix: trace search ignores result limit --- plugins/Jaeger/v1/dataStreams/traces.json | 31 +++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/plugins/Jaeger/v1/dataStreams/traces.json b/plugins/Jaeger/v1/dataStreams/traces.json index efe7b348..fdf3fd54 100644 --- a/plugins/Jaeger/v1/dataStreams/traces.json +++ b/plugins/Jaeger/v1/dataStreams/traces.json @@ -30,12 +30,25 @@ "value": "{{timeframe.end}}" }, { - "key": "query.search_depth", + "key": "query.num_traces", "value": "{{limit}}" + }, + { + "key": "query.operation_name", + "value": "{{operation || null}}" + }, + { + "key": "query.duration_min", + "value": "{{minDurationMs ? minDurationMs + 'ms' : null}}" } ], "headers": [], - "postRequestScript": "traces.js" + "postRequestScript": "traces.js", + "errorHandling": { + "type": "path", + "realm": "payload", + "path": "error.message" + } }, "ui": [ { @@ -44,6 +57,20 @@ "type": "number", "defaultValue": 20, "min": 1 + }, + { + "name": "operation", + "label": "Operation", + "type": "text", + "placeholder": "Leave empty for all operations", + "help": "Selects the traces that contain this operation, then returns every span in them — so the results still include spans named differently." + }, + { + "name": "minDurationMs", + "label": "Minimum duration (ms)", + "type": "number", + "min": 0, + "help": "Returns only traces whose root span ran at least this long." } ], "metadata": [ From 0f62e646b568c7bcfcefb5cc8226cc9133f8c149 Mon Sep 17 00:00:00 2001 From: Adam Kinniburgh Date: Thu, 17 Sep 2026 10:34:41 +0100 Subject: [PATCH 3/4] feat: spm coverage --- plugins/Jaeger/v1/cspell.json | 5 +- .../v1/dataStreams/scripts/serviceCallRate.js | 28 ++ .../dataStreams/scripts/serviceErrorRate.js | 28 ++ .../v1/dataStreams/scripts/serviceLatency.js | 28 ++ .../v1/dataStreams/serviceCallRate.json | 93 +++++++ .../v1/dataStreams/serviceErrorRate.json | 93 +++++++ .../Jaeger/v1/dataStreams/serviceLatency.json | 104 ++++++++ .../Jaeger/v1/defaultContent/manifest.json | 6 +- .../v1/defaultContent/redMetrics.dash.json | 244 ++++++++++++++++++ .../{service.dash.json => tracing.dash.json} | 56 +++- plugins/Jaeger/v1/docs/README.md | 64 +++-- plugins/Jaeger/v1/metadata.json | 7 +- 12 files changed, 732 insertions(+), 24 deletions(-) create mode 100644 plugins/Jaeger/v1/dataStreams/scripts/serviceCallRate.js create mode 100644 plugins/Jaeger/v1/dataStreams/scripts/serviceErrorRate.js create mode 100644 plugins/Jaeger/v1/dataStreams/scripts/serviceLatency.js create mode 100644 plugins/Jaeger/v1/dataStreams/serviceCallRate.json create mode 100644 plugins/Jaeger/v1/dataStreams/serviceErrorRate.json create mode 100644 plugins/Jaeger/v1/dataStreams/serviceLatency.json create mode 100644 plugins/Jaeger/v1/defaultContent/redMetrics.dash.json rename plugins/Jaeger/v1/defaultContent/{service.dash.json => tracing.dash.json} (67%) diff --git a/plugins/Jaeger/v1/cspell.json b/plugins/Jaeger/v1/cspell.json index 7f2ae8e3..f4c59981 100644 --- a/plugins/Jaeger/v1/cspell.json +++ b/plugins/Jaeger/v1/cspell.json @@ -1,6 +1,9 @@ { "words": [ "jaeger", - "opentelemetry" + "opentelemetry", + "jsonpb", + "gogo", + "quantile" ] } diff --git a/plugins/Jaeger/v1/dataStreams/scripts/serviceCallRate.js b/plugins/Jaeger/v1/dataStreams/scripts/serviceCallRate.js new file mode 100644 index 00000000..463e5111 --- /dev/null +++ b/plugins/Jaeger/v1/dataStreams/scripts/serviceCallRate.js @@ -0,0 +1,28 @@ +// dataStreams/scripts/serviceCallRate.js +// +// Jaeger serialises the metrics APIs with gogo's jsonpb marshaler, which gives +// a single MetricFamily in camelCase with default values omitted: a genuinely +// zero point arrives as an empty `gaugeValue: {}`, not as `doubleValue: 0`. +// Prometheus gaps come back as the string "NaN" (JSON has no NaN literal), so +// coerce and drop anything non-finite rather than charting it. +// +// Each row is one point of one series — calls per second. + +const series = (data && data.metrics) || []; + +result = _.filter( + _.flatMap(series, (s) => { + const labels = _.fromPairs((s.labels || []).map((l) => [l.name, l.value])); + return (s.metricPoints || []).map((point) => { + const gauge = point.gaugeValue || {}; + const raw = gauge.doubleValue !== undefined ? gauge.doubleValue : gauge.intValue; + return { + timestamp: point.timestamp, + service: labels.service_name, + operation: labels.operation, + value: Number(raw === undefined ? 0 : raw), + }; + }); + }), + (row) => row.timestamp && Number.isFinite(row.value), +); diff --git a/plugins/Jaeger/v1/dataStreams/scripts/serviceErrorRate.js b/plugins/Jaeger/v1/dataStreams/scripts/serviceErrorRate.js new file mode 100644 index 00000000..5402c818 --- /dev/null +++ b/plugins/Jaeger/v1/dataStreams/scripts/serviceErrorRate.js @@ -0,0 +1,28 @@ +// dataStreams/scripts/serviceErrorRate.js +// +// Jaeger serialises the metrics APIs with gogo's jsonpb marshaler, which gives +// a single MetricFamily in camelCase with default values omitted: a genuinely +// zero point arrives as an empty `gaugeValue: {}`, not as `doubleValue: 0`. +// Prometheus gaps come back as the string "NaN" (JSON has no NaN literal), so +// coerce and drop anything non-finite rather than charting it. +// +// Each row is one point of one series — error ratio. + +const series = (data && data.metrics) || []; + +result = _.filter( + _.flatMap(series, (s) => { + const labels = _.fromPairs((s.labels || []).map((l) => [l.name, l.value])); + return (s.metricPoints || []).map((point) => { + const gauge = point.gaugeValue || {}; + const raw = gauge.doubleValue !== undefined ? gauge.doubleValue : gauge.intValue; + return { + timestamp: point.timestamp, + service: labels.service_name, + operation: labels.operation, + value: Number(raw === undefined ? 0 : raw), + }; + }); + }), + (row) => row.timestamp && Number.isFinite(row.value), +); diff --git a/plugins/Jaeger/v1/dataStreams/scripts/serviceLatency.js b/plugins/Jaeger/v1/dataStreams/scripts/serviceLatency.js new file mode 100644 index 00000000..cbab4749 --- /dev/null +++ b/plugins/Jaeger/v1/dataStreams/scripts/serviceLatency.js @@ -0,0 +1,28 @@ +// dataStreams/scripts/serviceLatency.js +// +// Jaeger serialises the metrics APIs with gogo's jsonpb marshaler, which gives +// a single MetricFamily in camelCase with default values omitted: a genuinely +// zero point arrives as an empty `gaugeValue: {}`, not as `doubleValue: 0`. +// Prometheus gaps come back as the string "NaN" (JSON has no NaN literal), so +// coerce and drop anything non-finite rather than charting it. +// +// Each row is one point of one series — latency in milliseconds. + +const series = (data && data.metrics) || []; + +result = _.filter( + _.flatMap(series, (s) => { + const labels = _.fromPairs((s.labels || []).map((l) => [l.name, l.value])); + return (s.metricPoints || []).map((point) => { + const gauge = point.gaugeValue || {}; + const raw = gauge.doubleValue !== undefined ? gauge.doubleValue : gauge.intValue; + return { + timestamp: point.timestamp, + service: labels.service_name, + operation: labels.operation, + value: Number(raw === undefined ? 0 : raw), + }; + }); + }), + (row) => row.timestamp && Number.isFinite(row.value), +); diff --git a/plugins/Jaeger/v1/dataStreams/serviceCallRate.json b/plugins/Jaeger/v1/dataStreams/serviceCallRate.json new file mode 100644 index 00000000..d1916eb8 --- /dev/null +++ b/plugins/Jaeger/v1/dataStreams/serviceCallRate.json @@ -0,0 +1,93 @@ +{ + "name": "serviceCallRate", + "displayName": "Service Call Rate", + "description": "Requests per second served by a service (requires Jaeger's metrics backend)", + "tags": [ + "Performance" + ], + "baseDataSourceName": "httpRequestScopedSingle", + "matches": { + "sourceType": { + "type": "equals", + "value": "Service" + } + }, + "timeframes": [ + "last1hour", + "last12hours", + "last24hours", + "last7days", + "last30days" + ], + "config": { + "httpMethod": "get", + "endpointPath": "/api/metrics/calls", + "getArgs": [ + { + "key": "service", + "value": "{{object.rawId}}" + }, + { + "key": "endTs", + "value": "{{timeframe.unixEnd * 1000}}" + }, + { + "key": "lookback", + "value": "{{(timeframe.unixEnd - timeframe.unixStart) * 1000}}" + }, + { + "key": "step", + "value": "{{(timeframe.unixEnd - timeframe.unixStart) * 5}}" + }, + { + "key": "groupByOperation", + "value": "{{groupByOperation || null}}" + } + ], + "headers": [], + "postRequestScript": "serviceCallRate.js", + "errorHandling": { + "type": "path", + "realm": "payload", + "path": "errors.0.msg" + } + }, + "ui": [ + { + "name": "groupByOperation", + "label": "Split by operation", + "type": "checkbox", + "help": "Returns one series per operation instead of one for the service as a whole." + } + ], + "metadata": [ + { + "name": "timestamp", + "displayName": "Time", + "shape": "date", + "role": "timestamp" + }, + { + "name": "service", + "displayName": "Service", + "shape": "string", + "role": "label" + }, + { + "name": "operation", + "displayName": "Operation", + "shape": "string" + }, + { + "name": "value", + "displayName": "Calls/sec", + "shape": [ + "number", + { + "decimalPlaces": 2 + } + ], + "role": "value" + } + ] +} diff --git a/plugins/Jaeger/v1/dataStreams/serviceErrorRate.json b/plugins/Jaeger/v1/dataStreams/serviceErrorRate.json new file mode 100644 index 00000000..44291948 --- /dev/null +++ b/plugins/Jaeger/v1/dataStreams/serviceErrorRate.json @@ -0,0 +1,93 @@ +{ + "name": "serviceErrorRate", + "displayName": "Service Error Rate", + "description": "Proportion of a service's requests that failed (requires Jaeger's metrics backend)", + "tags": [ + "Performance" + ], + "baseDataSourceName": "httpRequestScopedSingle", + "matches": { + "sourceType": { + "type": "equals", + "value": "Service" + } + }, + "timeframes": [ + "last1hour", + "last12hours", + "last24hours", + "last7days", + "last30days" + ], + "config": { + "httpMethod": "get", + "endpointPath": "/api/metrics/errors", + "getArgs": [ + { + "key": "service", + "value": "{{object.rawId}}" + }, + { + "key": "endTs", + "value": "{{timeframe.unixEnd * 1000}}" + }, + { + "key": "lookback", + "value": "{{(timeframe.unixEnd - timeframe.unixStart) * 1000}}" + }, + { + "key": "step", + "value": "{{(timeframe.unixEnd - timeframe.unixStart) * 5}}" + }, + { + "key": "groupByOperation", + "value": "{{groupByOperation || null}}" + } + ], + "headers": [], + "postRequestScript": "serviceErrorRate.js", + "errorHandling": { + "type": "path", + "realm": "payload", + "path": "errors.0.msg" + } + }, + "ui": [ + { + "name": "groupByOperation", + "label": "Split by operation", + "type": "checkbox", + "help": "Returns one series per operation instead of one for the service as a whole." + } + ], + "metadata": [ + { + "name": "timestamp", + "displayName": "Time", + "shape": "date", + "role": "timestamp" + }, + { + "name": "service", + "displayName": "Service", + "shape": "string", + "role": "label" + }, + { + "name": "operation", + "displayName": "Operation", + "shape": "string" + }, + { + "name": "value", + "displayName": "Error Rate", + "shape": [ + "percent", + { + "asZeroToOne": true + } + ], + "role": "value" + } + ] +} diff --git a/plugins/Jaeger/v1/dataStreams/serviceLatency.json b/plugins/Jaeger/v1/dataStreams/serviceLatency.json new file mode 100644 index 00000000..66d40df4 --- /dev/null +++ b/plugins/Jaeger/v1/dataStreams/serviceLatency.json @@ -0,0 +1,104 @@ +{ + "name": "serviceLatency", + "displayName": "Service Latency", + "description": "Request latency at a chosen quantile (requires Jaeger's metrics backend)", + "tags": [ + "Performance" + ], + "baseDataSourceName": "httpRequestScopedSingle", + "matches": { + "sourceType": { + "type": "equals", + "value": "Service" + } + }, + "timeframes": [ + "last1hour", + "last12hours", + "last24hours", + "last7days", + "last30days" + ], + "config": { + "httpMethod": "get", + "endpointPath": "/api/metrics/latencies", + "getArgs": [ + { + "key": "service", + "value": "{{object.rawId}}" + }, + { + "key": "endTs", + "value": "{{timeframe.unixEnd * 1000}}" + }, + { + "key": "lookback", + "value": "{{(timeframe.unixEnd - timeframe.unixStart) * 1000}}" + }, + { + "key": "step", + "value": "{{(timeframe.unixEnd - timeframe.unixStart) * 5}}" + }, + { + "key": "groupByOperation", + "value": "{{groupByOperation || null}}" + }, + { + "key": "quantile", + "value": "{{quantile}}" + } + ], + "headers": [], + "postRequestScript": "serviceLatency.js", + "errorHandling": { + "type": "path", + "realm": "payload", + "path": "errors.0.msg" + } + }, + "ui": [ + { + "name": "groupByOperation", + "label": "Split by operation", + "type": "checkbox", + "help": "Returns one series per operation instead of one for the service as a whole." + }, + { + "name": "quantile", + "label": "Quantile", + "type": "number", + "defaultValue": 0.95, + "validation": { + "required": true, + "min": 0, + "max": 1 + }, + "help": "Between 0 and 1 — for example 0.95 for the 95th percentile." + } + ], + "metadata": [ + { + "name": "timestamp", + "displayName": "Time", + "shape": "date", + "role": "timestamp" + }, + { + "name": "service", + "displayName": "Service", + "shape": "string", + "role": "label" + }, + { + "name": "operation", + "displayName": "Operation", + "shape": "string" + }, + { + "name": "value", + "displayName": "Latency", + "shape": "milliseconds", + "role": "value" + } + ] +} diff --git a/plugins/Jaeger/v1/defaultContent/manifest.json b/plugins/Jaeger/v1/defaultContent/manifest.json index 31242423..334a5c0f 100644 --- a/plugins/Jaeger/v1/defaultContent/manifest.json +++ b/plugins/Jaeger/v1/defaultContent/manifest.json @@ -5,7 +5,11 @@ "type": "dashboard" }, { - "name": "service", + "name": "tracing", + "type": "dashboard" + }, + { + "name": "redMetrics", "type": "dashboard" } ] diff --git a/plugins/Jaeger/v1/defaultContent/redMetrics.dash.json b/plugins/Jaeger/v1/defaultContent/redMetrics.dash.json new file mode 100644 index 00000000..99513ea2 --- /dev/null +++ b/plugins/Jaeger/v1/defaultContent/redMetrics.dash.json @@ -0,0 +1,244 @@ +{ + "name": "RED Metrics", + "schemaVersion": "1.5", + "timeframe": "last24hours", + "variables": [ + "{{variables.[Service]}}" + ], + "dashboard": { + "_type": "layout/grid", + "columns": 6, + "version": 1, + "contents": [ + { + "i": "71f93292-d199-40b1-bb93-6acced2e26c7", + "x": 0, + "y": 0, + "w": 3, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Call Rate", + "description": "Requests per second served by this service", + "activePluginConfigIds": [ + "{{configId}}" + ], + "variables": [ + "{{variables.[Service]}}" + ], + "dataStream": { + "id": "{{dataStreams.[serviceCallRate]}}", + "name": "serviceCallRate", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Service]}}" + }, + "visualisation": { + "type": "data-stream-line-graph", + "config": { + "data-stream-line-graph": { + "xAxisData": "timestamp", + "yAxisData": [ + "value" + ], + "showLegend": false, + "showGrid": true + } + } + } + } + }, + { + "i": "4af46640-6220-466d-8fe4-77035b8eb03a", + "x": 3, + "y": 0, + "w": 3, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Error Rate", + "description": "Proportion of this service's requests that failed", + "activePluginConfigIds": [ + "{{configId}}" + ], + "variables": [ + "{{variables.[Service]}}" + ], + "dataStream": { + "id": "{{dataStreams.[serviceErrorRate]}}", + "name": "serviceErrorRate", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Service]}}" + }, + "visualisation": { + "type": "data-stream-line-graph", + "config": { + "data-stream-line-graph": { + "xAxisData": "timestamp", + "yAxisData": [ + "value" + ], + "showLegend": false, + "showGrid": true + } + } + } + } + }, + { + "i": "186f0034-a8de-4e2e-b244-ea21de7622a0", + "x": 0, + "y": 3, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Latency (P50)", + "description": "P50 request latency for this service", + "activePluginConfigIds": [ + "{{configId}}" + ], + "variables": [ + "{{variables.[Service]}}" + ], + "dataStream": { + "id": "{{dataStreams.[serviceLatency]}}", + "name": "serviceLatency", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "quantile": 0.5 + } + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Service]}}" + }, + "visualisation": { + "type": "data-stream-line-graph", + "config": { + "data-stream-line-graph": { + "xAxisData": "timestamp", + "yAxisData": [ + "value" + ], + "showLegend": false, + "showGrid": true + } + } + } + } + }, + { + "i": "b8f36c5e-3a03-4060-a6cb-47f5120beeb1", + "x": 2, + "y": 3, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Latency (P95)", + "description": "P95 request latency for this service", + "activePluginConfigIds": [ + "{{configId}}" + ], + "variables": [ + "{{variables.[Service]}}" + ], + "dataStream": { + "id": "{{dataStreams.[serviceLatency]}}", + "name": "serviceLatency", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "quantile": 0.95 + } + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Service]}}" + }, + "visualisation": { + "type": "data-stream-line-graph", + "config": { + "data-stream-line-graph": { + "xAxisData": "timestamp", + "yAxisData": [ + "value" + ], + "showLegend": false, + "showGrid": true + } + } + } + } + }, + { + "i": "eeed7ca5-30a2-4eb8-baf9-598a42beb4a0", + "x": 4, + "y": 3, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Latency (P99)", + "description": "P99 request latency for this service", + "activePluginConfigIds": [ + "{{configId}}" + ], + "variables": [ + "{{variables.[Service]}}" + ], + "dataStream": { + "id": "{{dataStreams.[serviceLatency]}}", + "name": "serviceLatency", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "quantile": 0.99 + } + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Service]}}" + }, + "visualisation": { + "type": "data-stream-line-graph", + "config": { + "data-stream-line-graph": { + "xAxisData": "timestamp", + "yAxisData": [ + "value" + ], + "showLegend": false, + "showGrid": true + } + } + } + } + } + ] + } +} diff --git a/plugins/Jaeger/v1/defaultContent/service.dash.json b/plugins/Jaeger/v1/defaultContent/tracing.dash.json similarity index 67% rename from plugins/Jaeger/v1/defaultContent/service.dash.json rename to plugins/Jaeger/v1/defaultContent/tracing.dash.json index ef515d97..6dca34a1 100644 --- a/plugins/Jaeger/v1/defaultContent/service.dash.json +++ b/plugins/Jaeger/v1/defaultContent/tracing.dash.json @@ -1,5 +1,5 @@ { - "name": "Service", + "name": "Tracing", "schemaVersion": "1.5", "timeframe": "last24hours", "variables": [ @@ -115,6 +115,60 @@ } } } + }, + { + "i": "42eeafa5-ece0-4e93-ac29-b1d1e160b6be", + "x": 0, + "y": 3, + "w": 4, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Dependencies", + "description": "Call paths into and out of this service", + "activePluginConfigIds": [ + "{{configId}}" + ], + "variables": [ + "{{variables.[Service]}}" + ], + "dataStream": { + "id": "{{dataStreams.[serviceDependencies]}}", + "name": "serviceDependencies", + "pluginConfigId": "{{configId}}", + "sort": { + "by": [ + [ + "callCount", + "desc" + ] + ] + } + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Service]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "key", + "parent", + "child", + "callCount" + ], + "hiddenColumns": [] + } + } + } + } } ] } diff --git a/plugins/Jaeger/v1/docs/README.md b/plugins/Jaeger/v1/docs/README.md index d071c484..7fe6d6cf 100644 --- a/plugins/Jaeger/v1/docs/README.md +++ b/plugins/Jaeger/v1/docs/README.md @@ -1,39 +1,63 @@ -Monitor services, dependencies, operations, and traces from a [Jaeger](https://www.jaegertracing.io/) distributed tracing backend, via its [Query API](https://www.jaegertracing.io/docs/latest/apis/). +Monitor services, dependencies, operations, traces and per-service performance from a [Jaeger](https://www.jaegertracing.io/) distributed tracing backend, via its [Query API](https://www.jaegertracing.io/docs/latest/apis/). > ⚠️ Jaeger has no built-in authentication. If your deployment sits behind an authenticating reverse proxy, this plugin does not support that — the Query API must be reachable without credentials from the URL you provide. ## Setup -You will need the base URL of your Jaeger **Query** service (the same one that serves the Jaeger UI) — for example `http://jaeger-query:16686`. +You will need the base URL of your Jaeger **Query** service (the same one that serves the Jaeger UI) — for example `http://jaeger-query:16686` — and to know which storage backend it is configured against. 1. If your Jaeger instance is only reachable on an internal network, install a SquaredUp [on-prem relay agent](https://docs.squaredup.com/) that can reach it, and select that agent group when adding this plugin. 2. Add this plugin and paste the base URL into the **URL** field — no scheme-relative path (like `/api`) and no trailing slash; the plugin appends those automatically. -3. If your Jaeger instance uses a self-signed certificate, enable **Ignore certificate errors**. +3. Set **Dependency storage** to match your backend — see [Dependency storage](#dependency-storage) below. +4. If your Jaeger instance uses a self-signed certificate, enable **Ignore certificate errors**. ## Configuration fields -| Field | What it is | Where to find it | Required | -| ----------------------------- | --------------------------------------------------------------- | ------------------------------------------------- | -------- | -| **URL** | The base URL of your Jaeger Query service. | The same host/port your Jaeger UI is served from. | Yes | -| **Ignore certificate errors** | Skips TLS certificate validation, for self-signed certificates. | — | No | +| Field | What it is | Where to find it | Required | +| ----------------------------- | --------------------------------------------------------------- | --------------------------------------------------- | -------- | +| **URL** | The base URL of your Jaeger Query service. | The same host/port your Jaeger UI is served from. | Yes | +| **Ignore certificate errors** | Skips TLS certificate validation, for self-signed certificates. | — | No | +| **Dependency storage** | How your backend produces the service dependency graph. | Your Jaeger storage configuration. | No | On save, the plugin validates the URL by fetching the list of known services; an unreachable URL or connection failure fails setup with a connection error. +## Dependency storage + +Jaeger produces its service dependency graph in one of two ways, and the dependency data streams have to ask for a different window depending on which. + +- **Pre-aggregated daily** — Elasticsearch, OpenSearch and Cassandra read the graph from a table the [`spark-dependencies`](https://github.com/jaegertracing/spark-dependencies) job writes on a schedule. That job stamps every batch it writes with **midnight UTC** rather than the time it ran, so a query for the last hour finds nothing for 23 hours of the day. Choose this option and the plugin asks for a 48-hour window, which always spans at least one written batch. Jaeger sums the call counts of the batches it finds, so a service pair is always a single row. +- **Derived from spans** — Badger and in-memory storage build the graph from the spans in the requested window. Choose this option and the plugin asks for a 1-hour window, which is as much as these backends can usually derive without timing out. + +Pre-aggregated is the default, and is what an existing configuration behaves as until you choose otherwise. If dependencies are missing when the Jaeger UI's System Architecture tab shows them, this is the setting to check first. + +## Service Performance Monitoring + +**Call Rate**, **Error Rate** and **Latency** read Jaeger's [Service Performance Monitoring](https://www.jaegertracing.io/docs/latest/spm/) endpoints. These only work if your Jaeger deployment has a metrics storage backend configured — span metrics produced by the OpenTelemetry Collector's `spanmetrics` connector, stored in Prometheus, with the Jaeger Query service pointed at it. If SPM is not enabled, these three streams return the error Jaeger itself reports and the other streams are unaffected. + +Each of the three accepts a **Split by operation** parameter, which returns one series per operation instead of one for the service as a whole. **Service Latency** also takes a **Quantile** — the RED Metrics perspective charts 0.5, 0.95 and 0.99 side by side, since a single quantile can't distinguish a slower median from a heavier tail. + ## What this plugin monitors - **Services** — every service Jaeger has seen spans for. -- **Dependencies** — call relationships between services, with call counts over the selected timeframe. +- **Dependencies** — call relationships between services, with call counts. - **Operations** — the operation (endpoint/method) names reported by a service. -- **Traces** — individual spans reported by a service within a selected timeframe, including duration, kind, and status. +- **Traces** — individual spans reported by a service, including duration, kind and status, each carrying its trace ID. +- **Performance** — call rate, error rate and latency per service, where SPM is enabled. -The out-of-the-box dashboards include an estate-wide **Overview** plus a **Service** perspective. +The out-of-the-box dashboards include an estate-wide **Overview**, plus two per-service perspectives: **Tracing** (operations, spans and call paths) and **RED Metrics** (call rate, error rate, and latency at the 50th, 95th and 99th percentiles). RED Metrics needs [SPM enabled](#service-performance-monitoring); Tracing works against any Jaeger. ## Data streams -- **Services** — services known to the Jaeger backend, account-wide. -- **Dependencies** — call dependencies between services, one row per parent-child pair, account-wide. -- **Operations** — operation names reported by a service, per service. -- **Traces** — spans reported by a service within the selected timeframe, per service. +| Stream | Scope | Returns | +| ------------------------ | ------------ | -------------------------------------------------------------------------- | +| **Services** | Account-wide | Services known to the Jaeger backend. | +| **Dependencies** | Account-wide | Call dependencies between services, one row per parent-child pair. | +| **Service Dependencies** | Per Service | Call paths into and out of one service. | +| **Operations** | Per Service | Operation names reported by a service. | +| **Traces** | Per Service | Spans reported by a service, filterable by operation and minimum duration. | +| **Service Call Rate** | Per Service | Requests per second, as a time series. | +| **Service Error Rate** | Per Service | Proportion of requests that failed, as a time series. | +| **Service Latency** | Per Service | Request latency at a chosen quantile, as a time series. | ## What gets indexed @@ -42,13 +66,15 @@ The out-of-the-box dashboards include an estate-wide **Overview** plus a **Servi | **Service** | `GET /api/v3/services` | A service Jaeger has seen spans for. | | **Dependency** | `GET /api/dependencies` | A call relationship between two services. | -Each Dependency row carries `parent` and `child` properties naming the two Services involved in that call path. +Each Dependency row carries `parent` and `child` properties naming the two Services involved. A correlation rule uses those Dependency objects to bridge the two Services directly, giving every caller a **depends on** relationship to the service it calls — so the estate's call graph is traversable service to service. ## Known limitations -- **Dependencies' timeframe is restricted to last 1 hour–last 7 days** — Jaeger's dependency-graph query is comparatively expensive, so the range of selectable timeframes is deliberately narrower than Traces' full range. -- **Scheduled indexing of Dependencies always uses a 1-hour trailing window** — the index step queries a fixed 1-hour trailing window rather than spanning the full interval between imports (imports run every 12 hours by default). A dependency between two services that only occurred outside that trailing hour won't be indexed until it recurs within one. -- **Traces are per-service only** — there's no cross-service trace search or single-trace detail view in this version. -- **No Service Performance Monitoring (SPM) metrics** — SPM requires a separate metrics storage backend that most Jaeger deployments don't enable, so it isn't covered here. +- **Dependencies are not timeframe-filtered** — the window is fixed by the **Dependency storage** setting rather than the dashboard timeframe, because pre-aggregated storage only writes the graph once per UTC day and a dashboard timeframe finer than that would silently return nothing. +- **Dependencies derived from spans lag behind** — under **Derived from spans**, the graph covers only the trailing hour, so a call path that last happened before then won't appear until it recurs. That 1-hour ceiling is what these backends can derive without timing out. +- **Traces are per-service only** — there's no cross-service trace search, and no single-trace detail view. Individual spans are listed per service with their trace ID, which you can paste into the Jaeger UI to see the full span tree. +- **Trace search can't filter by tag** — Jaeger's stable v3 API doesn't accept attribute filters over HTTP, so the Traces stream filters by operation and minimum duration only. +- **Filtering by operation still returns the whole trace** — Jaeger matches the operation to select traces, not spans, so the rows include every span in those traces, including ones named differently. +- **Service Performance Monitoring needs enabling in Jaeger** — see [above](#service-performance-monitoring). Without it the three performance streams return an error. - **No authentication** — the Jaeger Query API has none; this plugin can't authenticate through a reverse proxy that requires it. - **Read-only** — the plugin never creates, modifies, or deletes anything in Jaeger. diff --git a/plugins/Jaeger/v1/metadata.json b/plugins/Jaeger/v1/metadata.json index b4b32771..14acdff9 100644 --- a/plugins/Jaeger/v1/metadata.json +++ b/plugins/Jaeger/v1/metadata.json @@ -1,7 +1,7 @@ { "name": "jaeger", "displayName": "Jaeger", - "version": "1.0.0", + "version": "1.1.0", "author": { "name": "SquaredUp Labs", "type": "labs" @@ -20,7 +20,10 @@ "apm", "observability", "microservices", - "spans" + "spans", + "service map", + "latency", + "spm" ], "objectTypes": [ "Service", From bd4b7191ab309f837e303000be4e464c1a046212 Mon Sep 17 00:00:00 2001 From: Adam Kinniburgh Date: Thu, 17 Sep 2026 10:49:15 +0100 Subject: [PATCH 4/4] docs: cover both Jaeger SPM backend configurations --- plugins/Jaeger/v1/docs/README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/plugins/Jaeger/v1/docs/README.md b/plugins/Jaeger/v1/docs/README.md index 7fe6d6cf..40c1b41e 100644 --- a/plugins/Jaeger/v1/docs/README.md +++ b/plugins/Jaeger/v1/docs/README.md @@ -32,7 +32,12 @@ Pre-aggregated is the default, and is what an existing configuration behaves as ## Service Performance Monitoring -**Call Rate**, **Error Rate** and **Latency** read Jaeger's [Service Performance Monitoring](https://www.jaegertracing.io/docs/latest/spm/) endpoints. These only work if your Jaeger deployment has a metrics storage backend configured — span metrics produced by the OpenTelemetry Collector's `spanmetrics` connector, stored in Prometheus, with the Jaeger Query service pointed at it. If SPM is not enabled, these three streams return the error Jaeger itself reports and the other streams are unaffected. +**Call Rate**, **Error Rate** and **Latency** read Jaeger's [Service Performance Monitoring](https://www.jaegertracing.io/docs/latest/spm/) endpoints, which need a metrics backend configured on the Jaeger Query service. Jaeger supports two ways of providing one, and these streams work with either: + +- **Pre-computed into a PromQL-compatible store** — the OpenTelemetry Collector's `spanmetrics` connector derives RED metrics from spans and writes them to Prometheus (or any other PromQL-compatible backend), which Jaeger Query then reads. +- **Computed directly from trace storage** — on Elasticsearch and OpenSearch, Jaeger Query calculates the metrics at query time from the traces it already holds, with no connector and no separate metrics store. Point `jaeger_query.storage.metrics` at the same backend as `traces`. + +If neither is configured, these three streams return the error Jaeger itself reports and the other streams are unaffected. Each of the three accepts a **Split by operation** parameter, which returns one series per operation instead of one for the service as a whole. **Service Latency** also takes a **Quantile** — the RED Metrics perspective charts 0.5, 0.95 and 0.99 side by side, since a single quantile can't distinguish a slower median from a heavier tail.