diff --git a/plugins/Jaeger/v1/configValidation.json b/plugins/Jaeger/v1/configValidation.json new file mode 100644 index 00000000..02ee322f --- /dev/null +++ b/plugins/Jaeger/v1/configValidation.json @@ -0,0 +1,13 @@ +{ + "steps": [ + { + "displayName": "Check connection", + "dataStream": { + "name": "services" + }, + "required": true, + "error": "Could not reach the Jaeger Query API. Check the URL is correct, that the instance is reachable (directly or via an on-prem relay agent), and enable 'Ignore certificate errors' if it uses a self-signed certificate.", + "success": "Connected successfully." + } + ] +} diff --git a/plugins/Jaeger/v1/cspell.json b/plugins/Jaeger/v1/cspell.json new file mode 100644 index 00000000..7f2ae8e3 --- /dev/null +++ b/plugins/Jaeger/v1/cspell.json @@ -0,0 +1,6 @@ +{ + "words": [ + "jaeger", + "opentelemetry" + ] +} diff --git a/plugins/Jaeger/v1/custom_types.json b/plugins/Jaeger/v1/custom_types.json new file mode 100644 index 00000000..dc4501f3 --- /dev/null +++ b/plugins/Jaeger/v1/custom_types.json @@ -0,0 +1,16 @@ +[ + { + "name": "Service", + "sourceType": "Service", + "icon": "server", + "singular": "Service", + "plural": "Services" + }, + { + "name": "Dependency", + "sourceType": "Dependency", + "icon": "share-nodes", + "singular": "Dependency", + "plural": "Dependencies" + } +] diff --git a/plugins/Jaeger/v1/dataStreams/dependencies.json b/plugins/Jaeger/v1/dataStreams/dependencies.json new file mode 100644 index 00000000..ab9cb116 --- /dev/null +++ b/plugins/Jaeger/v1/dataStreams/dependencies.json @@ -0,0 +1,68 @@ +{ + "name": "dependencies", + "displayName": "Dependencies", + "description": "Call dependencies between services, one row per parent-child pair", + "tags": [ + "Services" + ], + "baseDataSourceName": "httpRequestUnscoped", + "matches": "none", + "timeframes": [ + "last1hour", + "last12hours", + "last24hours", + "last7days" + ], + "config": { + "httpMethod": "get", + "endpointPath": "/api/dependencies", + "pathToData": "data", + "getArgs": [ + { + "key": "endTs", + "value": "{{timeframe.unixEnd * 1000}}" + }, + { + "key": "lookback", + "value": "{{(timeframe.unixEnd - timeframe.unixStart) * 1000}}" + } + ], + "headers": [] + }, + "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 + } + ] + }, + { + "name": "source", + "displayName": "Source", + "shape": "string", + "visible": false + } + ] +} diff --git a/plugins/Jaeger/v1/dataStreams/operations.json b/plugins/Jaeger/v1/dataStreams/operations.json new file mode 100644 index 00000000..8dde26d3 --- /dev/null +++ b/plugins/Jaeger/v1/dataStreams/operations.json @@ -0,0 +1,41 @@ +{ + "name": "operations", + "displayName": "Operations", + "description": "Operation names reported by a service", + "tags": [ + "Operations" + ], + "baseDataSourceName": "httpRequestScopedSingle", + "matches": { + "sourceType": { + "type": "equals", + "value": "Service" + } + }, + "timeframes": false, + "config": { + "httpMethod": "get", + "endpointPath": "/api/v3/operations", + "getArgs": [ + { + "key": "service", + "value": "{{object.rawId}}" + } + ], + "headers": [], + "pathToData": "operations" + }, + "metadata": [ + { + "name": "name", + "displayName": "Operation", + "shape": "string", + "role": "label" + }, + { + "name": "spanKind", + "displayName": "Span Kind", + "shape": "string" + } + ] +} diff --git a/plugins/Jaeger/v1/dataStreams/scripts/traces.js b/plugins/Jaeger/v1/dataStreams/scripts/traces.js new file mode 100644 index 00000000..fe4939d5 --- /dev/null +++ b/plugins/Jaeger/v1/dataStreams/scripts/traces.js @@ -0,0 +1,99 @@ +// dataStreams/scripts/traces.js +// +// The /api/v3/traces response is a single JSON object shaped +// { "result": { "resourceSpans": [...] } } (OTLP JSON) - not NDJSON - so the +// platform's automatic JSON parse populates `data` and we read it directly. +// +// OTLP JSON encodes int64 (startTimeUnixNano/endTimeUnixNano) as STRING. +// Epoch nanoseconds (~1.7e18) exceed JS Number safe-integer precision, so we +// do the nanosecond arithmetic in BigInt and only convert to Number once +// we've reduced to milliseconds (~1.7e12, well within safe range). + +const SPAN_KIND = { + 0: "UNSPECIFIED", + 1: "INTERNAL", + 2: "SERVER", + 3: "CLIENT", + 4: "PRODUCER", + 5: "CONSUMER", +}; + +const STATUS_CODE = { + 0: "UNSET", + 1: "OK", + 2: "ERROR", +}; + +function anyValueToJs(value) { + if (!value) return undefined; + if (value.arrayValue) { + return (value.arrayValue.values || []).map(anyValueToJs); + } + if (value.kvlistValue) { + const obj = {}; + (value.kvlistValue.values || []).forEach((kv) => { + obj[kv.key] = anyValueToJs(kv.value); + }); + return obj; + } + return ( + value.stringValue ?? + value.boolValue ?? + value.intValue ?? + value.doubleValue ?? + value.bytesValue + ); +} + +function attrsToObject(attributes) { + const obj = {}; + (attributes || []).forEach((a) => { + obj[a.key] = anyValueToJs(a.value); + }); + return obj; +} + +function nanoStrToBig(nanoStr) { + return BigInt(nanoStr || "0"); +} + +const resourceSpans = (data && data.result && data.result.resourceSpans) || []; + +result = _.flatMap(resourceSpans, (rs) => { + const resourceAttrs = attrsToObject((rs.resource || {}).attributes); + const serviceName = resourceAttrs["service.name"] || ""; + + return _.flatMap(rs.scopeSpans || [], (ss) => { + return (ss.spans || []).map((s) => { + const startNsBig = nanoStrToBig(s.startTimeUnixNano); + const endNsBig = nanoStrToBig(s.endTimeUnixNano); + // Compute duration from the full-precision nanosecond values before + // rounding to ms — rounding each endpoint first and then subtracting + // can be off by up to 1ms (e.g. start=1.999999ms, end=2.000001ms + // truncate to 1ms/2ms, giving a 1ms duration for a ~2ns span). + const durationMsBig = (endNsBig - startNsBig) / 1000000n; + const status = s.status || {}; + // proto3 JSON omits fields at their default value, so an + // INTERNAL/unspecified-kind span (kind 0) typically has no `kind` + // field at all — default the missing case to 0 rather than + // stringifying `undefined`. + const kind = s.kind ?? 0; + + return { + traceId: s.traceId, + spanId: s.spanId, + parentSpanId: s.parentSpanId || "", + operationName: s.name, + serviceName, + kind: SPAN_KIND[kind] ?? String(kind), + startTime: new Date( + Number(startNsBig / 1000000n), + ).toISOString(), + durationMs: Number(durationMsBig), + statusCode: STATUS_CODE[status.code] ?? "UNSET", + statusMessage: status.message || "", + attributes: attrsToObject(s.attributes), + }; + }); + }); +}); diff --git a/plugins/Jaeger/v1/dataStreams/services.json b/plugins/Jaeger/v1/dataStreams/services.json new file mode 100644 index 00000000..f88c3abd --- /dev/null +++ b/plugins/Jaeger/v1/dataStreams/services.json @@ -0,0 +1,25 @@ +{ + "name": "services", + "displayName": "Services", + "description": "Services known to the Jaeger backend", + "tags": [ + "Services" + ], + "baseDataSourceName": "httpRequestUnscoped", + "matches": "none", + "timeframes": false, + "config": { + "httpMethod": "get", + "endpointPath": "/api/v3/services", + "postRequestScript": "result = (data.services || []).map(service => ({ service }));", + "headers": [] + }, + "metadata": [ + { + "name": "service", + "displayName": "Service", + "shape": "string", + "role": "label" + } + ] +} diff --git a/plugins/Jaeger/v1/dataStreams/traces.json b/plugins/Jaeger/v1/dataStreams/traces.json new file mode 100644 index 00000000..efe7b348 --- /dev/null +++ b/plugins/Jaeger/v1/dataStreams/traces.json @@ -0,0 +1,113 @@ +{ + "name": "traces", + "displayName": "Traces", + "description": "Spans reported by a service within the selected timeframe", + "tags": [ + "Traces" + ], + "baseDataSourceName": "httpRequestScopedSingle", + "matches": { + "sourceType": { + "type": "equals", + "value": "Service" + } + }, + "timeframes": true, + "config": { + "httpMethod": "get", + "endpointPath": "/api/v3/traces", + "getArgs": [ + { + "key": "query.service_name", + "value": "{{object.rawId}}" + }, + { + "key": "query.start_time_min", + "value": "{{timeframe.start}}" + }, + { + "key": "query.start_time_max", + "value": "{{timeframe.end}}" + }, + { + "key": "query.search_depth", + "value": "{{limit}}" + } + ], + "headers": [], + "postRequestScript": "traces.js" + }, + "ui": [ + { + "name": "limit", + "label": "Number of Traces", + "type": "number", + "defaultValue": 20, + "min": 1 + } + ], + "metadata": [ + { + "name": "traceId", + "displayName": "Trace ID", + "shape": "string" + }, + { + "name": "spanId", + "displayName": "Span ID", + "shape": "string", + "visible": false + }, + { + "name": "parentSpanId", + "displayName": "Parent Span ID", + "shape": "string", + "visible": false + }, + { + "name": "operationName", + "displayName": "Operation", + "shape": "string", + "role": "label" + }, + { + "name": "serviceName", + "displayName": "Service", + "shape": "string" + }, + { + "name": "kind", + "displayName": "Kind", + "shape": "string" + }, + { + "name": "startTime", + "displayName": "Start Time", + "shape": "date", + "role": "timestamp" + }, + { + "name": "durationMs", + "displayName": "Duration (ms)", + "shape": "milliseconds", + "role": "value" + }, + { + "name": "statusCode", + "displayName": "Status Code", + "shape": "string" + }, + { + "name": "statusMessage", + "displayName": "Status Message", + "shape": "string", + "visible": false + }, + { + "name": "attributes", + "displayName": "Attributes", + "shape": "json", + "visible": false + } + ] +} diff --git a/plugins/Jaeger/v1/defaultContent/manifest.json b/plugins/Jaeger/v1/defaultContent/manifest.json new file mode 100644 index 00000000..31242423 --- /dev/null +++ b/plugins/Jaeger/v1/defaultContent/manifest.json @@ -0,0 +1,12 @@ +{ + "items": [ + { + "name": "overview", + "type": "dashboard" + }, + { + "name": "service", + "type": "dashboard" + } + ] +} diff --git a/plugins/Jaeger/v1/defaultContent/overview.dash.json b/plugins/Jaeger/v1/defaultContent/overview.dash.json new file mode 100644 index 00000000..ef72d0a0 --- /dev/null +++ b/plugins/Jaeger/v1/defaultContent/overview.dash.json @@ -0,0 +1,145 @@ +{ + "name": "Overview", + "schemaVersion": "1.5", + "timeframe": "last1hour", + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "ca8ac41e-7b75-444a-a116-fd28fc6d3bba", + "x": 0, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Services", + "description": "Total number of services known to Jaeger", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[services]}}", + "name": "services", + "pluginConfigId": "{{configId}}", + "group": { + "by": [], + "aggregate": [ + { + "type": "count" + } + ] + } + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count", + "comparisonColumn": "none", + "label": "Services" + } + } + } + } + }, + { + "i": "09b6f5ed-2565-42d0-8295-e4b1fedaa2e3", + "x": 1, + "y": 0, + "w": 3, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Services", + "description": "Services known to the Jaeger backend", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[services]}}", + "name": "services", + "pluginConfigId": "{{configId}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false + } + } + } + } + }, + { + "i": "3479eea2-6a91-44e3-9b3b-c22f90edfd98", + "x": 0, + "y": 2, + "w": 4, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Top Dependencies by Call Count", + "description": "Busiest service-to-service call paths within the selected timeframe", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[dependencies]}}", + "name": "dependencies", + "pluginConfigId": "{{configId}}", + "sort": { + "by": [ + [ + "callCount", + "desc" + ] + ], + "top": 10 + } + }, + "visualisation": { + "type": "data-stream-bar-chart", + "config": { + "data-stream-bar-chart": { + "xAxisData": "key", + "yAxisData": [ + "callCount" + ], + "xAxisGroup": "none", + "xAxisLabel": "", + "yAxisLabel": "Calls", + "showXAxisLabel": false, + "showYAxisLabel": true, + "showLegend": false, + "legendPosition": "bottom", + "showGrid": true, + "horizontalLayout": "vertical", + "displayMode": "actual", + "showTotals": false, + "showValue": false, + "grouping": false, + "range": { + "type": "auto" + } + } + } + } + } + } + ] + } +} diff --git a/plugins/Jaeger/v1/defaultContent/scopes.json b/plugins/Jaeger/v1/defaultContent/scopes.json new file mode 100644 index 00000000..8d5adf22 --- /dev/null +++ b/plugins/Jaeger/v1/defaultContent/scopes.json @@ -0,0 +1,19 @@ +[ + { + "name": "Services", + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "Service" + ] + } + }, + "variable": { + "name": "Service", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + } +] diff --git a/plugins/Jaeger/v1/defaultContent/service.dash.json b/plugins/Jaeger/v1/defaultContent/service.dash.json new file mode 100644 index 00000000..ef515d97 --- /dev/null +++ b/plugins/Jaeger/v1/defaultContent/service.dash.json @@ -0,0 +1,121 @@ +{ + "name": "Service", + "schemaVersion": "1.5", + "timeframe": "last24hours", + "variables": [ + "{{variables.[Service]}}" + ], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "62d75b50-0b77-4358-947f-cb76744a6044", + "x": 0, + "y": 0, + "w": 1, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Operations", + "description": "Operation names reported by this service", + "activePluginConfigIds": [ + "{{configId}}" + ], + "timeframe": "none", + "variables": [ + "{{variables.[Service]}}" + ], + "dataStream": { + "id": "{{dataStreams.[operations]}}", + "name": "operations", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Service]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "name", + "spanKind" + ] + } + } + } + } + }, + { + "i": "5b847262-6bde-4b0c-9ae9-c2aba620599b", + "x": 1, + "y": 0, + "w": 3, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Traces", + "description": "Spans reported by this service within the selected timeframe", + "activePluginConfigIds": [ + "{{configId}}" + ], + "variables": [ + "{{variables.[Service]}}" + ], + "dataStream": { + "id": "{{dataStreams.[traces]}}", + "name": "traces", + "pluginConfigId": "{{configId}}", + "sort": { + "by": [ + [ + "startTime", + "desc" + ] + ] + } + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Service]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "traceId", + "operationName", + "serviceName", + "kind", + "startTime", + "durationMs", + "statusCode" + ], + "hiddenColumns": [ + "spanId", + "parentSpanId", + "statusMessage", + "attributes" + ] + } + } + } + } + } + ] + } +} diff --git a/plugins/Jaeger/v1/docs/README.md b/plugins/Jaeger/v1/docs/README.md new file mode 100644 index 00000000..d071c484 --- /dev/null +++ b/plugins/Jaeger/v1/docs/README.md @@ -0,0 +1,54 @@ +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/). + +> ⚠️ 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`. + +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**. + +## 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 | + +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. + +## What this plugin monitors + +- **Services** — every service Jaeger has seen spans for. +- **Dependencies** — call relationships between services, with call counts over the selected timeframe. +- **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. + +The out-of-the-box dashboards include an estate-wide **Overview** plus a **Service** perspective. + +## 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. + +## What gets indexed + +| Object type | API source | Represents | +| -------------- | ------------------------- | ------------------------------------------ | +| **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. + +## 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. +- **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/icon.png b/plugins/Jaeger/v1/icon.png new file mode 100644 index 00000000..2e2c6594 Binary files /dev/null and b/plugins/Jaeger/v1/icon.png differ diff --git a/plugins/Jaeger/v1/indexDefinitions/default.json b/plugins/Jaeger/v1/indexDefinitions/default.json new file mode 100644 index 00000000..aabecbdc --- /dev/null +++ b/plugins/Jaeger/v1/indexDefinitions/default.json @@ -0,0 +1,37 @@ +{ + "steps": [ + { + "name": "services", + "dataStream": { + "name": "services" + }, + "timeframe": "none", + "objectMapping": { + "id": "service", + "name": "service", + "type": { + "value": "Service" + } + } + }, + { + "name": "dependencies", + "dataStream": { + "name": "dependencies" + }, + "timeframe": "last1hour", + "objectMapping": { + "id": "key", + "name": "key", + "type": { + "value": "Dependency" + }, + "properties": [ + "parent", + "child", + "callCount" + ] + } + } + ] +} diff --git a/plugins/Jaeger/v1/metadata.json b/plugins/Jaeger/v1/metadata.json new file mode 100644 index 00000000..b4b32771 --- /dev/null +++ b/plugins/Jaeger/v1/metadata.json @@ -0,0 +1,51 @@ +{ + "name": "jaeger", + "displayName": "Jaeger", + "version": "1.0.0", + "author": { + "name": "SquaredUp Labs", + "type": "labs" + }, + "description": "Monitor services, dependencies, operations and traces from a Jaeger distributed tracing backend.", + "category": "APM", + "type": "hybrid", + "restrictedToPlatforms": [], + "importNotSupported": false, + "schemaVersion": "2.1", + "keywords": [ + "jaeger", + "tracing", + "distributed tracing", + "opentelemetry", + "apm", + "observability", + "microservices", + "spans" + ], + "objectTypes": [ + "Service", + "Dependency" + ], + "links": [ + { + "category": "documentation", + "url": "https://github.com/squaredup/plugins/blob/main/plugins/Jaeger/v1/docs/README.md", + "label": "Help adding this plugin" + }, + { + "category": "source", + "url": "https://github.com/squaredup/plugins/tree/main/plugins/Jaeger/v1", + "label": "Repository" + } + ], + "base": { + "plugin": "WebAPI", + "majorVersion": "1", + "config": { + "headers": [], + "authMode": "none", + "baseUrl": "{{url}}", + "ignoreCertificateErrors": "{{ignoreCertificateErrors}}" + } + } +} diff --git a/plugins/Jaeger/v1/ui.json b/plugins/Jaeger/v1/ui.json new file mode 100644 index 00000000..41e003df --- /dev/null +++ b/plugins/Jaeger/v1/ui.json @@ -0,0 +1,18 @@ +[ + { + "type": "text", + "name": "url", + "label": "URL", + "help": "The base URL of your Jaeger Query service, including scheme and port — for example `http://jaeger-query:16686`. Do not include a trailing slash or a path such as `/api`; the plugin appends those automatically.", + "placeholder": "http://jaeger-query:16686", + "validation": { + "required": true + } + }, + { + "type": "checkbox", + "name": "ignoreCertificateErrors", + "label": "Ignore certificate errors", + "help": "Enable when connecting to a Jaeger instance that uses a self-signed certificate." + } +]