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..287e95c2 --- /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 ? timeframe.unixEnd * 1000 : Date.now()}}" + }, + { + "key": "lookback", + "value": "{{(timeframe?.unixStart && timeframe?.unixEnd) ? (timeframe.unixEnd - timeframe.unixStart) * 1000 : 1800000}}" + } + ], + "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..157afed2 --- /dev/null +++ b/plugins/Jaeger/v1/dataStreams/scripts/traces.js @@ -0,0 +1,83 @@ +// 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 attrsToObject(attributes) { + const obj = {}; + (attributes || []).forEach((a) => { + const value = a.value || {}; + obj[a.key] = + value.stringValue ?? + value.intValue ?? + value.doubleValue ?? + value.boolValue; + }); + 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..9fc91192 --- /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** — indexing runs with no dashboard timeframe selected, so it falls back to querying just the hour before each run 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..c4130f11 --- /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": "none", + "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." + } +]