From 77379cb28b54a7627f393e6d1e46623df9fdabd5 Mon Sep 17 00:00:00 2001 From: John Hayes Date: Tue, 22 Sep 2026 10:27:36 +0100 Subject: [PATCH 01/22] Add OpenObserve plugin shell (streams, config validation) Deployable shell for OpenObserve Cloud: metadata/ui config, icon, and a config-validation stream against the streams list endpoint. Auth is still being worked out with OpenObserve engineering, so the config currently takes a raw Authorization key rather than separate username/password. Co-Authored-By: Claude Sonnet 5 --- plugins/OpenObserve/v1/configValidation.json | 11 ++++ .../OpenObserve/v1/dataStreams/streams.json | 19 +++++++ plugins/OpenObserve/v1/docs/README.md | 42 ++++++++++++++++ plugins/OpenObserve/v1/icon.svg | 10 ++++ plugins/OpenObserve/v1/metadata.json | 41 +++++++++++++++ plugins/OpenObserve/v1/ui.json | 50 +++++++++++++++++++ 6 files changed, 173 insertions(+) create mode 100644 plugins/OpenObserve/v1/configValidation.json create mode 100644 plugins/OpenObserve/v1/dataStreams/streams.json create mode 100644 plugins/OpenObserve/v1/docs/README.md create mode 100644 plugins/OpenObserve/v1/icon.svg create mode 100644 plugins/OpenObserve/v1/metadata.json create mode 100644 plugins/OpenObserve/v1/ui.json diff --git a/plugins/OpenObserve/v1/configValidation.json b/plugins/OpenObserve/v1/configValidation.json new file mode 100644 index 00000000..f6211c7d --- /dev/null +++ b/plugins/OpenObserve/v1/configValidation.json @@ -0,0 +1,11 @@ +{ + "steps": [ + { + "displayName": "Authenticate", + "dataStream": { "name": "streams" }, + "required": true, + "error": "Could not authenticate. Check your region, organization, email, and password/token.", + "success": "Connected successfully." + } + ] +} diff --git a/plugins/OpenObserve/v1/dataStreams/streams.json b/plugins/OpenObserve/v1/dataStreams/streams.json new file mode 100644 index 00000000..5a14656c --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/streams.json @@ -0,0 +1,19 @@ +{ + "name": "streams", + "displayName": "Streams", + "description": "Streams registered in the organization, with stats, schema, and settings", + "tags": ["Streams"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "streams", + "getArgs": [ + { "key": "type", "value": "logs" }, + { "key": "fetchSchema", "value": "false" } + ], + "pathToData": "list" + }, + "matches": "none", + "metadata": [{ "pattern": ".*" }], + "timeframes": false +} diff --git a/plugins/OpenObserve/v1/docs/README.md b/plugins/OpenObserve/v1/docs/README.md new file mode 100644 index 00000000..60692196 --- /dev/null +++ b/plugins/OpenObserve/v1/docs/README.md @@ -0,0 +1,42 @@ +Monitor your [OpenObserve](https://openobserve.ai) Cloud organization in SquaredUp — streams, log search, functions, and cluster health — via the [OpenObserve API](https://openobserve.ai/docs/reference/api/). + +> ⚠️ This plugin covers **OpenObserve Cloud** only, and only the **Streams**, **Search**, **Functions**, and **Cluster** API areas — it does not cover self-hosted/on-prem OpenObserve, ingestion, dashboards, reports, or user management. + +## Setup + +You will need your OpenObserve **organization identifier** and an **Authorization key** — the full HTTP Basic Auth value for your organization. + +1. Sign in to [OpenObserve Cloud](https://cloud.openobserve.ai) in the region your organization was created in. +2. Find your **organization identifier**: open **Data Sources** in the sidebar and copy it from the sample cURL command shown there — it's the path segment right after `/api/`. +3. Create a Service Account: go to **IAM → Service Accounts**, click **Add Service Account**, and give it a name. Copy the **token** shown immediately after creation — it is shown only once. +4. Build the **Authorization key**: base64-encode `:` — for a Service Account, the login identity is auto-generated as `.@sa.internal` (not the plain name you typed), e.g.: + ``` + echo -n "myaccount.myorgid@sa.internal:mytoken" | base64 + ``` +5. Paste your region, organization identifier, and the resulting Authorization key into the fields below. The key is sent through exactly as entered — it is not re-encoded. + +## Configuration fields + +| Field | What it is | Where to find it | Required | +| ----- | ---------- | ----------------- | -------- | +| **Region** | Which OpenObserve Cloud region your organization lives in. | The URL you sign in at (`cloud.openobserve.ai`, `us2.openobserve.ai`, or `eu1.openobserve.ai`). | Yes | +| **Organization** | The organization identifier used in every API call. | OpenObserve Cloud → **Data Sources** → the sample cURL command's `/api//...` segment. | Yes | +| **Authorization key** | The full base64-encoded `username:password`/`username:token` pair, sent unmodified as `Authorization: Basic `. | Built from a Service Account's login identity + token (see Setup step 4). | Yes | + +On save, the plugin validates these by fetching your organization's stream list; an invalid region, organization, or Authorization key fails setup with an authentication error. + +## What this plugin monitors + + + +## Data streams + + + +## What gets indexed + + + +## Known limitations + + diff --git a/plugins/OpenObserve/v1/icon.svg b/plugins/OpenObserve/v1/icon.svg new file mode 100644 index 00000000..47c2dca1 --- /dev/null +++ b/plugins/OpenObserve/v1/icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/plugins/OpenObserve/v1/metadata.json b/plugins/OpenObserve/v1/metadata.json new file mode 100644 index 00000000..334fd576 --- /dev/null +++ b/plugins/OpenObserve/v1/metadata.json @@ -0,0 +1,41 @@ +{ + "name": "openobserve", + "displayName": "OpenObserve", + "version": "1.0.0", + "author": { + "name": "SquaredUp Labs", + "type": "labs" + }, + "description": "Monitor streams, run log search, and check functions and cluster health in your OpenObserve Cloud organization.", + "category": "Monitoring", + "type": "hybrid", + "schemaVersion": "2.1", + "importNotSupported": false, + "restrictedToPlatforms": [], + "keywords": ["observability", "logs", "log search", "openobserve"], + "objectTypes": ["OpenObserve Stream"], + "links": [ + { + "category": "documentation", + "url": "https://github.com/squaredup/plugins/blob/main/plugins/OpenObserve/v1/docs/README.md", + "label": "Help adding this plugin" + }, + { + "category": "source", + "url": "https://github.com/squaredup/plugins/tree/main/plugins/OpenObserve/v1", + "label": "Repository" + } + ], + "base": { + "plugin": "WebAPI", + "majorVersion": "1", + "config": { + "baseUrl": "{{region}}/api/{{organization}}", + "authMode": "none", + "headers": [ + { "key": "Authorization", "value": "Basic {{key}}" } + ], + "queryArgs": [] + } + } +} diff --git a/plugins/OpenObserve/v1/ui.json b/plugins/OpenObserve/v1/ui.json new file mode 100644 index 00000000..a66b6789 --- /dev/null +++ b/plugins/OpenObserve/v1/ui.json @@ -0,0 +1,50 @@ +[ + { + "type": "autocomplete", + "name": "region", + "label": "Region", + "help": "The OpenObserve Cloud region your organization was created in — matches the URL you sign in at.", + "allowCustomValues": false, + "isMulti": false, + "isClearable": false, + "validation": { + "required": true + }, + "data": { + "source": "fixed", + "values": [ + { + "value": "https://cloud.openobserve.ai", + "label": "US East (Ohio) — cloud.openobserve.ai" + }, + { + "value": "https://us2.openobserve.ai", + "label": "US West (California) — us2.openobserve.ai" + }, + { + "value": "https://eu1.openobserve.ai", + "label": "EU (Frankfurt) — eu1.openobserve.ai" + } + ] + } + }, + { + "type": "text", + "name": "organization", + "label": "Organization", + "placeholder": "default", + "help": "Your OpenObserve organization identifier. Find it in **Data Sources** in the OpenObserve sidebar — it's the segment of the sample cURL command right after `/api/`.", + "validation": { + "required": true + } + }, + { + "type": "password", + "name": "key", + "label": "Authorization key", + "help": "The full HTTP Basic Authorization value for your organization — sent through unmodified as `Authorization: Basic `. This is **not** a plain password or token; it's the already base64-encoded `username:password` (or `username:token`) pair. Paste it exactly as given to you — it is not re-encoded.", + "validation": { + "required": true + } + } +] From 60b40e952c47dd92ebaffc7e15b59fcb963c9cc4 Mon Sep 17 00:00:00 2001 From: John Hayes Date: Thu, 15 Oct 2026 16:06:32 +0100 Subject: [PATCH 02/22] Complete OpenObserve plugin: self-hosted auth, import, streams, dashboards Pivots auth from OpenObserve Cloud (Basic auth via platform authMode kept failing against the hosted API from SquaredUp's backend) to self-hosted instances, where a raw pass-through Authorization key resolved it. Adds the full plugin: stream import (logs/metrics/traces into one OpenObserve Stream object type), five data streams (stream details, functions, search, field values, cluster info), an Overview and Stream perspective dashboard, and custom type metadata. Co-Authored-By: Claude Sonnet 5 --- plugins/OpenObserve/v1/custom_types.json | 9 + .../v1/dataStreams/clusterInfo.json | 20 + .../OpenObserve/v1/dataStreams/functions.json | 22 ++ .../v1/dataStreams/scripts/clusterInfo.js | 7 + .../scripts/errorHandling/clusterInfo.js | 5 + .../v1/dataStreams/scripts/search.js | 11 + .../dataStreams/scripts/streamFieldValues.js | 7 + .../OpenObserve/v1/dataStreams/search.json | 50 +++ .../v1/dataStreams/streamDetails.json | 48 +++ .../v1/dataStreams/streamFieldValues.json | 53 +++ .../OpenObserve/v1/dataStreams/streams.json | 40 +- .../v1/defaultContent/manifest.json | 6 + .../overviewDashboard.dash.json | 357 ++++++++++++++++++ .../defaultContent/streamDashboard.dash.json | 133 +++++++ plugins/OpenObserve/v1/docs/README.md | 51 ++- .../v1/indexDefinitions/default.json | 52 +++ plugins/OpenObserve/v1/metadata.json | 11 +- plugins/OpenObserve/v1/scopes.json | 14 + plugins/OpenObserve/v1/ui.json | 54 ++- 19 files changed, 897 insertions(+), 53 deletions(-) create mode 100644 plugins/OpenObserve/v1/custom_types.json create mode 100644 plugins/OpenObserve/v1/dataStreams/clusterInfo.json create mode 100644 plugins/OpenObserve/v1/dataStreams/functions.json create mode 100644 plugins/OpenObserve/v1/dataStreams/scripts/clusterInfo.js create mode 100644 plugins/OpenObserve/v1/dataStreams/scripts/errorHandling/clusterInfo.js create mode 100644 plugins/OpenObserve/v1/dataStreams/scripts/search.js create mode 100644 plugins/OpenObserve/v1/dataStreams/scripts/streamFieldValues.js create mode 100644 plugins/OpenObserve/v1/dataStreams/search.json create mode 100644 plugins/OpenObserve/v1/dataStreams/streamDetails.json create mode 100644 plugins/OpenObserve/v1/dataStreams/streamFieldValues.json create mode 100644 plugins/OpenObserve/v1/defaultContent/manifest.json create mode 100644 plugins/OpenObserve/v1/defaultContent/overviewDashboard.dash.json create mode 100644 plugins/OpenObserve/v1/defaultContent/streamDashboard.dash.json create mode 100644 plugins/OpenObserve/v1/indexDefinitions/default.json create mode 100644 plugins/OpenObserve/v1/scopes.json diff --git a/plugins/OpenObserve/v1/custom_types.json b/plugins/OpenObserve/v1/custom_types.json new file mode 100644 index 00000000..20930744 --- /dev/null +++ b/plugins/OpenObserve/v1/custom_types.json @@ -0,0 +1,9 @@ +[ + { + "name": "OpenObserve Stream", + "sourceType": "OpenObserve Stream", + "icon": "stream", + "singular": "Stream", + "plural": "Streams" + } +] diff --git a/plugins/OpenObserve/v1/dataStreams/clusterInfo.json b/plugins/OpenObserve/v1/dataStreams/clusterInfo.json new file mode 100644 index 00000000..6686680a --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/clusterInfo.json @@ -0,0 +1,20 @@ +{ + "name": "clusterInfo", + "displayName": "Cluster Info", + "description": "Pending compaction jobs per node, grouped by region, for the OpenObserve cluster", + "tags": ["Cluster"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "cluster_info", + "postRequestScript": "clusterInfo.js", + "errorHandling": { "type": "script", "script": "errorHandling/clusterInfo.js" } + }, + "matches": "none", + "metadata": [ + { "name": "region", "displayName": "Region", "shape": "string" }, + { "name": "node", "displayName": "Node", "shape": "string", "role": "label" }, + { "name": "pendingJobs", "displayName": "Pending Compaction Jobs", "shape": "number" } + ], + "timeframes": false +} diff --git a/plugins/OpenObserve/v1/dataStreams/functions.json b/plugins/OpenObserve/v1/dataStreams/functions.json new file mode 100644 index 00000000..dadb3c7e --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/functions.json @@ -0,0 +1,22 @@ +{ + "name": "functions", + "displayName": "Functions", + "description": "VRL functions defined in the organization", + "tags": ["Functions"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "functions", + "pathToData": "list" + }, + "matches": "none", + "metadata": [ + { "name": "stream_name", "displayName": "Stream", "shape": "string" }, + { "name": "name", "displayName": "Function Name", "shape": "string", "role": "label" }, + { "name": "function", "displayName": "Function Body", "shape": "string" }, + { "name": "order", "displayName": "Order", "shape": "number" }, + { "name": "num_args", "displayName": "Argument Count", "shape": "number" }, + { "name": "trans_type", "displayName": "Transform Type", "shape": "number" } + ], + "timeframes": false +} diff --git a/plugins/OpenObserve/v1/dataStreams/scripts/clusterInfo.js b/plugins/OpenObserve/v1/dataStreams/scripts/clusterInfo.js new file mode 100644 index 00000000..be350c6d --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/scripts/clusterInfo.js @@ -0,0 +1,7 @@ +result = Object.entries(data.regions || {}).flatMap(([region, nodes]) => + Object.entries(nodes).map(([node, info]) => ({ + region, + node, + pendingJobs: info.pending_jobs, + })) +); diff --git a/plugins/OpenObserve/v1/dataStreams/scripts/errorHandling/clusterInfo.js b/plugins/OpenObserve/v1/dataStreams/scripts/errorHandling/clusterInfo.js new file mode 100644 index 00000000..0f722bd2 --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/scripts/errorHandling/clusterInfo.js @@ -0,0 +1,5 @@ +result = + response.status === 404 + ? "Cluster info is not available on this instance — this endpoint requires a clustered OpenObserve deployment." + : (data && (data.message || data.error)) || + "Request failed with HTTP " + response.status; diff --git a/plugins/OpenObserve/v1/dataStreams/scripts/search.js b/plugins/OpenObserve/v1/dataStreams/scripts/search.js new file mode 100644 index 00000000..58648c76 --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/scripts/search.js @@ -0,0 +1,11 @@ +// dataStreams/scripts/search.js +// +// A row's "_timestamp" field (leading underscore) is invisible to +// metadata valueExpression lookups ($['_timestamp'] / $._timestamp both +// resolve to undefined even though the key is present in the raw row and +// declared in metadata) - so the microseconds -> milliseconds conversion +// for the date-shaped timestamp column has to happen here instead. +result = (data.hits || []).map((row) => ({ + ...row, + timestamp: row._timestamp / 1000, +})); diff --git a/plugins/OpenObserve/v1/dataStreams/scripts/streamFieldValues.js b/plugins/OpenObserve/v1/dataStreams/scripts/streamFieldValues.js new file mode 100644 index 00000000..4471a7ef --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/scripts/streamFieldValues.js @@ -0,0 +1,7 @@ +result = (data.hits || []).flatMap((h) => + (h.values || []).map((v) => ({ + field: h.field, + value: v.zo_sql_key, + count: v.zo_sql_num, + })), +); diff --git a/plugins/OpenObserve/v1/dataStreams/search.json b/plugins/OpenObserve/v1/dataStreams/search.json new file mode 100644 index 00000000..2b01cd73 --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/search.json @@ -0,0 +1,50 @@ +{ + "name": "search", + "displayName": "Search", + "description": "Log, metric, or trace records matching a SQL query against a stream", + "tags": ["Search"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "post", + "endpointPath": "_search", + "getArgs": [{ "key": "type", "value": "{{object.stream_type}}" }], + "postBody": { + "query": { + "sql": "{{sql}}", + "start_time": "{{timeframe.unixStart * 1000000}}", + "end_time": "{{timeframe.unixEnd * 1000000}}", + "from": 0, + "size": "{{size || 100}}" + }, + "search_type": "ui" + }, + "postRequestScript": "search.js" + }, + "matches": { "sourceType": { "type": "oneOf", "values": ["OpenObserve Stream"] } }, + "ui": [ + { + "type": "textarea", + "name": "sql", + "label": "SQL Query", + "help": "The stream name isn't auto-substituted — type it yourself in the FROM clause, e.g. SELECT * FROM \"default\" LIMIT 100", + "placeholder": "SELECT * FROM \"default\" LIMIT 100", + "required": true + }, + { + "type": "number", + "name": "size", + "label": "Max rows", + "defaultValue": 100 + } + ], + "metadata": [ + { + "name": "timestamp", + "displayName": "Timestamp", + "role": "timestamp", + "shape": "date" + }, + { "pattern": ".*" } + ], + "timeframes": true +} diff --git a/plugins/OpenObserve/v1/dataStreams/streamDetails.json b/plugins/OpenObserve/v1/dataStreams/streamDetails.json new file mode 100644 index 00000000..32d6eaff --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/streamDetails.json @@ -0,0 +1,48 @@ +{ + "name": "streamDetails", + "displayName": "Stream Details", + "description": "Streams registered in the organization, with stats, field schema, and settings", + "tags": ["Streams"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "streams", + "getArgs": [ + { "key": "type", "value": "{{type}}" }, + { "key": "fetchSchema", "value": "true" } + ], + "expandInnerObjects": true, + "pathToData": "list" + }, + "matches": "none", + "ui": [ + { + "type": "autocomplete", + "name": "type", + "label": "Stream type", + "defaultValue": "logs", + "allowCustomValues": false, + "isMulti": false, + "isClearable": false, + "data": { + "source": "fixed", + "values": [ + { "value": "logs", "label": "Logs" }, + { "value": "metrics", "label": "Metrics" }, + { "value": "traces", "label": "Traces" } + ] + } + } + ], + "metadata": [ + { "name": "name", "displayName": "Name", "shape": "string", "role": "label" }, + { "name": "stream_type", "displayName": "Stream Type", "shape": "string" }, + { "name": "total_fields", "displayName": "Total Fields", "shape": "number" }, + { "name": "stats.doc_num", "displayName": "Document Count", "shape": "number" }, + { "name": "stats.storage_size", "displayName": "Storage Size (MB)", "shape": ["megabytes", {}] }, + { "name": "stats.compressed_size", "displayName": "Compressed Size (MB)", "shape": ["megabytes", {}] }, + { "name": "schema", "displayName": "Schema", "shape": "json" }, + { "name": "settings", "displayName": "Settings", "shape": "json" } + ], + "timeframes": false +} diff --git a/plugins/OpenObserve/v1/dataStreams/streamFieldValues.json b/plugins/OpenObserve/v1/dataStreams/streamFieldValues.json new file mode 100644 index 00000000..e65b61be --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/streamFieldValues.json @@ -0,0 +1,53 @@ +{ + "name": "streamFieldValues", + "displayName": "Stream Field Values", + "description": "Top values for chosen fields in a stream", + "tags": ["Search"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "{{object.name}}/_values", + "getArgs": [ + { "key": "fields", "value": "{{fields}}" }, + { "key": "type", "value": "{{object.stream_type}}" }, + { "key": "start_time", "value": "{{timeframe.unixStart * 1000000}}" }, + { "key": "end_time", "value": "{{timeframe.unixEnd * 1000000}}" }, + { "key": "size", "value": "{{size || null}}" }, + { "key": "keyword", "value": "{{keyword || null}}" }, + { "key": "no_count", "value": "{{no_count ? 'true' : 'false'}}" } + ], + "postRequestScript": "streamFieldValues.js" + }, + "matches": { "sourceType": { "type": "oneOf", "values": ["OpenObserve Stream"] } }, + "ui": [ + { + "type": "text", + "name": "fields", + "label": "Fields", + "placeholder": "field1,field2", + "required": true + }, + { + "type": "number", + "name": "size", + "label": "Max values per field", + "defaultValue": 10 + }, + { + "type": "text", + "name": "keyword", + "label": "Filter keyword (optional)" + }, + { + "type": "checkbox", + "name": "no_count", + "label": "Order by value instead of count" + } + ], + "metadata": [ + { "name": "field", "displayName": "Field", "shape": "string" }, + { "name": "value", "displayName": "Value", "shape": "string", "role": "label" }, + { "name": "count", "displayName": "Count", "shape": "number" } + ], + "timeframes": true +} diff --git a/plugins/OpenObserve/v1/dataStreams/streams.json b/plugins/OpenObserve/v1/dataStreams/streams.json index 5a14656c..c6a139fe 100644 --- a/plugins/OpenObserve/v1/dataStreams/streams.json +++ b/plugins/OpenObserve/v1/dataStreams/streams.json @@ -1,19 +1,53 @@ { "name": "streams", "displayName": "Streams", - "description": "Streams registered in the organization, with stats, schema, and settings", + "description": "Streams registered in the organization, with stats and settings", "tags": ["Streams"], "baseDataSourceName": "httpRequestUnscoped", "config": { "httpMethod": "get", "endpointPath": "streams", "getArgs": [ - { "key": "type", "value": "logs" }, + { "key": "type", "value": "{{type}}" }, { "key": "fetchSchema", "value": "false" } ], + "expandInnerObjects": true, "pathToData": "list" }, "matches": "none", - "metadata": [{ "pattern": ".*" }], + "ui": [ + { + "type": "autocomplete", + "name": "type", + "label": "Stream type", + "defaultValue": "logs", + "allowCustomValues": false, + "isMulti": false, + "isClearable": false, + "data": { + "source": "fixed", + "values": [ + { "value": "logs", "label": "Logs" }, + { "value": "metrics", "label": "Metrics" }, + { "value": "traces", "label": "Traces" } + ] + } + } + ], + "metadata": [ + { "name": "name", "displayName": "Name", "shape": "string" }, + { "name": "stream_type", "displayName": "Stream Type", "shape": "string" }, + { "name": "stats.doc_num", "displayName": "Document Count", "shape": "number" }, + { "name": "stats.storage_size", "displayName": "Storage Size (MB)", "shape": ["megabytes", {}] }, + { "name": "stats.compressed_size", "displayName": "Compressed Size (MB)", "shape": ["megabytes", {}] }, + { + "name": "sourceId", + "displayName": "Source ID", + "computed": true, + "valueExpression": "{{ $['name'] + ':' + $['stream_type'] }}", + "visible": false + } + ], + "visibility": { "type": "hidden" }, "timeframes": false } diff --git a/plugins/OpenObserve/v1/defaultContent/manifest.json b/plugins/OpenObserve/v1/defaultContent/manifest.json new file mode 100644 index 00000000..af55a58d --- /dev/null +++ b/plugins/OpenObserve/v1/defaultContent/manifest.json @@ -0,0 +1,6 @@ +{ + "items": [ + { "name": "overviewDashboard", "type": "dashboard" }, + { "name": "streamDashboard", "type": "dashboard" } + ] +} diff --git a/plugins/OpenObserve/v1/defaultContent/overviewDashboard.dash.json b/plugins/OpenObserve/v1/defaultContent/overviewDashboard.dash.json new file mode 100644 index 00000000..bf35d310 --- /dev/null +++ b/plugins/OpenObserve/v1/defaultContent/overviewDashboard.dash.json @@ -0,0 +1,357 @@ +{ + "name": "Overview", + "schemaVersion": "1.5", + "timeframe": "last24hours", + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "df9a043f-60f3-4e2b-be8a-ec84e9b70465", + "x": 0, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Log Streams", + "description": "", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.streamDetails}}", + "name": "streamDetails", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { "type": "logs" }, + "group": { "by": [], "aggregate": [{ "type": "count" }] } + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count", + "comparisonColumn": "none", + "label": "Log Streams" + } + } + } + } + }, + { + "i": "a2fcdb1e-900e-4a54-a3e7-2c364a9f94b8", + "x": 1, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Metric Streams", + "description": "", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.streamDetails}}", + "name": "streamDetails", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { "type": "metrics" }, + "group": { "by": [], "aggregate": [{ "type": "count" }] } + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count", + "comparisonColumn": "none", + "label": "Metric Streams" + } + } + } + } + }, + { + "i": "587f8c6e-05a5-441d-9472-b67934682ebb", + "x": 2, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Trace Streams", + "description": "", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.streamDetails}}", + "name": "streamDetails", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { "type": "traces" }, + "group": { "by": [], "aggregate": [{ "type": "count" }] } + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count", + "comparisonColumn": "none", + "label": "Trace Streams" + } + } + } + } + }, + { + "i": "9a0da9ec-2a4a-468a-b1d9-c2533a1c2a92", + "x": 3, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Functions", + "description": "", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.functions}}", + "name": "functions", + "pluginConfigId": "{{configId}}", + "group": { "by": [], "aggregate": [{ "type": "count" }] } + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count", + "comparisonColumn": "none", + "label": "Functions" + } + } + } + } + }, + { + "i": "c3bca13c-1d96-418a-8f02-ec97f411ce53", + "x": 0, + "y": 2, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Log Documents", + "description": "Total documents across log streams", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.streamDetails}}", + "name": "streamDetails", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { "type": "logs" }, + "group": { "by": [], "aggregate": [{ "type": "sum", "names": ["stats.doc_num"] }] } + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "stats.doc_num_sum", + "comparisonColumn": "none", + "label": "Log Documents" + } + } + } + } + }, + { + "i": "fe78afdc-1df4-405f-af8e-6cdac9e1e372", + "x": 1, + "y": 2, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Log Storage (MB)", + "description": "Total storage size across log streams", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.streamDetails}}", + "name": "streamDetails", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { "type": "logs" }, + "group": { "by": [], "aggregate": [{ "type": "sum", "names": ["stats.storage_size"] }] } + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "stats.storage_size_sum", + "comparisonColumn": "none", + "label": "Log Storage (MB)" + } + } + } + } + }, + { + "i": "a8540d44-2802-4aa9-9ce1-d96670c9ee30", + "x": 2, + "y": 2, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Log Compressed Size (MB)", + "description": "Total compressed size across log streams", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.streamDetails}}", + "name": "streamDetails", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { "type": "logs" }, + "group": { "by": [], "aggregate": [{ "type": "sum", "names": ["stats.compressed_size"] }] } + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "stats.compressed_size_sum", + "comparisonColumn": "none", + "label": "Log Compressed Size (MB)" + } + } + } + } + }, + { + "i": "f824deed-8a13-457a-b268-ed0febe4b977", + "x": 3, + "y": 2, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Avg Fields per Log Stream", + "description": "", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.streamDetails}}", + "name": "streamDetails", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { "type": "logs" }, + "group": { "by": [], "aggregate": [{ "type": "mean", "names": ["total_fields"] }] } + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "total_fields_mean", + "comparisonColumn": "none", + "label": "Avg Fields per Log Stream" + } + } + } + } + }, + { + "i": "334ff0d1-3683-4324-8f9a-fcc0aef21172", + "x": 0, + "y": 4, + "w": 4, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Log Streams", + "description": "Registered log streams with document counts and storage size", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.streamDetails}}", + "name": "streamDetails", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { "type": "logs" }, + "sort": { "by": [["stats.doc_num", "desc"]] } + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "name", + "stream_type", + "total_fields", + "stats.doc_num", + "stats.storage_size", + "stats.compressed_size" + ], + "hiddenColumns": ["schema", "settings"] + } + } + } + } + }, + { + "i": "c8e574cb-5c34-4330-b081-688f62b85f2d", + "x": 0, + "y": 7, + "w": 4, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Functions", + "description": "VRL functions defined in the organization", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.functions}}", + "name": "functions", + "pluginConfigId": "{{configId}}", + "sort": { "by": [["stream_name", "asc"]] } + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": ["stream_name", "name", "num_args", "function"], + "hiddenColumns": ["order", "trans_type"] + } + } + } + } + } + ] + } +} diff --git a/plugins/OpenObserve/v1/defaultContent/streamDashboard.dash.json b/plugins/OpenObserve/v1/defaultContent/streamDashboard.dash.json new file mode 100644 index 00000000..c2120aa6 --- /dev/null +++ b/plugins/OpenObserve/v1/defaultContent/streamDashboard.dash.json @@ -0,0 +1,133 @@ +{ + "name": "Stream", + "schemaVersion": "1.5", + "timeframe": "last24hours", + "variables": ["{{variables.[Stream]}}"], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "3540ff26-7372-4b82-bd6b-2ceb99032fb0", + "x": 0, + "y": 0, + "w": 1, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Properties", + "description": "", + "timeframe": "none", + "dataStream": { + "id": "datastream-properties", + "name": "properties" + }, + "scope": { + "scope": "{{scopes.[Streams]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Stream]}}" + }, + "variables": ["{{variables.[Stream]}}"], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true, + "columnOrder": [ + "name", + "stream_type", + "docCount", + "storageSizeMb", + "compressedSizeMb" + ], + "hiddenColumns": ["id", "links", "type", "label", "sourceId", "sourceType"] + } + } + } + } + }, + { + "i": "123ab338-6bc3-4585-bc6c-1a0dd4411089", + "x": 1, + "y": 0, + "w": 3, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Sample Log Search", + "description": "Edit the SQL below — the stream name must be typed into the FROM clause yourself", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.search}}", + "name": "search", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "sql": "SELECT * FROM \"placeholder\" LIMIT 50", + "size": 50 + }, + "sort": { "by": [["timestamp", "desc"]] } + }, + "scope": { + "scope": "{{scopes.[Streams]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Stream]}}" + }, + "variables": ["{{variables.[Stream]}}"], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false + } + } + } + } + }, + { + "i": "192d3b53-6e6f-4f66-9574-0d6e37174ff6", + "x": 0, + "y": 3, + "w": 4, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Field Values", + "description": "Top values for the configured field(s) — edit the field list in the tile's parameters", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.streamFieldValues}}", + "name": "streamFieldValues", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { "fields": "_timestamp" }, + "sort": { "by": [["count", "desc"]] } + }, + "scope": { + "scope": "{{scopes.[Streams]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Stream]}}" + }, + "variables": ["{{variables.[Stream]}}"], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": ["field", "value", "count"] + } + } + } + } + } + ] + } +} diff --git a/plugins/OpenObserve/v1/docs/README.md b/plugins/OpenObserve/v1/docs/README.md index 60692196..fe7dade2 100644 --- a/plugins/OpenObserve/v1/docs/README.md +++ b/plugins/OpenObserve/v1/docs/README.md @@ -1,42 +1,59 @@ -Monitor your [OpenObserve](https://openobserve.ai) Cloud organization in SquaredUp — streams, log search, functions, and cluster health — via the [OpenObserve API](https://openobserve.ai/docs/reference/api/). +Monitor your self-hosted [OpenObserve](https://openobserve.ai) instance in SquaredUp — streams, log search, functions, and cluster health — via the [OpenObserve API](https://openobserve.ai/docs/reference/api/). -> ⚠️ This plugin covers **OpenObserve Cloud** only, and only the **Streams**, **Search**, **Functions**, and **Cluster** API areas — it does not cover self-hosted/on-prem OpenObserve, ingestion, dashboards, reports, or user management. +> ⚠️ This plugin covers self-hosted OpenObserve only, and only the **Streams**, **Search**, **Functions**, and **Cluster** API areas — it does not cover OpenObserve Cloud, ingestion, dashboards, reports, or user management. ## Setup -You will need your OpenObserve **organization identifier** and an **Authorization key** — the full HTTP Basic Auth value for your organization. +You will need your OpenObserve instance's **URL**, **port**, **organization identifier**, and an **API Key** — the full HTTP Basic Auth value for that organization. -1. Sign in to [OpenObserve Cloud](https://cloud.openobserve.ai) in the region your organization was created in. -2. Find your **organization identifier**: open **Data Sources** in the sidebar and copy it from the sample cURL command shown there — it's the path segment right after `/api/`. -3. Create a Service Account: go to **IAM → Service Accounts**, click **Add Service Account**, and give it a name. Copy the **token** shown immediately after creation — it is shown only once. -4. Build the **Authorization key**: base64-encode `:` — for a Service Account, the login identity is auto-generated as `.@sa.internal` (not the plain name you typed), e.g.: +1. Sign in to your OpenObserve instance. +2. Find your **organization identifier** — `default` unless you created additional organizations. +3. Create a Service Account (or use an existing user's credentials): go to **IAM → Service Accounts**, click **Add Service Account**, and give it a name. Copy the **token** shown immediately after creation — it is shown only once. +4. Build the **API Key**: base64-encode `:` (or `:` for a regular user), e.g.: ``` - echo -n "myaccount.myorgid@sa.internal:mytoken" | base64 + echo -n "myaccount@example.com:mytoken" | base64 ``` -5. Paste your region, organization identifier, and the resulting Authorization key into the fields below. The key is sent through exactly as entered — it is not re-encoded. +5. Paste your instance URL, port, organization identifier, and the resulting API Key into the fields below. The key is sent through exactly as entered — it is not re-encoded. ## Configuration fields | Field | What it is | Where to find it | Required | | ----- | ---------- | ----------------- | -------- | -| **Region** | Which OpenObserve Cloud region your organization lives in. | The URL you sign in at (`cloud.openobserve.ai`, `us2.openobserve.ai`, or `eu1.openobserve.ai`). | Yes | -| **Organization** | The organization identifier used in every API call. | OpenObserve Cloud → **Data Sources** → the sample cURL command's `/api//...` segment. | Yes | -| **Authorization key** | The full base64-encoded `username:password`/`username:token` pair, sent unmodified as `Authorization: Basic `. | Built from a Service Account's login identity + token (see Setup step 4). | Yes | +| **URL** | The base URL of your OpenObserve instance, including the scheme (e.g. `http://51.8.18.48`) — no trailing slash or port. | The address you use to sign in to OpenObserve. | Yes | +| **Port** | The port your OpenObserve instance listens on. | Defaults to `5080`, OpenObserve's standard port. | Yes | +| **Organization** | The organization identifier used in every API call. | Defaults to `default`; check **IAM → Organizations** if you use a different one. | Yes | +| **API Key** | The full base64-encoded `username:password`/`username:token` pair, sent unmodified as `Authorization: Basic `. | Built from a Service Account's login identity + token, or a user's email + password (see Setup step 4). | Yes | +| **Ignore certificate errors** | Skips TLS certificate validation. | Enable only if your instance uses a self-signed certificate. | No | -On save, the plugin validates these by fetching your organization's stream list; an invalid region, organization, or Authorization key fails setup with an authentication error. +On save, the plugin validates these by fetching your organization's stream list; an invalid URL, port, organization, or API Key fails setup with an authentication error. ## What this plugin monitors - +- **Streams** — logs, metrics, and trace streams registered in your organization, with document counts, storage size, schema, and settings. +- **Search** — ad hoc SQL queries against a stream's raw log, metric, or trace records, and the most common values for chosen fields. +- **Functions** — VRL data-transformation functions defined in the organization. +- **Cluster health** — pending compaction jobs per region/node, for clustered OpenObserve deployments. + +The out-of-the-box dashboards include an org-wide **Overview** plus a **Stream** perspective for drilling into an individual stream. ## Data streams - +- **Streams** — streams of a chosen type (logs/metrics/traces), with name, document count, and size; account-wide. Backs import and is hidden from the tile editor. +- **Stream Details** — streams of a chosen type (logs/metrics/traces), with document counts, storage size, schema, and settings; account-wide. +- **Functions** — VRL functions defined in the organization; account-wide. +- **Search** — records matching a user-supplied SQL query against one stream; scoped to a stream. +- **Stream Field Values** — the most common values for chosen fields in one stream; scoped to a stream. +- **Cluster Info** — pending compaction jobs per region/node; account-wide. Only returns data on a clustered OpenObserve deployment (see Known limitations). ## What gets indexed - +| Object type | API source | Represents | +| ----------- | ---------- | ---------- | +| **Stream** | `GET /api/{org}/streams` (called once per type: logs, metrics, traces) | A log, metric, or trace stream registered in the organization. | ## Known limitations - +- **Cluster Info requires a clustered deployment** — on a standalone single-node OpenObserve instance (the common self-hosted setup) this endpoint returns a 404; the plugin surfaces this as a clear message rather than raw error, and no default dashboard tile uses it. Clustered deployments can add their own tile. +- **Search requires the stream name typed manually** — the SQL's `FROM` clause isn't auto-filled with the scoped stream's name; the default "Sample Log Search" tile ships with placeholder SQL that must be edited to a real stream name. +- **Very broad search timeframes may exceed the platform's response size limit** — narrow the timeframe or reduce **Max rows** if a Search tile fails to load. +- **Read-only** — the plugin never creates, modifies, or deletes anything in OpenObserve. diff --git a/plugins/OpenObserve/v1/indexDefinitions/default.json b/plugins/OpenObserve/v1/indexDefinitions/default.json new file mode 100644 index 00000000..780815b9 --- /dev/null +++ b/plugins/OpenObserve/v1/indexDefinitions/default.json @@ -0,0 +1,52 @@ +{ + "steps": [ + { + "name": "logStreams", + "dataStream": { "name": "streams", "config": { "type": "logs" } }, + "timeframe": "none", + "objectMapping": { + "id": "sourceId", + "name": "name", + "type": { "value": "OpenObserve Stream" }, + "properties": [ + "stream_type", + { "docCount": "stats.doc_num" }, + { "storageSizeMb": "stats.storage_size" }, + { "compressedSizeMb": "stats.compressed_size" } + ] + } + }, + { + "name": "metricStreams", + "dataStream": { "name": "streams", "config": { "type": "metrics" } }, + "timeframe": "none", + "objectMapping": { + "id": "sourceId", + "name": "name", + "type": { "value": "OpenObserve Stream" }, + "properties": [ + "stream_type", + { "docCount": "stats.doc_num" }, + { "storageSizeMb": "stats.storage_size" }, + { "compressedSizeMb": "stats.compressed_size" } + ] + } + }, + { + "name": "traceStreams", + "dataStream": { "name": "streams", "config": { "type": "traces" } }, + "timeframe": "none", + "objectMapping": { + "id": "sourceId", + "name": "name", + "type": { "value": "OpenObserve Stream" }, + "properties": [ + "stream_type", + { "docCount": "stats.doc_num" }, + { "storageSizeMb": "stats.storage_size" }, + { "compressedSizeMb": "stats.compressed_size" } + ] + } + } + ] +} diff --git a/plugins/OpenObserve/v1/metadata.json b/plugins/OpenObserve/v1/metadata.json index 334fd576..85175faf 100644 --- a/plugins/OpenObserve/v1/metadata.json +++ b/plugins/OpenObserve/v1/metadata.json @@ -6,13 +6,13 @@ "name": "SquaredUp Labs", "type": "labs" }, - "description": "Monitor streams, run log search, and check functions and cluster health in your OpenObserve Cloud organization.", + "description": "Monitor streams, run log search, and check functions and cluster health in your self-hosted OpenObserve instance.", "category": "Monitoring", "type": "hybrid", "schemaVersion": "2.1", "importNotSupported": false, "restrictedToPlatforms": [], - "keywords": ["observability", "logs", "log search", "openobserve"], + "keywords": ["observability", "logs", "log search", "openobserve", "self-hosted"], "objectTypes": ["OpenObserve Stream"], "links": [ { @@ -30,12 +30,13 @@ "plugin": "WebAPI", "majorVersion": "1", "config": { - "baseUrl": "{{region}}/api/{{organization}}", + "baseUrl": "{{url}}:{{port}}/api/{{organization}}", "authMode": "none", "headers": [ - { "key": "Authorization", "value": "Basic {{key}}" } + { "key": "Authorization", "value": "Basic {{apiKey}}" } ], - "queryArgs": [] + "queryArgs": [], + "ignoreCertificateErrors": "{{ignoreCertificateErrors}}" } } } diff --git a/plugins/OpenObserve/v1/scopes.json b/plugins/OpenObserve/v1/scopes.json new file mode 100644 index 00000000..449f527b --- /dev/null +++ b/plugins/OpenObserve/v1/scopes.json @@ -0,0 +1,14 @@ +[ + { + "name": "Streams", + "matches": { + "sourceType": { "type": "oneOf", "values": ["OpenObserve Stream"] } + }, + "variable": { + "name": "Stream", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + } +] diff --git a/plugins/OpenObserve/v1/ui.json b/plugins/OpenObserve/v1/ui.json index a66b6789..39356ce8 100644 --- a/plugins/OpenObserve/v1/ui.json +++ b/plugins/OpenObserve/v1/ui.json @@ -1,50 +1,48 @@ [ { - "type": "autocomplete", - "name": "region", - "label": "Region", - "help": "The OpenObserve Cloud region your organization was created in — matches the URL you sign in at.", - "allowCustomValues": false, - "isMulti": false, - "isClearable": false, + "type": "url", + "name": "url", + "label": "URL", + "placeholder": "http://51.8.18.48", + "help": "The base URL of your self-hosted OpenObserve instance, **including the scheme** (`http://` or `https://`), without a trailing slash or port — the port is set separately below.", + "validation": { + "required": true + } + }, + { + "type": "number", + "name": "port", + "label": "Port", + "defaultValue": 5080, + "help": "The port your OpenObserve instance listens on.", "validation": { "required": true - }, - "data": { - "source": "fixed", - "values": [ - { - "value": "https://cloud.openobserve.ai", - "label": "US East (Ohio) — cloud.openobserve.ai" - }, - { - "value": "https://us2.openobserve.ai", - "label": "US West (California) — us2.openobserve.ai" - }, - { - "value": "https://eu1.openobserve.ai", - "label": "EU (Frankfurt) — eu1.openobserve.ai" - } - ] } }, { "type": "text", "name": "organization", "label": "Organization", + "defaultValue": "default", "placeholder": "default", - "help": "Your OpenObserve organization identifier. Find it in **Data Sources** in the OpenObserve sidebar — it's the segment of the sample cURL command right after `/api/`.", + "help": "Your OpenObserve organization identifier.", "validation": { "required": true } }, { "type": "password", - "name": "key", - "label": "Authorization key", - "help": "The full HTTP Basic Authorization value for your organization — sent through unmodified as `Authorization: Basic `. This is **not** a plain password or token; it's the already base64-encoded `username:password` (or `username:token`) pair. Paste it exactly as given to you — it is not re-encoded.", + "name": "apiKey", + "label": "API Key", + "help": "The full HTTP Basic Authorization value for your organization — sent through unmodified as `Authorization: Basic `. This is the already base64-encoded `username:password` (or `username:token`) pair, not a plain password or token. Paste it exactly as given to you — it is not re-encoded.", "validation": { "required": true } + }, + { + "type": "checkbox", + "name": "ignoreCertificateErrors", + "label": "Ignore certificate errors", + "help": "Enable when connecting to an instance with a self-signed certificate." } ] From b6653c88866bfe58dbba7017f839b49b5651be84 Mon Sep 17 00:00:00 2001 From: John Hayes Date: Fri, 16 Oct 2026 00:44:11 +0100 Subject: [PATCH 03/22] Add Metrics, Traces, Pipelines, and Incidents streams MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds 7 new data streams discovered via the instance's Swagger UI: metricsQuery (PromQL range queries), tracesLatest (recent spans in a trace stream), pipelines + pipelineHistory, incidents + incidentStats, and logsSearch (an unscoped sibling of the existing scoped search stream). Surfaces Pipelines/Incidents/Metrics/Logs tiles on the Overview dashboard. incidentStats returns 403 on this instance (likely edition/license gated) and serviceGraphTopology was dropped entirely after 404ing on every path tried — both handled the same way as the existing clusterInfo precedent, or removed outright. Co-Authored-By: Claude Sonnet 5 --- .../v1/dataStreams/incidentStats.json | 25 ++ .../OpenObserve/v1/dataStreams/incidents.json | 96 +++++ .../v1/dataStreams/logsSearch.json | 50 +++ .../v1/dataStreams/metricsQuery.json | 40 +++ .../v1/dataStreams/pipelineHistory.json | 59 ++++ .../OpenObserve/v1/dataStreams/pipelines.json | 26 ++ .../scripts/errorHandling/incidentStats.js | 5 + .../v1/dataStreams/scripts/incidentStats.js | 3 + .../v1/dataStreams/scripts/metricsQuery.js | 8 + .../v1/dataStreams/scripts/pipelines.js | 21 ++ .../v1/dataStreams/scripts/tracesLatest.js | 11 + .../v1/dataStreams/tracesLatest.json | 46 +++ .../overviewDashboard.dash.json | 328 ++++++++++++++++++ plugins/OpenObserve/v1/docs/README.md | 22 +- 14 files changed, 736 insertions(+), 4 deletions(-) create mode 100644 plugins/OpenObserve/v1/dataStreams/incidentStats.json create mode 100644 plugins/OpenObserve/v1/dataStreams/incidents.json create mode 100644 plugins/OpenObserve/v1/dataStreams/logsSearch.json create mode 100644 plugins/OpenObserve/v1/dataStreams/metricsQuery.json create mode 100644 plugins/OpenObserve/v1/dataStreams/pipelineHistory.json create mode 100644 plugins/OpenObserve/v1/dataStreams/pipelines.json create mode 100644 plugins/OpenObserve/v1/dataStreams/scripts/errorHandling/incidentStats.js create mode 100644 plugins/OpenObserve/v1/dataStreams/scripts/incidentStats.js create mode 100644 plugins/OpenObserve/v1/dataStreams/scripts/metricsQuery.js create mode 100644 plugins/OpenObserve/v1/dataStreams/scripts/pipelines.js create mode 100644 plugins/OpenObserve/v1/dataStreams/scripts/tracesLatest.js create mode 100644 plugins/OpenObserve/v1/dataStreams/tracesLatest.json diff --git a/plugins/OpenObserve/v1/dataStreams/incidentStats.json b/plugins/OpenObserve/v1/dataStreams/incidentStats.json new file mode 100644 index 00000000..cd0c81f0 --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/incidentStats.json @@ -0,0 +1,25 @@ +{ + "name": "incidentStats", + "displayName": "Incident Stats", + "description": "Summary statistics for alert incidents in the organization", + "tags": ["Incidents"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "../v2/{{dataSource.organization}}/alerts/incidents/stats", + "postRequestScript": "incidentStats.js", + "errorHandling": { "type": "script", "script": "errorHandling/incidentStats.js" } + }, + "matches": "none", + "metadata": [ + { "name": "total_incidents", "displayName": "Total Incidents", "shape": "number" }, + { "name": "open_incidents", "displayName": "Open Incidents", "shape": "number" }, + { "name": "acknowledged_incidents", "displayName": "Acknowledged Incidents", "shape": "number" }, + { "name": "resolved_incidents", "displayName": "Resolved Incidents", "shape": "number" }, + { "name": "alerts_per_incident_avg", "displayName": "Alerts Per Incident (avg)", "shape": ["number", { "decimalPlaces": 2 }] }, + { "name": "mttr_minutes", "displayName": "Mean Time to Resolve (min)", "shape": "number" }, + { "name": "by_severity", "displayName": "By Severity", "shape": "json" }, + { "name": "by_service", "displayName": "By Service", "shape": "json" } + ], + "timeframes": false +} diff --git a/plugins/OpenObserve/v1/dataStreams/incidents.json b/plugins/OpenObserve/v1/dataStreams/incidents.json new file mode 100644 index 00000000..787866c2 --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/incidents.json @@ -0,0 +1,96 @@ +{ + "name": "incidents", + "displayName": "Incidents", + "description": "Alert incidents in the organization, groups of correlated alerts", + "tags": ["Incidents"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "../v2/{{dataSource.organization}}/alerts/incidents", + "getArgs": [ + { "key": "status", "value": "{{status || null}}" }, + { "key": "limit", "value": "{{limit}}" }, + { "key": "offset", "value": "{{offset}}" } + ], + "pathToData": "incidents" + }, + "matches": "none", + "ui": [ + { + "type": "choiceChips", + "name": "status", + "label": "Status filter (optional)", + "isMulti": false, + "options": [ + { "value": "open", "label": "Open" }, + { "value": "acknowledged", "label": "Acknowledged" }, + { "value": "resolved", "label": "Resolved" } + ] + }, + { + "type": "number", + "name": "limit", + "label": "Limit", + "defaultValue": 50 + }, + { + "type": "number", + "name": "offset", + "label": "Offset", + "defaultValue": 0 + } + ], + "metadata": [ + { "name": "id", "displayName": "ID", "shape": "string", "role": "label" }, + { "name": "title", "displayName": "Title", "shape": "string" }, + { + "name": "status", + "displayName": "Status", + "shape": [ + "state", + { + "map": { + "error": ["open"], + "warning": ["acknowledged"], + "success": ["resolved"] + } + } + ] + }, + { "name": "severity", "displayName": "Severity", "shape": "string" }, + { "name": "alert_count", "displayName": "Alert Count", "shape": "number" }, + { "name": "assigned_to", "displayName": "Assigned To", "shape": "string" }, + { + "name": "created_at", + "displayName": "Created At", + "shape": "date", + "role": "timestamp", + "valueExpression": "{{ $['created_at'] / 1000 }}" + }, + { + "name": "updated_at", + "displayName": "Updated At", + "shape": "date", + "valueExpression": "{{ $['updated_at'] / 1000 }}" + }, + { + "name": "first_alert_at", + "displayName": "First Alert At", + "shape": "date", + "valueExpression": "{{ $['first_alert_at'] / 1000 }}" + }, + { + "name": "last_alert_at", + "displayName": "Last Alert At", + "shape": "date", + "valueExpression": "{{ $['last_alert_at'] / 1000 }}" + }, + { + "name": "resolved_at", + "displayName": "Resolved At", + "shape": "date", + "valueExpression": "{{ $['resolved_at'] ? $['resolved_at'] / 1000 : null }}" + } + ], + "timeframes": false +} diff --git a/plugins/OpenObserve/v1/dataStreams/logsSearch.json b/plugins/OpenObserve/v1/dataStreams/logsSearch.json new file mode 100644 index 00000000..c42e6de0 --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/logsSearch.json @@ -0,0 +1,50 @@ +{ + "name": "logsSearch", + "displayName": "Logs Search", + "description": "Log records matching a SQL query, across any log stream", + "tags": ["Search"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "post", + "endpointPath": "_search", + "getArgs": [{ "key": "type", "value": "logs" }], + "postBody": { + "query": { + "sql": "{{sql}}", + "start_time": "{{timeframe.unixStart * 1000000}}", + "end_time": "{{timeframe.unixEnd * 1000000}}", + "from": 0, + "size": "{{size || 100}}" + }, + "search_type": "ui" + }, + "postRequestScript": "search.js" + }, + "matches": "none", + "ui": [ + { + "type": "textarea", + "name": "sql", + "label": "SQL Query", + "help": "The stream name isn't auto-substituted — type it yourself in the FROM clause, e.g. SELECT * FROM \"default\" LIMIT 100. This stream searches logs only.", + "placeholder": "SELECT * FROM \"default\" LIMIT 100", + "required": true + }, + { + "type": "number", + "name": "size", + "label": "Max rows", + "defaultValue": 100 + } + ], + "metadata": [ + { + "name": "timestamp", + "displayName": "Timestamp", + "role": "timestamp", + "shape": "date" + }, + { "pattern": ".*" } + ], + "timeframes": true +} diff --git a/plugins/OpenObserve/v1/dataStreams/metricsQuery.json b/plugins/OpenObserve/v1/dataStreams/metricsQuery.json new file mode 100644 index 00000000..cdb0914a --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/metricsQuery.json @@ -0,0 +1,40 @@ +{ + "name": "metricsQuery", + "displayName": "Metrics Query", + "description": "Time series data matching a PromQL query", + "tags": ["Metrics"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "prometheus/api/v1/query_range", + "getArgs": [ + { "key": "query", "value": "{{promql}}" }, + { "key": "start", "value": "{{timeframe.unixStart}}" }, + { "key": "end", "value": "{{timeframe.unixEnd}}" }, + { "key": "step", "value": "{{step || '60s'}}" } + ], + "postRequestScript": "metricsQuery.js" + }, + "matches": "none", + "ui": [ + { + "type": "text", + "name": "promql", + "label": "PromQL Query", + "required": true, + "placeholder": "up or rate(http_requests_total[5m])" + }, + { + "type": "text", + "name": "step", + "label": "Step (e.g. 60s, 5m)", + "defaultValue": "60s" + } + ], + "metadata": [ + { "name": "labels", "displayName": "Labels", "shape": "string" }, + { "name": "timestamp", "displayName": "Timestamp", "shape": "date", "role": "timestamp" }, + { "name": "value", "displayName": "Value", "shape": "number" } + ], + "timeframes": true +} diff --git a/plugins/OpenObserve/v1/dataStreams/pipelineHistory.json b/plugins/OpenObserve/v1/dataStreams/pipelineHistory.json new file mode 100644 index 00000000..ec73c1a4 --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/pipelineHistory.json @@ -0,0 +1,59 @@ +{ + "name": "pipelineHistory", + "displayName": "Pipeline History", + "description": "Pipeline execution history within a time range", + "tags": ["Pipelines"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "pipelines/history", + "getArgs": [ + { "key": "pipeline_id", "value": "{{pipeline_id || null}}" }, + { "key": "start_time", "value": "{{timeframe.unixStart * 1000000}}" }, + { "key": "end_time", "value": "{{timeframe.unixEnd * 1000000}}" }, + { "key": "from", "value": "{{from || 0}}" }, + { "key": "size", "value": "{{size || 50}}" } + ], + "pathToData": "hits" + }, + "matches": "none", + "ui": [ + { + "type": "text", + "name": "pipeline_id", + "label": "Pipeline ID (optional)" + }, + { + "type": "number", + "name": "from", + "label": "From", + "defaultValue": 0 + }, + { + "type": "number", + "name": "size", + "label": "Size", + "defaultValue": 50 + } + ], + "metadata": [ + { "name": "pipeline_name", "displayName": "Pipeline Name", "shape": "string", "role": "label" }, + { "name": "status", "displayName": "Status", "shape": "string" }, + { + "name": "timestamp", + "displayName": "Timestamp", + "shape": "date", + "role": "timestamp", + "valueExpression": "{{ $['timestamp'] / 1000 }}" + }, + { "name": "is_realtime", "displayName": "Is Realtime", "shape": "boolean" }, + { "name": "is_silenced", "displayName": "Is Silenced", "shape": "boolean" }, + { "name": "retries", "displayName": "Retries", "shape": "number" }, + { "name": "delay_in_secs", "displayName": "Delay (s)", "shape": "number" }, + { "name": "error", "displayName": "Error", "shape": "string" }, + { "name": "evaluation_took_in_secs", "displayName": "Evaluation Took (s)", "shape": "number" }, + { "name": "query_took", "displayName": "Query Took", "shape": "number" }, + { "name": "source_node", "displayName": "Source Node", "shape": "string" } + ], + "timeframes": true +} diff --git a/plugins/OpenObserve/v1/dataStreams/pipelines.json b/plugins/OpenObserve/v1/dataStreams/pipelines.json new file mode 100644 index 00000000..83b4f9b4 --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/pipelines.json @@ -0,0 +1,26 @@ +{ + "name": "pipelines", + "displayName": "Pipelines", + "description": "Data transformation pipelines defined in the organization", + "tags": ["Pipelines"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "pipelines", + "postRequestScript": "pipelines.js" + }, + "matches": "none", + "metadata": [ + { "name": "pipeline_id", "displayName": "Pipeline ID", "shape": "string" }, + { "name": "name", "displayName": "Name", "shape": "string", "role": "label" }, + { "name": "description", "displayName": "Description", "shape": "string" }, + { "name": "enabled", "displayName": "Enabled", "shape": "boolean" }, + { "name": "version", "displayName": "Version", "shape": "number" }, + { "name": "paused_at", "displayName": "Paused At", "shape": "number" }, + { "name": "last_error", "displayName": "Last Error", "shape": "json" }, + { "name": "source", "displayName": "Source", "shape": "json" }, + { "name": "nodeCount", "displayName": "Node Count", "shape": "number" }, + { "name": "edgeCount", "displayName": "Edge Count", "shape": "number" } + ], + "timeframes": false +} diff --git a/plugins/OpenObserve/v1/dataStreams/scripts/errorHandling/incidentStats.js b/plugins/OpenObserve/v1/dataStreams/scripts/errorHandling/incidentStats.js new file mode 100644 index 00000000..17fe6e73 --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/scripts/errorHandling/incidentStats.js @@ -0,0 +1,5 @@ +result = + response.status === 403 + ? "Incident statistics are not available on this instance — this endpoint may require a different OpenObserve edition or license." + : (data && (data.message || data.error)) || + "Request failed with HTTP " + response.status; diff --git a/plugins/OpenObserve/v1/dataStreams/scripts/incidentStats.js b/plugins/OpenObserve/v1/dataStreams/scripts/incidentStats.js new file mode 100644 index 00000000..feb405f9 --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/scripts/incidentStats.js @@ -0,0 +1,3 @@ +// dataStreams/scripts/incidentStats.js +// The endpoint returns a single summary object, not a list — wrap it as one row. +result = [data]; diff --git a/plugins/OpenObserve/v1/dataStreams/scripts/metricsQuery.js b/plugins/OpenObserve/v1/dataStreams/scripts/metricsQuery.js new file mode 100644 index 00000000..07b780ae --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/scripts/metricsQuery.js @@ -0,0 +1,8 @@ +// dataStreams/scripts/metricsQuery.js +result = ((data.data && data.data.result) || []).flatMap((series) => + (series.values || []).map(([ts, val]) => ({ + labels: JSON.stringify(series.metric || {}), + timestamp: ts * 1000, + value: parseFloat(val), + })), +); diff --git a/plugins/OpenObserve/v1/dataStreams/scripts/pipelines.js b/plugins/OpenObserve/v1/dataStreams/scripts/pipelines.js new file mode 100644 index 00000000..2dd89c79 --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/scripts/pipelines.js @@ -0,0 +1,21 @@ +// dataStreams/scripts/pipelines.js +// Array-valued fields (nodes/edges) arrive at valueExpression already stringified +// ("[object Array]" or a JSON-text blob), so .length there counts characters, not +// elements. Compute real counts here, and stringify the nested json-shaped fields +// so they render as JSON rather than "[object Object]". +result = (data.list || []).map((p) => { + const nodes = p.nodes || []; + const edges = p.edges || []; + return { + pipeline_id: p.pipeline_id, + name: p.name, + description: p.description || "", + enabled: p.enabled, + version: p.version, + paused_at: p.paused_at != null ? p.paused_at : null, + last_error: p.last_error != null ? JSON.stringify(p.last_error) : null, + source: p.source != null ? JSON.stringify(p.source) : null, + nodeCount: nodes.length, + edgeCount: edges.length, + }; +}); diff --git a/plugins/OpenObserve/v1/dataStreams/scripts/tracesLatest.js b/plugins/OpenObserve/v1/dataStreams/scripts/tracesLatest.js new file mode 100644 index 00000000..33d2e5fa --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/scripts/tracesLatest.js @@ -0,0 +1,11 @@ +// dataStreams/scripts/tracesLatest.js +// +// Unlike the _search endpoint, /traces/latest rows have no "_timestamp" +// field at all - each hit is a trace summary with "start_time"/"end_time" +// in NANOSECONDS (not the microseconds used by the start_time/end_time +// query args). Derive a millisecond "timestamp" column from start_time +// here, since a declarative valueExpression can't do unit conversion. +result = (data.hits || []).map((row) => ({ + ...row, + timestamp: row.start_time / 1000000, +})); diff --git a/plugins/OpenObserve/v1/dataStreams/tracesLatest.json b/plugins/OpenObserve/v1/dataStreams/tracesLatest.json new file mode 100644 index 00000000..209b0a1f --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/tracesLatest.json @@ -0,0 +1,46 @@ +{ + "name": "tracesLatest", + "displayName": "Traces Latest", + "description": "Recent trace spans in a stream, within a time range", + "tags": ["Traces"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "{{object.name}}/traces/latest", + "getArgs": [ + { "key": "start_time", "value": "{{timeframe.unixStart * 1000000}}" }, + { "key": "end_time", "value": "{{timeframe.unixEnd * 1000000}}" }, + { "key": "size", "value": "{{size}}" }, + { "key": "from", "value": "0" }, + { "key": "filter", "value": "{{filter || null}}" } + ], + "postRequestScript": "tracesLatest.js" + }, + "matches": { + "sourceType": { "type": "oneOf", "values": ["OpenObserve Stream"] }, + "stream_type": { "type": "equals", "value": "traces" } + }, + "ui": [ + { + "type": "number", + "name": "size", + "label": "Max rows", + "defaultValue": 50 + }, + { + "type": "text", + "name": "filter", + "label": "Filter (optional)" + } + ], + "metadata": [ + { + "name": "timestamp", + "displayName": "Timestamp", + "role": "timestamp", + "shape": "date" + }, + { "pattern": ".*" } + ], + "timeframes": true +} diff --git a/plugins/OpenObserve/v1/defaultContent/overviewDashboard.dash.json b/plugins/OpenObserve/v1/defaultContent/overviewDashboard.dash.json index bf35d310..e9a222f6 100644 --- a/plugins/OpenObserve/v1/defaultContent/overviewDashboard.dash.json +++ b/plugins/OpenObserve/v1/defaultContent/overviewDashboard.dash.json @@ -351,6 +351,334 @@ } } } + }, + { + "i": "f63ae978-8728-4106-9817-d63e5581048f", + "x": 0, + "y": 10, + "w": 4, + "h": 1, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/text", + "title": "", + "description": "", + "visualisation": { + "config": { + "content": "Pipelines", + "fontSize": 16, + "align": "center", + "autoSize": true + } + } + } + }, + { + "i": "8ede297a-7aac-40b2-a5aa-c7d25586e795", + "x": 0, + "y": 11, + "w": 1, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Pipelines", + "description": "", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.pipelines}}", + "name": "pipelines", + "pluginConfigId": "{{configId}}", + "group": { "by": [], "aggregate": [{ "type": "count" }] } + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count", + "comparisonColumn": "none", + "label": "Pipelines" + } + } + } + } + }, + { + "i": "50d1a0d2-cceb-48b7-b9a5-833ae73d6ec7", + "x": 1, + "y": 11, + "w": 3, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Pipelines", + "description": "Data transformation pipelines defined in the organization", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.pipelines}}", + "name": "pipelines", + "pluginConfigId": "{{configId}}", + "sort": { "by": [["name", "asc"]] } + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": ["name", "enabled", "nodeCount", "edgeCount", "description"], + "hiddenColumns": ["pipeline_id", "paused_at", "last_error", "source", "version"] + } + } + } + } + }, + { + "i": "8fd4efc0-1be4-4763-abe4-6879939c8413", + "x": 0, + "y": 14, + "w": 4, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Pipeline History", + "description": "Pipeline execution history for all pipelines within the dashboard timeframe", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.pipelineHistory}}", + "name": "pipelineHistory", + "pluginConfigId": "{{configId}}", + "sort": { "by": [["timestamp", "desc"]] } + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": ["pipeline_name", "status", "timestamp", "retries", "delay_in_secs", "error"], + "hiddenColumns": ["is_realtime", "is_silenced", "evaluation_took_in_secs", "query_took", "source_node"] + } + } + } + } + }, + { + "i": "34d85dce-9c62-4909-8ca9-4b5a2c637278", + "x": 0, + "y": 17, + "w": 4, + "h": 1, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/text", + "title": "", + "description": "", + "visualisation": { + "config": { + "content": "Incidents", + "fontSize": 16, + "align": "center", + "autoSize": true + } + } + } + }, + { + "i": "72a32567-7302-46a9-b9b0-d7a95e161794", + "x": 0, + "y": 18, + "w": 1, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Incidents", + "description": "", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.incidents}}", + "name": "incidents", + "pluginConfigId": "{{configId}}", + "group": { "by": [], "aggregate": [{ "type": "count" }] } + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count", + "comparisonColumn": "none", + "label": "Incidents" + } + } + } + } + }, + { + "i": "fe1fde93-2545-4823-81a5-5040572ec1dc", + "x": 1, + "y": 18, + "w": 3, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Incidents", + "description": "Alert incidents in the organization, grouped from correlated alerts", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.incidents}}", + "name": "incidents", + "pluginConfigId": "{{configId}}", + "sort": { "by": [["created_at", "desc"]] } + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": ["id", "title", "status", "severity", "alert_count", "created_at"], + "hiddenColumns": ["assigned_to", "updated_at", "first_alert_at", "last_alert_at", "resolved_at"] + } + } + } + } + }, + { + "i": "4c7abdca-c1a4-4de4-9a7b-52e9e31dd1eb", + "x": 0, + "y": 21, + "w": 4, + "h": 1, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/text", + "title": "", + "description": "", + "visualisation": { + "config": { + "content": "Metrics", + "fontSize": 16, + "align": "center", + "autoSize": true + } + } + } + }, + { + "i": "8124babb-8bb4-4153-abbf-d661c3646fc2", + "x": 0, + "y": 22, + "w": 4, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Metric Count Over Time", + "description": "Example PromQL query against this instance's metric_count metric — edit the query in the tile's parameters", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.metricsQuery}}", + "name": "metricsQuery", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { "promql": "metric_count" }, + "sort": { "by": [["timestamp", "asc"]] } + }, + "visualisation": { + "type": "data-stream-line-graph", + "config": { + "data-stream-line-graph": { + "xAxisColumn": "timestamp", + "yAxisColumn": ["value"], + "seriesColumn": "labels", + "showLegend": true, + "legendPosition": "bottom", + "yAxisLabel": "Value", + "showYAxisLabel": true, + "showTrendLine": false + } + } + } + } + }, + { + "i": "0fd7e442-e0b7-422a-9c00-5f3ef0db5f45", + "x": 0, + "y": 25, + "w": 4, + "h": 1, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/text", + "title": "", + "description": "", + "visualisation": { + "config": { + "content": "Logs", + "fontSize": 16, + "align": "center", + "autoSize": true + } + } + } + }, + { + "i": "fb810b51-b82f-48b6-be86-6e44c20574fb", + "x": 0, + "y": 26, + "w": 4, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Sample Log Search", + "description": "Example SQL search against the \"default\" log stream — edit the SQL in the tile's parameters", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.logsSearch}}", + "name": "logsSearch", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "sql": "SELECT * FROM \"default\" LIMIT 50", + "size": 50 + }, + "sort": { "by": [["timestamp", "desc"]] } + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false + } + } + } + } } ] } diff --git a/plugins/OpenObserve/v1/docs/README.md b/plugins/OpenObserve/v1/docs/README.md index fe7dade2..4a03affb 100644 --- a/plugins/OpenObserve/v1/docs/README.md +++ b/plugins/OpenObserve/v1/docs/README.md @@ -1,6 +1,6 @@ -Monitor your self-hosted [OpenObserve](https://openobserve.ai) instance in SquaredUp — streams, log search, functions, and cluster health — via the [OpenObserve API](https://openobserve.ai/docs/reference/api/). +Monitor your self-hosted [OpenObserve](https://openobserve.ai) instance in SquaredUp — streams, log/metric/trace search, functions, pipelines, incidents, and cluster health — via the [OpenObserve API](https://openobserve.ai/docs/reference/api/). -> ⚠️ This plugin covers self-hosted OpenObserve only, and only the **Streams**, **Search**, **Functions**, and **Cluster** API areas — it does not cover OpenObserve Cloud, ingestion, dashboards, reports, or user management. +> ⚠️ This plugin covers self-hosted OpenObserve only, and only the **Streams**, **Search**, **Functions**, **Cluster**, **Metrics**, **Traces**, **Pipelines**, and **Incidents** API areas — it does not cover OpenObserve Cloud, ingestion, dashboards, reports, or user management. ## Setup @@ -31,7 +31,11 @@ On save, the plugin validates these by fetching your organization's stream list; - **Streams** — logs, metrics, and trace streams registered in your organization, with document counts, storage size, schema, and settings. - **Search** — ad hoc SQL queries against a stream's raw log, metric, or trace records, and the most common values for chosen fields. +- **Metrics** — PromQL range queries against metrics ingested into OpenObserve. +- **Traces** — recent trace spans within a time range, for an individual trace stream. - **Functions** — VRL data-transformation functions defined in the organization. +- **Pipelines** — data-transformation pipelines and their execution history. +- **Incidents** — alert incidents (groups of correlated alerts) and summary statistics. - **Cluster health** — pending compaction jobs per region/node, for clustered OpenObserve deployments. The out-of-the-box dashboards include an org-wide **Overview** plus a **Stream** perspective for drilling into an individual stream. @@ -42,7 +46,14 @@ The out-of-the-box dashboards include an org-wide **Overview** plus a **Stream** - **Stream Details** — streams of a chosen type (logs/metrics/traces), with document counts, storage size, schema, and settings; account-wide. - **Functions** — VRL functions defined in the organization; account-wide. - **Search** — records matching a user-supplied SQL query against one stream; scoped to a stream. +- **Logs Search** — records matching a user-supplied SQL query against any log stream, without picking a stream first; account-wide. - **Stream Field Values** — the most common values for chosen fields in one stream; scoped to a stream. +- **Traces Latest** — recent trace spans within a time range; scoped to a trace stream. +- **Metrics Query** — time series data matching a PromQL query; account-wide. +- **Pipelines** — data-transformation pipelines defined in the organization, with node/edge counts and last error; account-wide. +- **Pipeline History** — pipeline execution history within a time range; account-wide. +- **Incidents** — alert incidents, with status, severity, and timestamps; account-wide. +- **Incident Stats** — summary statistics for alert incidents; account-wide. May not be available on all self-hosted deployments (see Known limitations). - **Cluster Info** — pending compaction jobs per region/node; account-wide. Only returns data on a clustered OpenObserve deployment (see Known limitations). ## What gets indexed @@ -54,6 +65,9 @@ The out-of-the-box dashboards include an org-wide **Overview** plus a **Stream** ## Known limitations - **Cluster Info requires a clustered deployment** — on a standalone single-node OpenObserve instance (the common self-hosted setup) this endpoint returns a 404; the plugin surfaces this as a clear message rather than raw error, and no default dashboard tile uses it. Clustered deployments can add their own tile. -- **Search requires the stream name typed manually** — the SQL's `FROM` clause isn't auto-filled with the scoped stream's name; the default "Sample Log Search" tile ships with placeholder SQL that must be edited to a real stream name. -- **Very broad search timeframes may exceed the platform's response size limit** — narrow the timeframe or reduce **Max rows** if a Search tile fails to load. +- **Incident Stats may not be available on your deployment** — this endpoint returned "Not Supported" on the instance this plugin was tested against, likely an edition/license gate; the plugin surfaces this as a clear message, and no default dashboard tile uses it. +- **Search and Logs Search require the stream name typed manually** — the SQL's `FROM` clause isn't auto-filled; default tiles ship with placeholder/example SQL that may need editing to match your streams. +- **Very broad search timeframes may exceed the platform's response size limit** — narrow the timeframe or reduce **Max rows** if a Search, Logs Search, or Traces Latest tile fails to load. +- **Pipeline History's time-range unit is unverified** — this plugin sends microseconds, matching every other timed endpoint in this plugin, but it couldn't be confirmed against real pipeline-run data; if the tile returns no rows despite a pipeline having run recently, this is worth rechecking. +- **Traces Latest only applies to trace-type streams** — it isn't included on the Stream perspective dashboard (which covers all three stream types); add it manually on a dashboard scoped to a trace stream. - **Read-only** — the plugin never creates, modifies, or deletes anything in OpenObserve. From 2ee48d9082fca2c7bf4d272d52416dba8c2592a0 Mon Sep 17 00:00:00 2001 From: John Hayes Date: Fri, 16 Oct 2026 15:36:10 +0100 Subject: [PATCH 04/22] Improve tracesLatest: span count, date timestamps, trace link Drops the raw spans array in favour of a spanCount column, converts start/end time to dates, adds a durationMs column (duration is confirmed microseconds, verified against raw start/end deltas), and makes trace_id a link into OpenObserve's web UI for that trace. The web UI link format is unverified against the live product (no working Chrome extension connection this session) - documented as a known limitation. Co-Authored-By: Claude Sonnet 5 --- .../v1/dataStreams/scripts/tracesLatest.js | 29 +++++++++----- .../v1/dataStreams/tracesLatest.json | 39 +++++++++++++++++-- plugins/OpenObserve/v1/docs/README.md | 3 +- 3 files changed, 57 insertions(+), 14 deletions(-) diff --git a/plugins/OpenObserve/v1/dataStreams/scripts/tracesLatest.js b/plugins/OpenObserve/v1/dataStreams/scripts/tracesLatest.js index 33d2e5fa..68415c01 100644 --- a/plugins/OpenObserve/v1/dataStreams/scripts/tracesLatest.js +++ b/plugins/OpenObserve/v1/dataStreams/scripts/tracesLatest.js @@ -1,11 +1,22 @@ // dataStreams/scripts/tracesLatest.js // -// Unlike the _search endpoint, /traces/latest rows have no "_timestamp" -// field at all - each hit is a trace summary with "start_time"/"end_time" -// in NANOSECONDS (not the microseconds used by the start_time/end_time -// query args). Derive a millisecond "timestamp" column from start_time -// here, since a declarative valueExpression can't do unit conversion. -result = (data.hits || []).map((row) => ({ - ...row, - timestamp: row.start_time / 1000000, -})); +// "spans" is a 2-element array ([spanCount, ...]), not a list of individual +// spans - array fields can't be read via valueExpression even just to count +// them (see project memory), so pull the count out here. The trace-detail +// link also has to be built here: "dataSource.*" is only resolvable inside +// config strings/scripts, not inside a metadata valueExpression (confirmed +// empirically - it throws "dataSource is not defined" there). +const ds = (context.dataSources && context.dataSources[0]) || {}; +const webBase = [ds.url, ds.port].filter(Boolean).join(":"); +const streamName = (context.objects && context.objects[0] && context.objects[0].name) || ""; + +result = (data.hits || []).map((row) => { + const { spans, ...rest } = row; + const fromUs = Math.floor(row.start_time / 1000); + const toUs = Math.floor(row.end_time / 1000); + return { + ...rest, + spanCount: (spans && spans[0]) || 0, + traceUrl: `${webBase}/web/traces/trace-details?org_identifier=${ds.organization}&stream=${streamName}&trace_id=${row.trace_id}&from=${fromUs}&to=${toUs}`, + }; +}); diff --git a/plugins/OpenObserve/v1/dataStreams/tracesLatest.json b/plugins/OpenObserve/v1/dataStreams/tracesLatest.json index 209b0a1f..a5299cc2 100644 --- a/plugins/OpenObserve/v1/dataStreams/tracesLatest.json +++ b/plugins/OpenObserve/v1/dataStreams/tracesLatest.json @@ -1,7 +1,7 @@ { "name": "tracesLatest", "displayName": "Traces Latest", - "description": "Recent trace spans in a stream, within a time range", + "description": "Recent traces in a stream, within a time range", "tags": ["Traces"], "baseDataSourceName": "httpRequestScopedSingle", "config": { @@ -34,11 +34,42 @@ } ], "metadata": [ + { "name": "start_time", "visible": false }, + { "name": "end_time", "visible": false }, + { "name": "duration", "visible": false }, + { "name": "traceUrl", "visible": false }, { - "name": "timestamp", - "displayName": "Timestamp", + "name": "startTime", + "displayName": "Start Time", + "computed": true, "role": "timestamp", - "shape": "date" + "shape": "date", + "valueExpression": "{{ $['start_time'] / 1000000 }}" + }, + { + "name": "endTime", + "displayName": "End Time", + "computed": true, + "shape": "date", + "valueExpression": "{{ $['end_time'] / 1000000 }}" + }, + { + "name": "durationMs", + "displayName": "Duration (ms)", + "computed": true, + "shape": "milliseconds", + "valueExpression": "{{ $['duration'] / 1000 }}" + }, + { + "name": "spanCount", + "displayName": "Span Count", + "shape": "number" + }, + { + "name": "trace_id", + "displayName": "Trace ID", + "valueExpression": "{{ $['traceUrl'] }}", + "shape": ["url", { "label": "View Trace" }] }, { "pattern": ".*" } ], diff --git a/plugins/OpenObserve/v1/docs/README.md b/plugins/OpenObserve/v1/docs/README.md index 4a03affb..97d6e3fe 100644 --- a/plugins/OpenObserve/v1/docs/README.md +++ b/plugins/OpenObserve/v1/docs/README.md @@ -48,7 +48,7 @@ The out-of-the-box dashboards include an org-wide **Overview** plus a **Stream** - **Search** — records matching a user-supplied SQL query against one stream; scoped to a stream. - **Logs Search** — records matching a user-supplied SQL query against any log stream, without picking a stream first; account-wide. - **Stream Field Values** — the most common values for chosen fields in one stream; scoped to a stream. -- **Traces Latest** — recent trace spans within a time range; scoped to a trace stream. +- **Traces Latest** — recent traces within a time range, with a span count and a link to view each trace in OpenObserve; scoped to a trace stream. - **Metrics Query** — time series data matching a PromQL query; account-wide. - **Pipelines** — data-transformation pipelines defined in the organization, with node/edge counts and last error; account-wide. - **Pipeline History** — pipeline execution history within a time range; account-wide. @@ -70,4 +70,5 @@ The out-of-the-box dashboards include an org-wide **Overview** plus a **Stream** - **Very broad search timeframes may exceed the platform's response size limit** — narrow the timeframe or reduce **Max rows** if a Search, Logs Search, or Traces Latest tile fails to load. - **Pipeline History's time-range unit is unverified** — this plugin sends microseconds, matching every other timed endpoint in this plugin, but it couldn't be confirmed against real pipeline-run data; if the tile returns no rows despite a pipeline having run recently, this is worth rechecking. - **Traces Latest only applies to trace-type streams** — it isn't included on the Stream perspective dashboard (which covers all three stream types); add it manually on a dashboard scoped to a trace stream. +- **Traces Latest's "View Trace" link format is unverified** — it's built from OpenObserve's documented `/web/traces/trace-details` route (`org_identifier`, `stream`, `trace_id`, `from`, `to` query params, with `from`/`to` in microseconds), but this plugin was built without being able to click through and confirm the link actually opens the right trace. If it doesn't resolve correctly, this is the first thing to check. - **Read-only** — the plugin never creates, modifies, or deletes anything in OpenObserve. From 5380153d1253fd0d6563f39a94674ed8071c3edf Mon Sep 17 00:00:00 2001 From: John Hayes Date: Fri, 16 Oct 2026 15:51:02 +0100 Subject: [PATCH 05/22] Collapse per-service/model arrays in tracesLatest service_name and models were still exploding into numbered per-index columns (service_name.0.*, models.0) via the metadata catch-all, same problem as the spans array fixed previously. Summarize both instead: serviceCount + a comma-joined services list, and a comma-joined models string. Co-Authored-By: Claude Sonnet 5 --- .../v1/dataStreams/scripts/tracesLatest.js | 14 +++++++++++--- .../OpenObserve/v1/dataStreams/tracesLatest.json | 15 +++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/plugins/OpenObserve/v1/dataStreams/scripts/tracesLatest.js b/plugins/OpenObserve/v1/dataStreams/scripts/tracesLatest.js index 68415c01..34334997 100644 --- a/plugins/OpenObserve/v1/dataStreams/scripts/tracesLatest.js +++ b/plugins/OpenObserve/v1/dataStreams/scripts/tracesLatest.js @@ -1,8 +1,13 @@ // dataStreams/scripts/tracesLatest.js // // "spans" is a 2-element array ([spanCount, ...]), not a list of individual -// spans - array fields can't be read via valueExpression even just to count -// them (see project memory), so pull the count out here. The trace-detail +// spans. "service_name" is an array of per-service summaries, and "models" +// an array of model names - left as-is, these would each explode into +// numbered per-index columns (service_name.0.*, service_name.1.*, ...) via +// the metadata catch-all, which reads like per-span detail even though it +// isn't. Summarize all three here instead of exploding them (array fields +// also can't be read via valueExpression, even just to count/join them - +// see project memory - so this has to happen in script). The trace-detail // link also has to be built here: "dataSource.*" is only resolvable inside // config strings/scripts, not inside a metadata valueExpression (confirmed // empirically - it throws "dataSource is not defined" there). @@ -11,12 +16,15 @@ const webBase = [ds.url, ds.port].filter(Boolean).join(":"); const streamName = (context.objects && context.objects[0] && context.objects[0].name) || ""; result = (data.hits || []).map((row) => { - const { spans, ...rest } = row; + const { spans, service_name, models, ...rest } = row; const fromUs = Math.floor(row.start_time / 1000); const toUs = Math.floor(row.end_time / 1000); return { ...rest, spanCount: (spans && spans[0]) || 0, + serviceCount: (service_name || []).length, + services: (service_name || []).map((s) => s.service_name).join(", "), + models: (models || []).join(", "), traceUrl: `${webBase}/web/traces/trace-details?org_identifier=${ds.organization}&stream=${streamName}&trace_id=${row.trace_id}&from=${fromUs}&to=${toUs}`, }; }); diff --git a/plugins/OpenObserve/v1/dataStreams/tracesLatest.json b/plugins/OpenObserve/v1/dataStreams/tracesLatest.json index a5299cc2..103ff3d9 100644 --- a/plugins/OpenObserve/v1/dataStreams/tracesLatest.json +++ b/plugins/OpenObserve/v1/dataStreams/tracesLatest.json @@ -65,6 +65,21 @@ "displayName": "Span Count", "shape": "number" }, + { + "name": "serviceCount", + "displayName": "Service Count", + "shape": "number" + }, + { + "name": "services", + "displayName": "Services", + "shape": "string" + }, + { + "name": "models", + "displayName": "Models", + "shape": "string" + }, { "name": "trace_id", "displayName": "Trace ID", From 80c98eb3857989a172447aa90bc3fd271be5ad82 Mon Sep 17 00:00:00 2001 From: John Hayes Date: Tue, 20 Oct 2026 11:59:09 +0100 Subject: [PATCH 06/22] Fix required validation on four SQL/PromQL query fields search.sql, logsSearch.sql, streamFieldValues.fields, and metricsQuery.promql all had "required": true as a bare top-level property instead of nested under "validation", so it was silently ignored. This let a tile save with an empty query, which OpenObserve's Prometheus endpoint correctly rejected with "no expression found in input" rather than the platform blocking it at config time. Co-Authored-By: Claude Sonnet 5 --- plugins/OpenObserve/v1/dataStreams/logsSearch.json | 2 +- plugins/OpenObserve/v1/dataStreams/metricsQuery.json | 4 ++-- plugins/OpenObserve/v1/dataStreams/search.json | 2 +- plugins/OpenObserve/v1/dataStreams/streamFieldValues.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/OpenObserve/v1/dataStreams/logsSearch.json b/plugins/OpenObserve/v1/dataStreams/logsSearch.json index c42e6de0..c7b437b9 100644 --- a/plugins/OpenObserve/v1/dataStreams/logsSearch.json +++ b/plugins/OpenObserve/v1/dataStreams/logsSearch.json @@ -28,7 +28,7 @@ "label": "SQL Query", "help": "The stream name isn't auto-substituted — type it yourself in the FROM clause, e.g. SELECT * FROM \"default\" LIMIT 100. This stream searches logs only.", "placeholder": "SELECT * FROM \"default\" LIMIT 100", - "required": true + "validation": { "required": true } }, { "type": "number", diff --git a/plugins/OpenObserve/v1/dataStreams/metricsQuery.json b/plugins/OpenObserve/v1/dataStreams/metricsQuery.json index cdb0914a..beeafd2e 100644 --- a/plugins/OpenObserve/v1/dataStreams/metricsQuery.json +++ b/plugins/OpenObserve/v1/dataStreams/metricsQuery.json @@ -21,8 +21,8 @@ "type": "text", "name": "promql", "label": "PromQL Query", - "required": true, - "placeholder": "up or rate(http_requests_total[5m])" + "placeholder": "up or rate(http_requests_total[5m])", + "validation": { "required": true } }, { "type": "text", diff --git a/plugins/OpenObserve/v1/dataStreams/search.json b/plugins/OpenObserve/v1/dataStreams/search.json index 2b01cd73..484bf0d7 100644 --- a/plugins/OpenObserve/v1/dataStreams/search.json +++ b/plugins/OpenObserve/v1/dataStreams/search.json @@ -28,7 +28,7 @@ "label": "SQL Query", "help": "The stream name isn't auto-substituted — type it yourself in the FROM clause, e.g. SELECT * FROM \"default\" LIMIT 100", "placeholder": "SELECT * FROM \"default\" LIMIT 100", - "required": true + "validation": { "required": true } }, { "type": "number", diff --git a/plugins/OpenObserve/v1/dataStreams/streamFieldValues.json b/plugins/OpenObserve/v1/dataStreams/streamFieldValues.json index e65b61be..1df34f4f 100644 --- a/plugins/OpenObserve/v1/dataStreams/streamFieldValues.json +++ b/plugins/OpenObserve/v1/dataStreams/streamFieldValues.json @@ -25,7 +25,7 @@ "name": "fields", "label": "Fields", "placeholder": "field1,field2", - "required": true + "validation": { "required": true } }, { "type": "number", From 01014c80ee2544fd685f1c40b0d129a72e651856 Mon Sep 17 00:00:00 2001 From: John Hayes Date: Tue, 20 Oct 2026 13:22:12 +0100 Subject: [PATCH 07/22] Add guided Simple Metrics Query picker (metric/label/value cascade) Adds simpleMetricsQuery plus three hidden backing streams (metricNames, metricLabels, labelValues) implementing cascading autocomplete dropdowns - pick a metric, then a label scoped to it, then values scoped to both - with an optional aggregation wrapping the resulting PromQL query. Found and fixed two platform quirks along the way: a primitive-array pathToData row auto-names its column "value" (not "result"), which collided with a same-named declared column; and single-select autocomplete fields resolve as plain strings inside {{ }} getArgs expressions, not the {value} object shape scripts see. Co-Authored-By: Claude Sonnet 5 --- .../v1/dataStreams/labelValues.json | 29 ++++ .../v1/dataStreams/metricLabels.json | 26 +++ .../v1/dataStreams/metricNames.json | 24 +++ .../v1/dataStreams/scripts/labelValues.js | 6 + .../v1/dataStreams/scripts/metricLabels.js | 6 + .../v1/dataStreams/simpleMetricsQuery.json | 150 ++++++++++++++++++ plugins/OpenObserve/v1/docs/README.md | 6 +- 7 files changed, 245 insertions(+), 2 deletions(-) create mode 100644 plugins/OpenObserve/v1/dataStreams/labelValues.json create mode 100644 plugins/OpenObserve/v1/dataStreams/metricLabels.json create mode 100644 plugins/OpenObserve/v1/dataStreams/metricNames.json create mode 100644 plugins/OpenObserve/v1/dataStreams/scripts/labelValues.js create mode 100644 plugins/OpenObserve/v1/dataStreams/scripts/metricLabels.js create mode 100644 plugins/OpenObserve/v1/dataStreams/simpleMetricsQuery.json diff --git a/plugins/OpenObserve/v1/dataStreams/labelValues.json b/plugins/OpenObserve/v1/dataStreams/labelValues.json new file mode 100644 index 00000000..74baae52 --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/labelValues.json @@ -0,0 +1,29 @@ +{ + "name": "labelValues", + "displayName": "Label Values", + "description": "Values present for a label on a metric", + "tags": ["Metrics"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "prometheus/api/v1/label/{{labelName}}/values", + "getArgs": [ + { "key": "match[]", "value": "{{metric}}" }, + { "key": "start", "value": "{{timeframe.unixStart}}" }, + { "key": "end", "value": "{{timeframe.unixEnd}}" } + ], + "postRequestScript": "labelValues.js" + }, + "matches": "none", + "visibility": { "type": "hidden" }, + "ui": [ + { "type": "text", "name": "metric", "label": "Metric" }, + { "type": "text", "name": "labelName", "label": "Label" } + ], + "metadata": [ + { "name": "result", "visible": false }, + { "name": "label", "computed": true, "valueExpression": "{{ $['result'] }}", "role": "label" }, + { "name": "value", "computed": true, "valueExpression": "{{ $['result'] }}", "role": "value" } + ], + "timeframes": true +} diff --git a/plugins/OpenObserve/v1/dataStreams/metricLabels.json b/plugins/OpenObserve/v1/dataStreams/metricLabels.json new file mode 100644 index 00000000..c00a0e82 --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/metricLabels.json @@ -0,0 +1,26 @@ +{ + "name": "metricLabels", + "displayName": "Metric Labels", + "description": "Label names present on a metric", + "tags": ["Metrics"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "prometheus/api/v1/labels", + "getArgs": [ + { "key": "match[]", "value": "{{metric}}" }, + { "key": "start", "value": "{{timeframe.unixStart}}" }, + { "key": "end", "value": "{{timeframe.unixEnd}}" } + ], + "postRequestScript": "metricLabels.js" + }, + "matches": "none", + "visibility": { "type": "hidden" }, + "ui": [{ "type": "text", "name": "metric", "label": "Metric" }], + "metadata": [ + { "name": "result", "visible": false }, + { "name": "label", "computed": true, "valueExpression": "{{ $['result'] }}", "role": "label" }, + { "name": "value", "computed": true, "valueExpression": "{{ $['result'] }}", "role": "value" } + ], + "timeframes": true +} diff --git a/plugins/OpenObserve/v1/dataStreams/metricNames.json b/plugins/OpenObserve/v1/dataStreams/metricNames.json new file mode 100644 index 00000000..04bed48d --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/metricNames.json @@ -0,0 +1,24 @@ +{ + "name": "metricNames", + "displayName": "Metric Names", + "description": "Metric names available for querying", + "tags": ["Metrics"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "streams", + "getArgs": [ + { "key": "type", "value": "metrics" }, + { "key": "fetchSchema", "value": "false" } + ], + "pathToData": "list" + }, + "matches": "none", + "visibility": { "type": "hidden" }, + "metadata": [ + { "name": "name", "visible": false }, + { "name": "label", "computed": true, "valueExpression": "{{ $['name'] }}", "role": "label" }, + { "name": "value", "computed": true, "valueExpression": "{{ $['name'] }}", "role": "value" } + ], + "timeframes": false +} diff --git a/plugins/OpenObserve/v1/dataStreams/scripts/labelValues.js b/plugins/OpenObserve/v1/dataStreams/scripts/labelValues.js new file mode 100644 index 00000000..be07bc2c --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/scripts/labelValues.js @@ -0,0 +1,6 @@ +// dataStreams/scripts/labelValues.js +// A primitive-array pathToData row auto-names its single column "value", +// which collides with the declared computed "value" column below (Cannot +// append columns, duplicate column names: value,value). Wrap in script +// instead so the raw field is under our own "result" key. +result = (data.data || []).map((v) => ({ result: v })); diff --git a/plugins/OpenObserve/v1/dataStreams/scripts/metricLabels.js b/plugins/OpenObserve/v1/dataStreams/scripts/metricLabels.js new file mode 100644 index 00000000..8706d05f --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/scripts/metricLabels.js @@ -0,0 +1,6 @@ +// dataStreams/scripts/metricLabels.js +// Exclude "__name__" - that's the metric-name selector itself, not a real +// dimension worth offering in the "Filter by label" picker. +result = (data.data || []) + .filter((l) => l !== "__name__") + .map((l) => ({ result: l })); diff --git a/plugins/OpenObserve/v1/dataStreams/simpleMetricsQuery.json b/plugins/OpenObserve/v1/dataStreams/simpleMetricsQuery.json new file mode 100644 index 00000000..8a678f2d --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/simpleMetricsQuery.json @@ -0,0 +1,150 @@ +{ + "name": "simpleMetricsQuery", + "displayName": "Simple Metrics Query", + "description": "Time series data for a metric, built from a guided metric/label/value picker", + "tags": [ + "Metrics" + ], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "prometheus/api/v1/query_range", + "getArgs": [ + { + "key": "query", + "value": "{{ (aggregation && aggregation !== 'none' ? aggregation + '(' : '') + metric + (label && values && values.length ? '{' + label + '=~\"' + values.join('|') + '\"}' : '') + (aggregation && aggregation !== 'none' ? ')' : '') }}" + }, + { + "key": "start", + "value": "{{timeframe.unixStart}}" + }, + { + "key": "end", + "value": "{{timeframe.unixEnd}}" + }, + { + "key": "step", + "value": "{{step || '60s'}}" + } + ], + "postRequestScript": "metricsQuery.js" + }, + "matches": "none", + "ui": [ + { + "type": "autocomplete", + "name": "metric", + "label": "Metric", + "allowCustomValues": false, + "isMulti": false, + "validation": { + "required": true + }, + "data": { + "source": "dataStream", + "dataStreamName": "metricNames", + "dataSourceConfig": {} + } + }, + { + "type": "autocomplete", + "name": "label", + "label": "Filter by label", + "allowCustomValues": false, + "isMulti": false, + "data": { + "source": "dataStream", + "dataStreamName": "metricLabels", + "dataSourceConfig": { + "metric": { + "fieldName": "metric", + "required": true + } + } + } + }, + { + "type": "autocomplete", + "name": "values", + "label": "Values", + "allowCustomValues": false, + "isMulti": true, + "data": { + "source": "dataStream", + "dataStreamName": "labelValues", + "dataSourceConfig": { + "metric": { + "fieldName": "metric", + "required": true + }, + "labelName": { + "fieldName": "label", + "required": true + } + } + } + }, + { + "type": "autocomplete", + "name": "aggregation", + "label": "Aggregation", + "allowCustomValues": false, + "isMulti": false, + "defaultValue": "none", + "data": { + "source": "fixed", + "values": [ + { + "value": "none", + "label": "None" + }, + { + "value": "sum", + "label": "Sum" + }, + { + "value": "avg", + "label": "Average" + }, + { + "value": "min", + "label": "Minimum" + }, + { + "value": "max", + "label": "Maximum" + }, + { + "value": "count", + "label": "Count" + } + ] + } + }, + { + "type": "text", + "name": "step", + "label": "Step (e.g. 60s, 5m)", + "defaultValue": "60s" + } + ], + "metadata": [ + { + "name": "labels", + "displayName": "Labels", + "shape": "string" + }, + { + "name": "timestamp", + "displayName": "Timestamp", + "shape": "date", + "role": "timestamp" + }, + { + "name": "value", + "displayName": "Value", + "shape": "number" + } + ], + "timeframes": true +} \ No newline at end of file diff --git a/plugins/OpenObserve/v1/docs/README.md b/plugins/OpenObserve/v1/docs/README.md index 97d6e3fe..e82c0b3b 100644 --- a/plugins/OpenObserve/v1/docs/README.md +++ b/plugins/OpenObserve/v1/docs/README.md @@ -31,7 +31,7 @@ On save, the plugin validates these by fetching your organization's stream list; - **Streams** — logs, metrics, and trace streams registered in your organization, with document counts, storage size, schema, and settings. - **Search** — ad hoc SQL queries against a stream's raw log, metric, or trace records, and the most common values for chosen fields. -- **Metrics** — PromQL range queries against metrics ingested into OpenObserve. +- **Metrics** — PromQL range queries against metrics ingested into OpenObserve, including a guided metric/label/value picker for building a query without writing PromQL. - **Traces** — recent trace spans within a time range, for an individual trace stream. - **Functions** — VRL data-transformation functions defined in the organization. - **Pipelines** — data-transformation pipelines and their execution history. @@ -50,6 +50,8 @@ The out-of-the-box dashboards include an org-wide **Overview** plus a **Stream** - **Stream Field Values** — the most common values for chosen fields in one stream; scoped to a stream. - **Traces Latest** — recent traces within a time range, with a span count and a link to view each trace in OpenObserve; scoped to a trace stream. - **Metrics Query** — time series data matching a PromQL query; account-wide. +- **Simple Metrics Query** — time series data for a metric, built from a guided picker (metric → label → values → aggregation) instead of hand-written PromQL; account-wide. +- **Metric Names**, **Metric Labels**, **Label Values** — back the Simple Metrics Query picker's cascading dropdowns; hidden from the tile editor. - **Pipelines** — data-transformation pipelines defined in the organization, with node/edge counts and last error; account-wide. - **Pipeline History** — pipeline execution history within a time range; account-wide. - **Incidents** — alert incidents, with status, severity, and timestamps; account-wide. @@ -67,7 +69,7 @@ The out-of-the-box dashboards include an org-wide **Overview** plus a **Stream** - **Cluster Info requires a clustered deployment** — on a standalone single-node OpenObserve instance (the common self-hosted setup) this endpoint returns a 404; the plugin surfaces this as a clear message rather than raw error, and no default dashboard tile uses it. Clustered deployments can add their own tile. - **Incident Stats may not be available on your deployment** — this endpoint returned "Not Supported" on the instance this plugin was tested against, likely an edition/license gate; the plugin surfaces this as a clear message, and no default dashboard tile uses it. - **Search and Logs Search require the stream name typed manually** — the SQL's `FROM` clause isn't auto-filled; default tiles ship with placeholder/example SQL that may need editing to match your streams. -- **Very broad search timeframes may exceed the platform's response size limit** — narrow the timeframe or reduce **Max rows** if a Search, Logs Search, or Traces Latest tile fails to load. +- **Very broad search timeframes may exceed the platform's response size limit** — narrow the timeframe or reduce **Max rows** if a Search, Logs Search, or Traces Latest tile fails to load. A Simple Metrics Query or Metrics Query with no label filter can hit the same limit on a high-cardinality metric over a wide timeframe — add a label filter or narrow the timeframe. - **Pipeline History's time-range unit is unverified** — this plugin sends microseconds, matching every other timed endpoint in this plugin, but it couldn't be confirmed against real pipeline-run data; if the tile returns no rows despite a pipeline having run recently, this is worth rechecking. - **Traces Latest only applies to trace-type streams** — it isn't included on the Stream perspective dashboard (which covers all three stream types); add it manually on a dashboard scoped to a trace stream. - **Traces Latest's "View Trace" link format is unverified** — it's built from OpenObserve's documented `/web/traces/trace-details` route (`org_identifier`, `stream`, `trace_id`, `from`, `to` query params, with `from`/`to` in microseconds), but this plugin was built without being able to click through and confirm the link actually opens the right trace. If it doesn't resolve correctly, this is the first thing to check. From 8de7fce06e0c2606b0dab6220f8a2e79da168296 Mon Sep 17 00:00:00 2001 From: John Hayes Date: Tue, 20 Oct 2026 14:59:29 +0100 Subject: [PATCH 08/22] Restructure metrics query output: Name column + per-label columns Replaces the combined "Labels" JSON blob column with a dedicated "Name" column (the metric name) and one individual column per label, shared by both Metrics Query and Simple Metrics Query since they use the same response-flattening script. Co-Authored-By: Claude Sonnet 5 --- .../OpenObserve/v1/dataStreams/metricsQuery.json | 5 +++-- .../v1/dataStreams/scripts/metricsQuery.js | 15 ++++++++++----- .../v1/dataStreams/simpleMetricsQuery.json | 10 ++++++---- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/plugins/OpenObserve/v1/dataStreams/metricsQuery.json b/plugins/OpenObserve/v1/dataStreams/metricsQuery.json index beeafd2e..a3a1b91d 100644 --- a/plugins/OpenObserve/v1/dataStreams/metricsQuery.json +++ b/plugins/OpenObserve/v1/dataStreams/metricsQuery.json @@ -32,9 +32,10 @@ } ], "metadata": [ - { "name": "labels", "displayName": "Labels", "shape": "string" }, + { "name": "name", "displayName": "Name", "shape": "string", "role": "label" }, { "name": "timestamp", "displayName": "Timestamp", "shape": "date", "role": "timestamp" }, - { "name": "value", "displayName": "Value", "shape": "number" } + { "name": "value", "displayName": "Value", "shape": "number" }, + { "pattern": ".*" } ], "timeframes": true } diff --git a/plugins/OpenObserve/v1/dataStreams/scripts/metricsQuery.js b/plugins/OpenObserve/v1/dataStreams/scripts/metricsQuery.js index 07b780ae..9a3262f8 100644 --- a/plugins/OpenObserve/v1/dataStreams/scripts/metricsQuery.js +++ b/plugins/OpenObserve/v1/dataStreams/scripts/metricsQuery.js @@ -1,8 +1,13 @@ // dataStreams/scripts/metricsQuery.js -result = ((data.data && data.data.result) || []).flatMap((series) => - (series.values || []).map(([ts, val]) => ({ - labels: JSON.stringify(series.metric || {}), +// Spread each series' labels as individual top-level columns (named exactly +// as the label) instead of one combined JSON blob, with the reserved +// "__name__" label pulled out separately as "name". +result = ((data.data && data.data.result) || []).flatMap((series) => { + const { __name__, ...labels } = series.metric || {}; + return (series.values || []).map(([ts, val]) => ({ + name: __name__ || "", + ...labels, timestamp: ts * 1000, value: parseFloat(val), - })), -); + })); +}); diff --git a/plugins/OpenObserve/v1/dataStreams/simpleMetricsQuery.json b/plugins/OpenObserve/v1/dataStreams/simpleMetricsQuery.json index 8a678f2d..863129b2 100644 --- a/plugins/OpenObserve/v1/dataStreams/simpleMetricsQuery.json +++ b/plugins/OpenObserve/v1/dataStreams/simpleMetricsQuery.json @@ -130,9 +130,10 @@ ], "metadata": [ { - "name": "labels", - "displayName": "Labels", - "shape": "string" + "name": "name", + "displayName": "Name", + "shape": "string", + "role": "label" }, { "name": "timestamp", @@ -144,7 +145,8 @@ "name": "value", "displayName": "Value", "shape": "number" - } + }, + { "pattern": ".*" } ], "timeframes": true } \ No newline at end of file From 5bee4cfea3c492ea23fdd9536d541633b61289d9 Mon Sep 17 00:00:00 2001 From: John Hayes Date: Tue, 20 Oct 2026 19:19:27 +0100 Subject: [PATCH 09/22] Use a code editor for the Metrics Query PromQL field Switches the PromQL Query field from a single-line text input to a multi-line code editor, matching the look of built-in Prometheus-style query fields elsewhere in the platform. Co-Authored-By: Claude Sonnet 5 --- plugins/OpenObserve/v1/dataStreams/metricsQuery.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/OpenObserve/v1/dataStreams/metricsQuery.json b/plugins/OpenObserve/v1/dataStreams/metricsQuery.json index a3a1b91d..3c7f08fe 100644 --- a/plugins/OpenObserve/v1/dataStreams/metricsQuery.json +++ b/plugins/OpenObserve/v1/dataStreams/metricsQuery.json @@ -18,9 +18,10 @@ "matches": "none", "ui": [ { - "type": "text", + "type": "code", "name": "promql", "label": "PromQL Query", + "language": "promql", "placeholder": "up or rate(http_requests_total[5m])", "validation": { "required": true } }, From 78c224d878c6c6e1599cc6eb5437ecea4e9e0955 Mon Sep 17 00:00:00 2001 From: John Hayes Date: Tue, 20 Oct 2026 19:19:32 +0100 Subject: [PATCH 10/22] Surface pipeline source stream name/type as columns Extracts stream_name/stream_type from the source JSON blob into their own Source Stream Name / Source Stream Type columns, alongside the existing raw Source column. Co-Authored-By: Claude Sonnet 5 --- plugins/OpenObserve/v1/dataStreams/pipelines.json | 2 ++ plugins/OpenObserve/v1/dataStreams/scripts/pipelines.js | 2 ++ 2 files changed, 4 insertions(+) diff --git a/plugins/OpenObserve/v1/dataStreams/pipelines.json b/plugins/OpenObserve/v1/dataStreams/pipelines.json index 83b4f9b4..e36ff222 100644 --- a/plugins/OpenObserve/v1/dataStreams/pipelines.json +++ b/plugins/OpenObserve/v1/dataStreams/pipelines.json @@ -19,6 +19,8 @@ { "name": "paused_at", "displayName": "Paused At", "shape": "number" }, { "name": "last_error", "displayName": "Last Error", "shape": "json" }, { "name": "source", "displayName": "Source", "shape": "json" }, + { "name": "sourceStreamName", "displayName": "Source Stream Name", "shape": "string" }, + { "name": "sourceStreamType", "displayName": "Source Stream Type", "shape": "string" }, { "name": "nodeCount", "displayName": "Node Count", "shape": "number" }, { "name": "edgeCount", "displayName": "Edge Count", "shape": "number" } ], diff --git a/plugins/OpenObserve/v1/dataStreams/scripts/pipelines.js b/plugins/OpenObserve/v1/dataStreams/scripts/pipelines.js index 2dd89c79..077bea72 100644 --- a/plugins/OpenObserve/v1/dataStreams/scripts/pipelines.js +++ b/plugins/OpenObserve/v1/dataStreams/scripts/pipelines.js @@ -15,6 +15,8 @@ result = (data.list || []).map((p) => { paused_at: p.paused_at != null ? p.paused_at : null, last_error: p.last_error != null ? JSON.stringify(p.last_error) : null, source: p.source != null ? JSON.stringify(p.source) : null, + sourceStreamName: (p.source && p.source.stream_name) || "", + sourceStreamType: (p.source && p.source.stream_type) || "", nodeCount: nodes.length, edgeCount: edges.length, }; From bd1ca1c901bc4324ff050ca279f28f2357e4e071 Mon Sep 17 00:00:00 2001 From: John Hayes Date: Tue, 20 Oct 2026 19:19:38 +0100 Subject: [PATCH 11/22] Fix Traces Latest date bucketing, add configurable bucket interval startTime was a "computed: true" + valueExpression-derived column, which doesn't show up as a Bucket option in the tile editor's native Group-by UI - every other working date column in this plugin is a real field set directly in the script instead, so startTime/endTime/ durationMs move there too. Also adds an optional Bucket interval field (e.g. "5m") that switches the stream to one row per fixed-width time window (trace count plus avg/min/max duration), since the tile editor's native Group-by has no granularity finer than hourly. Co-Authored-By: Claude Sonnet 5 --- .../v1/dataStreams/scripts/tracesLatest.js | 66 +++++++++++++++---- .../v1/dataStreams/tracesLatest.json | 39 ++++++++--- plugins/OpenObserve/v1/docs/README.md | 3 +- 3 files changed, 85 insertions(+), 23 deletions(-) diff --git a/plugins/OpenObserve/v1/dataStreams/scripts/tracesLatest.js b/plugins/OpenObserve/v1/dataStreams/scripts/tracesLatest.js index 34334997..6cbf0bc0 100644 --- a/plugins/OpenObserve/v1/dataStreams/scripts/tracesLatest.js +++ b/plugins/OpenObserve/v1/dataStreams/scripts/tracesLatest.js @@ -11,20 +11,60 @@ // link also has to be built here: "dataSource.*" is only resolvable inside // config strings/scripts, not inside a metadata valueExpression (confirmed // empirically - it throws "dataSource is not defined" there). +// +// startTime/endTime/durationMs are set here as real fields rather than +// metadata-level "computed: true" + valueExpression derivations - a +// computed/derived date column doesn't show up as a "Bucket" option in the +// tile editor's Group-by UI, unlike a real field already in the script's +// row (the pattern every other date column in this plugin uses). The tile +// editor's native Group-by buckets no finer than byHour, so an optional +// "bucketInterval" param (e.g. "5m") switches this stream to pre-aggregated +// rows - one per fixed-width time window - computed here instead. const ds = (context.dataSources && context.dataSources[0]) || {}; const webBase = [ds.url, ds.port].filter(Boolean).join(":"); const streamName = (context.objects && context.objects[0] && context.objects[0].name) || ""; -result = (data.hits || []).map((row) => { - const { spans, service_name, models, ...rest } = row; - const fromUs = Math.floor(row.start_time / 1000); - const toUs = Math.floor(row.end_time / 1000); - return { - ...rest, - spanCount: (spans && spans[0]) || 0, - serviceCount: (service_name || []).length, - services: (service_name || []).map((s) => s.service_name).join(", "), - models: (models || []).join(", "), - traceUrl: `${webBase}/web/traces/trace-details?org_identifier=${ds.organization}&stream=${streamName}&trace_id=${row.trace_id}&from=${fromUs}&to=${toUs}`, - }; -}); +function parseIntervalMs(interval) { + const m = /^(\d+)\s*(s|m|h)$/i.exec((interval || "").trim()); + if (!m) return null; + const n = parseInt(m[1], 10); + const unitMs = { s: 1000, m: 60000, h: 3600000 }[m[2].toLowerCase()]; + return n * unitMs; +} + +const bucketMs = parseIntervalMs(context.config && context.config.bucketInterval); + +if (bucketMs) { + const buckets = new Map(); + for (const row of data.hits || []) { + const bucketStart = Math.floor(row.start_time / 1000000 / bucketMs) * bucketMs; + if (!buckets.has(bucketStart)) buckets.set(bucketStart, []); + buckets.get(bucketStart).push(row.duration / 1000); + } + result = Array.from(buckets.entries()) + .sort((a, b) => a[0] - b[0]) + .map(([bucketStart, durations]) => ({ + startTime: bucketStart, + traceCount: durations.length, + avgDurationMs: durations.reduce((a, b) => a + b, 0) / durations.length, + minDurationMs: Math.min(...durations), + maxDurationMs: Math.max(...durations), + })); +} else { + result = (data.hits || []).map((row) => { + const { spans, service_name, models, ...rest } = row; + const fromUs = Math.floor(row.start_time / 1000); + const toUs = Math.floor(row.end_time / 1000); + return { + ...rest, + startTime: row.start_time / 1000000, + endTime: row.end_time / 1000000, + durationMs: row.duration / 1000, + spanCount: (spans && spans[0]) || 0, + serviceCount: (service_name || []).length, + services: (service_name || []).map((s) => s.service_name).join(", "), + models: (models || []).join(", "), + traceUrl: `${webBase}/web/traces/trace-details?org_identifier=${ds.organization}&stream=${streamName}&trace_id=${row.trace_id}&from=${fromUs}&to=${toUs}`, + }; + }); +} diff --git a/plugins/OpenObserve/v1/dataStreams/tracesLatest.json b/plugins/OpenObserve/v1/dataStreams/tracesLatest.json index 103ff3d9..9ec557eb 100644 --- a/plugins/OpenObserve/v1/dataStreams/tracesLatest.json +++ b/plugins/OpenObserve/v1/dataStreams/tracesLatest.json @@ -31,6 +31,13 @@ "type": "text", "name": "filter", "label": "Filter (optional)" + }, + { + "type": "text", + "name": "bucketInterval", + "label": "Bucket interval (optional)", + "placeholder": "e.g. 5m, 15m, 1h", + "help": "Leave blank to return one row per trace. Set to a width like `5m` or `1h` to return one row per time bucket instead, with a trace count and duration stats — useful for charting trace volume at a finer granularity than the tile editor's own Group-by buckets support (hourly is the finest native option)." } ], "metadata": [ @@ -41,24 +48,38 @@ { "name": "startTime", "displayName": "Start Time", - "computed": true, "role": "timestamp", - "shape": "date", - "valueExpression": "{{ $['start_time'] / 1000000 }}" + "shape": "date" }, { "name": "endTime", "displayName": "End Time", - "computed": true, - "shape": "date", - "valueExpression": "{{ $['end_time'] / 1000000 }}" + "shape": "date" }, { "name": "durationMs", "displayName": "Duration (ms)", - "computed": true, - "shape": "milliseconds", - "valueExpression": "{{ $['duration'] / 1000 }}" + "shape": "milliseconds" + }, + { + "name": "traceCount", + "displayName": "Trace Count", + "shape": "number" + }, + { + "name": "avgDurationMs", + "displayName": "Avg Duration (ms)", + "shape": "milliseconds" + }, + { + "name": "minDurationMs", + "displayName": "Min Duration (ms)", + "shape": "milliseconds" + }, + { + "name": "maxDurationMs", + "displayName": "Max Duration (ms)", + "shape": "milliseconds" }, { "name": "spanCount", diff --git a/plugins/OpenObserve/v1/docs/README.md b/plugins/OpenObserve/v1/docs/README.md index e82c0b3b..d4f15988 100644 --- a/plugins/OpenObserve/v1/docs/README.md +++ b/plugins/OpenObserve/v1/docs/README.md @@ -48,7 +48,7 @@ The out-of-the-box dashboards include an org-wide **Overview** plus a **Stream** - **Search** — records matching a user-supplied SQL query against one stream; scoped to a stream. - **Logs Search** — records matching a user-supplied SQL query against any log stream, without picking a stream first; account-wide. - **Stream Field Values** — the most common values for chosen fields in one stream; scoped to a stream. -- **Traces Latest** — recent traces within a time range, with a span count and a link to view each trace in OpenObserve; scoped to a trace stream. +- **Traces Latest** — recent traces within a time range, with a span count and a link to view each trace in OpenObserve; scoped to a trace stream. An optional **Bucket interval** (e.g. `5m`) switches it to one row per time bucket (trace count plus avg/min/max duration) for charting finer than the tile editor's own hourly minimum. - **Metrics Query** — time series data matching a PromQL query; account-wide. - **Simple Metrics Query** — time series data for a metric, built from a guided picker (metric → label → values → aggregation) instead of hand-written PromQL; account-wide. - **Metric Names**, **Metric Labels**, **Label Values** — back the Simple Metrics Query picker's cascading dropdowns; hidden from the tile editor. @@ -73,4 +73,5 @@ The out-of-the-box dashboards include an org-wide **Overview** plus a **Stream** - **Pipeline History's time-range unit is unverified** — this plugin sends microseconds, matching every other timed endpoint in this plugin, but it couldn't be confirmed against real pipeline-run data; if the tile returns no rows despite a pipeline having run recently, this is worth rechecking. - **Traces Latest only applies to trace-type streams** — it isn't included on the Stream perspective dashboard (which covers all three stream types); add it manually on a dashboard scoped to a trace stream. - **Traces Latest's "View Trace" link format is unverified** — it's built from OpenObserve's documented `/web/traces/trace-details` route (`org_identifier`, `stream`, `trace_id`, `from`, `to` query params, with `from`/`to` in microseconds), but this plugin was built without being able to click through and confirm the link actually opens the right trace. If it doesn't resolve correctly, this is the first thing to check. +- **Traces Latest's Bucket interval only buckets the traces actually fetched** — it doesn't change how many traces the API returns, only **Max rows**. On a high-volume trace stream, a short bucket width (e.g. `1m`) may need a correspondingly larger **Max rows** to see more than one or two buckets, which risks the response-size limit above — there's a real trade-off between bucket granularity and how much time the buckets actually cover. - **Read-only** — the plugin never creates, modifies, or deletes anything in OpenObserve. From d7039b151228cae48d7db3fc28059b22d46f0ddc Mon Sep 17 00:00:00 2001 From: John Hayes Date: Tue, 20 Oct 2026 19:53:03 +0100 Subject: [PATCH 12/22] Fix empty Pipeline History timestamp, clarify Query Took's unit timestamp's valueExpression self-referenced $['timestamp'] (the same column it was overriding), which rendered empty - same bug class as tracesLatest's trace_id link. Moved to a real script-computed field, verified against real pipeline-run data now available on the test instance. Query Took's unit isn't documented anywhere by OpenObserve; labeled as milliseconds (estimated) based on the Search endpoint's documented "took" unit and the realistic magnitudes seen in testing. Delay and Evaluation Took now use proper "seconds" shapes, matching their explicit _in_secs field names. Co-Authored-By: Claude Sonnet 5 --- .../OpenObserve/v1/dataStreams/pipelineHistory.json | 11 +++++------ .../v1/dataStreams/scripts/pipelineHistory.js | 10 ++++++++++ plugins/OpenObserve/v1/docs/README.md | 2 +- 3 files changed, 16 insertions(+), 7 deletions(-) create mode 100644 plugins/OpenObserve/v1/dataStreams/scripts/pipelineHistory.js diff --git a/plugins/OpenObserve/v1/dataStreams/pipelineHistory.json b/plugins/OpenObserve/v1/dataStreams/pipelineHistory.json index ec73c1a4..ba70284c 100644 --- a/plugins/OpenObserve/v1/dataStreams/pipelineHistory.json +++ b/plugins/OpenObserve/v1/dataStreams/pipelineHistory.json @@ -14,7 +14,7 @@ { "key": "from", "value": "{{from || 0}}" }, { "key": "size", "value": "{{size || 50}}" } ], - "pathToData": "hits" + "postRequestScript": "pipelineHistory.js" }, "matches": "none", "ui": [ @@ -43,16 +43,15 @@ "name": "timestamp", "displayName": "Timestamp", "shape": "date", - "role": "timestamp", - "valueExpression": "{{ $['timestamp'] / 1000 }}" + "role": "timestamp" }, { "name": "is_realtime", "displayName": "Is Realtime", "shape": "boolean" }, { "name": "is_silenced", "displayName": "Is Silenced", "shape": "boolean" }, { "name": "retries", "displayName": "Retries", "shape": "number" }, - { "name": "delay_in_secs", "displayName": "Delay (s)", "shape": "number" }, + { "name": "delay_in_secs", "displayName": "Delay", "shape": "seconds" }, { "name": "error", "displayName": "Error", "shape": "string" }, - { "name": "evaluation_took_in_secs", "displayName": "Evaluation Took (s)", "shape": "number" }, - { "name": "query_took", "displayName": "Query Took", "shape": "number" }, + { "name": "evaluation_took_in_secs", "displayName": "Evaluation Took", "shape": "seconds" }, + { "name": "query_took", "displayName": "Query Took (ms, estimated)", "shape": "milliseconds" }, { "name": "source_node", "displayName": "Source Node", "shape": "string" } ], "timeframes": true diff --git a/plugins/OpenObserve/v1/dataStreams/scripts/pipelineHistory.js b/plugins/OpenObserve/v1/dataStreams/scripts/pipelineHistory.js new file mode 100644 index 00000000..e1ce1a8b --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/scripts/pipelineHistory.js @@ -0,0 +1,10 @@ +// dataStreams/scripts/pipelineHistory.js +// "timestamp"'s valueExpression previously self-referenced $['timestamp'] +// (the same column it was overriding), which rendered empty - the same +// class of bug fixed in tracesLatest's trace_id link. Compute it as a real +// field here instead, matching the proven pattern used elsewhere in this +// plugin. +result = (data.hits || []).map((entry) => ({ + ...entry, + timestamp: entry.timestamp / 1000, +})); diff --git a/plugins/OpenObserve/v1/docs/README.md b/plugins/OpenObserve/v1/docs/README.md index d4f15988..f40ecc5c 100644 --- a/plugins/OpenObserve/v1/docs/README.md +++ b/plugins/OpenObserve/v1/docs/README.md @@ -70,7 +70,7 @@ The out-of-the-box dashboards include an org-wide **Overview** plus a **Stream** - **Incident Stats may not be available on your deployment** — this endpoint returned "Not Supported" on the instance this plugin was tested against, likely an edition/license gate; the plugin surfaces this as a clear message, and no default dashboard tile uses it. - **Search and Logs Search require the stream name typed manually** — the SQL's `FROM` clause isn't auto-filled; default tiles ship with placeholder/example SQL that may need editing to match your streams. - **Very broad search timeframes may exceed the platform's response size limit** — narrow the timeframe or reduce **Max rows** if a Search, Logs Search, or Traces Latest tile fails to load. A Simple Metrics Query or Metrics Query with no label filter can hit the same limit on a high-cardinality metric over a wide timeframe — add a label filter or narrow the timeframe. -- **Pipeline History's time-range unit is unverified** — this plugin sends microseconds, matching every other timed endpoint in this plugin, but it couldn't be confirmed against real pipeline-run data; if the tile returns no rows despite a pipeline having run recently, this is worth rechecking. +- **Pipeline History's Query Took unit is inferred, not documented** — OpenObserve doesn't document the unit anywhere; it's shown as milliseconds, matching the `took` field's documented unit on the Search endpoint and the realistic magnitude seen in testing (single-digit ms for a lightweight scheduled query). Delay and Evaluation Took are shown as seconds, since OpenObserve names those fields explicitly (`delay_in_secs`, `evaluation_took_in_secs`). - **Traces Latest only applies to trace-type streams** — it isn't included on the Stream perspective dashboard (which covers all three stream types); add it manually on a dashboard scoped to a trace stream. - **Traces Latest's "View Trace" link format is unverified** — it's built from OpenObserve's documented `/web/traces/trace-details` route (`org_identifier`, `stream`, `trace_id`, `from`, `to` query params, with `from`/`to` in microseconds), but this plugin was built without being able to click through and confirm the link actually opens the right trace. If it doesn't resolve correctly, this is the first thing to check. - **Traces Latest's Bucket interval only buckets the traces actually fetched** — it doesn't change how many traces the API returns, only **Max rows**. On a high-volume trace stream, a short bucket width (e.g. `1m`) may need a correspondingly larger **Max rows** to see more than one or two buckets, which risks the response-size limit above — there's a real trade-off between bucket granularity and how much time the buckets actually cover. From c8df9385f8cc2af4d42192023196c38198ced2e2 Mon Sep 17 00:00:00 2001 From: John Hayes Date: Tue, 20 Oct 2026 22:57:45 +0100 Subject: [PATCH 13/22] Drop Stream Field Values data stream Removes the streamFieldValues stream and the "Field Values" tile that referenced it on the Stream perspective dashboard. Co-Authored-By: Claude Sonnet 5 --- .../dataStreams/scripts/streamFieldValues.js | 7 --- .../v1/dataStreams/streamFieldValues.json | 53 ------------------- .../defaultContent/streamDashboard.dash.json | 38 ------------- 3 files changed, 98 deletions(-) delete mode 100644 plugins/OpenObserve/v1/dataStreams/scripts/streamFieldValues.js delete mode 100644 plugins/OpenObserve/v1/dataStreams/streamFieldValues.json diff --git a/plugins/OpenObserve/v1/dataStreams/scripts/streamFieldValues.js b/plugins/OpenObserve/v1/dataStreams/scripts/streamFieldValues.js deleted file mode 100644 index 4471a7ef..00000000 --- a/plugins/OpenObserve/v1/dataStreams/scripts/streamFieldValues.js +++ /dev/null @@ -1,7 +0,0 @@ -result = (data.hits || []).flatMap((h) => - (h.values || []).map((v) => ({ - field: h.field, - value: v.zo_sql_key, - count: v.zo_sql_num, - })), -); diff --git a/plugins/OpenObserve/v1/dataStreams/streamFieldValues.json b/plugins/OpenObserve/v1/dataStreams/streamFieldValues.json deleted file mode 100644 index 1df34f4f..00000000 --- a/plugins/OpenObserve/v1/dataStreams/streamFieldValues.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "name": "streamFieldValues", - "displayName": "Stream Field Values", - "description": "Top values for chosen fields in a stream", - "tags": ["Search"], - "baseDataSourceName": "httpRequestScopedSingle", - "config": { - "httpMethod": "get", - "endpointPath": "{{object.name}}/_values", - "getArgs": [ - { "key": "fields", "value": "{{fields}}" }, - { "key": "type", "value": "{{object.stream_type}}" }, - { "key": "start_time", "value": "{{timeframe.unixStart * 1000000}}" }, - { "key": "end_time", "value": "{{timeframe.unixEnd * 1000000}}" }, - { "key": "size", "value": "{{size || null}}" }, - { "key": "keyword", "value": "{{keyword || null}}" }, - { "key": "no_count", "value": "{{no_count ? 'true' : 'false'}}" } - ], - "postRequestScript": "streamFieldValues.js" - }, - "matches": { "sourceType": { "type": "oneOf", "values": ["OpenObserve Stream"] } }, - "ui": [ - { - "type": "text", - "name": "fields", - "label": "Fields", - "placeholder": "field1,field2", - "validation": { "required": true } - }, - { - "type": "number", - "name": "size", - "label": "Max values per field", - "defaultValue": 10 - }, - { - "type": "text", - "name": "keyword", - "label": "Filter keyword (optional)" - }, - { - "type": "checkbox", - "name": "no_count", - "label": "Order by value instead of count" - } - ], - "metadata": [ - { "name": "field", "displayName": "Field", "shape": "string" }, - { "name": "value", "displayName": "Value", "shape": "string", "role": "label" }, - { "name": "count", "displayName": "Count", "shape": "number" } - ], - "timeframes": true -} diff --git a/plugins/OpenObserve/v1/defaultContent/streamDashboard.dash.json b/plugins/OpenObserve/v1/defaultContent/streamDashboard.dash.json index c2120aa6..f32292a8 100644 --- a/plugins/OpenObserve/v1/defaultContent/streamDashboard.dash.json +++ b/plugins/OpenObserve/v1/defaultContent/streamDashboard.dash.json @@ -89,44 +89,6 @@ } } } - }, - { - "i": "192d3b53-6e6f-4f66-9574-0d6e37174ff6", - "x": 0, - "y": 3, - "w": 4, - "h": 3, - "moved": false, - "static": false, - "z": 0, - "config": { - "_type": "tile/data-stream", - "title": "Field Values", - "description": "Top values for the configured field(s) — edit the field list in the tile's parameters", - "activePluginConfigIds": ["{{configId}}"], - "dataStream": { - "id": "{{dataStreams.streamFieldValues}}", - "name": "streamFieldValues", - "pluginConfigId": "{{configId}}", - "dataSourceConfig": { "fields": "_timestamp" }, - "sort": { "by": [["count", "desc"]] } - }, - "scope": { - "scope": "{{scopes.[Streams]}}", - "workspace": "{{workspaceId}}", - "variable": "{{variables.[Stream]}}" - }, - "variables": ["{{variables.[Stream]}}"], - "visualisation": { - "type": "data-stream-table", - "config": { - "data-stream-table": { - "transpose": false, - "columnOrder": ["field", "value", "count"] - } - } - } - } } ] } From c07f88268f73f40c9f0383b149c845a06370eae0 Mon Sep 17 00:00:00 2001 From: John Hayes Date: Tue, 20 Oct 2026 22:57:52 +0100 Subject: [PATCH 14/22] Drop Functions data stream Removes the functions stream and the two Overview dashboard tiles (scalar count + table) that referenced it. Co-Authored-By: Claude Sonnet 5 --- .../OpenObserve/v1/dataStreams/functions.json | 22 ------- .../overviewDashboard.dash.json | 66 ------------------- 2 files changed, 88 deletions(-) delete mode 100644 plugins/OpenObserve/v1/dataStreams/functions.json diff --git a/plugins/OpenObserve/v1/dataStreams/functions.json b/plugins/OpenObserve/v1/dataStreams/functions.json deleted file mode 100644 index dadb3c7e..00000000 --- a/plugins/OpenObserve/v1/dataStreams/functions.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "functions", - "displayName": "Functions", - "description": "VRL functions defined in the organization", - "tags": ["Functions"], - "baseDataSourceName": "httpRequestUnscoped", - "config": { - "httpMethod": "get", - "endpointPath": "functions", - "pathToData": "list" - }, - "matches": "none", - "metadata": [ - { "name": "stream_name", "displayName": "Stream", "shape": "string" }, - { "name": "name", "displayName": "Function Name", "shape": "string", "role": "label" }, - { "name": "function", "displayName": "Function Body", "shape": "string" }, - { "name": "order", "displayName": "Order", "shape": "number" }, - { "name": "num_args", "displayName": "Argument Count", "shape": "number" }, - { "name": "trans_type", "displayName": "Transform Type", "shape": "number" } - ], - "timeframes": false -} diff --git a/plugins/OpenObserve/v1/defaultContent/overviewDashboard.dash.json b/plugins/OpenObserve/v1/defaultContent/overviewDashboard.dash.json index e9a222f6..d38f40e5 100644 --- a/plugins/OpenObserve/v1/defaultContent/overviewDashboard.dash.json +++ b/plugins/OpenObserve/v1/defaultContent/overviewDashboard.dash.json @@ -109,39 +109,6 @@ } } }, - { - "i": "9a0da9ec-2a4a-468a-b1d9-c2533a1c2a92", - "x": 3, - "y": 0, - "w": 1, - "h": 2, - "moved": false, - "static": false, - "z": 0, - "config": { - "_type": "tile/data-stream", - "title": "Functions", - "description": "", - "timeframe": "none", - "activePluginConfigIds": ["{{configId}}"], - "dataStream": { - "id": "{{dataStreams.functions}}", - "name": "functions", - "pluginConfigId": "{{configId}}", - "group": { "by": [], "aggregate": [{ "type": "count" }] } - }, - "visualisation": { - "type": "data-stream-scalar", - "config": { - "data-stream-scalar": { - "value": "count", - "comparisonColumn": "none", - "label": "Functions" - } - } - } - } - }, { "i": "c3bca13c-1d96-418a-8f02-ec97f411ce53", "x": 0, @@ -319,39 +286,6 @@ } } }, - { - "i": "c8e574cb-5c34-4330-b081-688f62b85f2d", - "x": 0, - "y": 7, - "w": 4, - "h": 3, - "moved": false, - "static": false, - "z": 0, - "config": { - "_type": "tile/data-stream", - "title": "Functions", - "description": "VRL functions defined in the organization", - "timeframe": "none", - "activePluginConfigIds": ["{{configId}}"], - "dataStream": { - "id": "{{dataStreams.functions}}", - "name": "functions", - "pluginConfigId": "{{configId}}", - "sort": { "by": [["stream_name", "asc"]] } - }, - "visualisation": { - "type": "data-stream-table", - "config": { - "data-stream-table": { - "transpose": false, - "columnOrder": ["stream_name", "name", "num_args", "function"], - "hiddenColumns": ["order", "trans_type"] - } - } - } - } - }, { "i": "f63ae978-8728-4106-9817-d63e5581048f", "x": 0, From bd1951adf6ae29e48082a117c9af24ccece1a1b3 Mon Sep 17 00:00:00 2001 From: John Hayes Date: Tue, 20 Oct 2026 22:57:57 +0100 Subject: [PATCH 15/22] Add SLOs data stream Lists Service Level Objectives with their current status (SLI, coverage, error budget remaining, burn rate), with an optional folder filter. Skips the no_data figures rather than defaulting them to zero, matching OpenObserve's own distinction between "not yet measured" and "measured as zero". Also covers the Functions/Stream Field Values removals in the Data streams and What this plugin monitors sections. Co-Authored-By: Claude Sonnet 5 --- .../v1/dataStreams/scripts/slos.js | 39 +++++++++++++++ plugins/OpenObserve/v1/dataStreams/slos.json | 50 +++++++++++++++++++ plugins/OpenObserve/v1/docs/README.md | 13 ++--- 3 files changed, 96 insertions(+), 6 deletions(-) create mode 100644 plugins/OpenObserve/v1/dataStreams/scripts/slos.js create mode 100644 plugins/OpenObserve/v1/dataStreams/slos.json diff --git a/plugins/OpenObserve/v1/dataStreams/scripts/slos.js b/plugins/OpenObserve/v1/dataStreams/scripts/slos.js new file mode 100644 index 00000000..b7f87c77 --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/scripts/slos.js @@ -0,0 +1,39 @@ +// dataStreams/scripts/slos.js +// "status" is null until an SLO's first measurement pass completes - every +// derived figure below must stay null in that case too, not default to 0 +// (per OpenObserve's own docs: "not yet measured" and "measured as 0" are +// different). "tags"/"group_by" are arrays, which can't be read via +// valueExpression even just to join them (see project memory), so that +// happens here. "config" is a free-form object shaped by sli_type - kept as +// a JSON blob rather than modelled field-by-field. +result = (data.list || []).map((slo) => { + const status = slo.status; + return { + id: slo.id, + name: slo.name, + description: slo.description || "", + enabled: slo.enabled, + target: slo.target, + owner: slo.owner || "", + folderId: slo.folder_id, + tags: (slo.tags || []).join(", "), + sliType: slo.sli_type, + config: slo.config != null ? JSON.stringify(slo.config) : null, + windowSecs: slo.window_secs, + sliceIntervalSecs: slo.slice_interval_secs, + groupBy: (slo.group_by || []).join(", "), + noData: status ? status.no_data : null, + sli: status ? status.sli : null, + coverage: status ? status.coverage : null, + good: status ? status.good : null, + total: status ? status.total : null, + coveredSlices: status ? status.covered_slices : null, + burnRate: status ? status.burn_rate : null, + errorBudgetRemaining: status ? status.error_budget_remaining : null, + timeToExhaustSecs: status ? status.time_to_exhaust_secs : null, + computedAt: status && status.computed_at != null ? status.computed_at * 1000 : null, + watermarkEnd: status && status.watermark_end != null ? status.watermark_end * 1000 : null, + measuringSince: status && status.measuring_since != null ? status.measuring_since * 1000 : null, + staleWatermark: status ? status.stale_watermark : null, + }; +}); diff --git a/plugins/OpenObserve/v1/dataStreams/slos.json b/plugins/OpenObserve/v1/dataStreams/slos.json new file mode 100644 index 00000000..3566779c --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/slos.json @@ -0,0 +1,50 @@ +{ + "name": "slos", + "displayName": "SLOs", + "description": "Service Level Objectives defined in the organization, with current status", + "tags": ["SLOs"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "slos", + "getArgs": [{ "key": "folder", "value": "{{folder || null}}" }], + "postRequestScript": "slos.js" + }, + "matches": "none", + "ui": [ + { + "type": "text", + "name": "folder", + "label": "Folder (optional)" + } + ], + "metadata": [ + { "name": "id", "displayName": "ID", "shape": "string" }, + { "name": "name", "displayName": "Name", "shape": "string", "role": "label" }, + { "name": "description", "displayName": "Description", "shape": "string" }, + { "name": "enabled", "displayName": "Enabled", "shape": "boolean" }, + { "name": "target", "displayName": "Target", "shape": "percent" }, + { "name": "owner", "displayName": "Owner", "shape": "string" }, + { "name": "folderId", "displayName": "Folder ID", "shape": "string" }, + { "name": "tags", "displayName": "Tags", "shape": "string" }, + { "name": "sliType", "displayName": "SLI Type", "shape": "string" }, + { "name": "config", "displayName": "Config", "shape": "json" }, + { "name": "windowSecs", "displayName": "Window", "shape": "seconds" }, + { "name": "sliceIntervalSecs", "displayName": "Slice Interval", "shape": "seconds" }, + { "name": "groupBy", "displayName": "Group By", "shape": "string" }, + { "name": "noData", "displayName": "No Data", "shape": "boolean" }, + { "name": "sli", "displayName": "SLI", "shape": "percent" }, + { "name": "coverage", "displayName": "Coverage", "shape": ["percent", { "asZeroToOne": true }] }, + { "name": "good", "displayName": "Good", "shape": "number" }, + { "name": "total", "displayName": "Total", "shape": "number" }, + { "name": "coveredSlices", "displayName": "Covered Slices", "shape": "number" }, + { "name": "burnRate", "displayName": "Burn Rate", "shape": ["number", { "decimalPlaces": 2 }] }, + { "name": "errorBudgetRemaining", "displayName": "Error Budget Remaining", "shape": "percent" }, + { "name": "timeToExhaustSecs", "displayName": "Time To Exhaust", "shape": "seconds" }, + { "name": "computedAt", "displayName": "Computed At", "shape": "date", "role": "timestamp" }, + { "name": "watermarkEnd", "displayName": "Watermark End", "shape": "date" }, + { "name": "measuringSince", "displayName": "Measuring Since", "shape": "date" }, + { "name": "staleWatermark", "displayName": "Stale Watermark", "shape": "boolean" } + ], + "timeframes": false +} diff --git a/plugins/OpenObserve/v1/docs/README.md b/plugins/OpenObserve/v1/docs/README.md index f40ecc5c..589d152b 100644 --- a/plugins/OpenObserve/v1/docs/README.md +++ b/plugins/OpenObserve/v1/docs/README.md @@ -1,6 +1,6 @@ -Monitor your self-hosted [OpenObserve](https://openobserve.ai) instance in SquaredUp — streams, log/metric/trace search, functions, pipelines, incidents, and cluster health — via the [OpenObserve API](https://openobserve.ai/docs/reference/api/). +Monitor your self-hosted [OpenObserve](https://openobserve.ai) instance in SquaredUp — streams, log/metric/trace search, pipelines, incidents, SLOs, and cluster health — via the [OpenObserve API](https://openobserve.ai/docs/reference/api/). -> ⚠️ This plugin covers self-hosted OpenObserve only, and only the **Streams**, **Search**, **Functions**, **Cluster**, **Metrics**, **Traces**, **Pipelines**, and **Incidents** API areas — it does not cover OpenObserve Cloud, ingestion, dashboards, reports, or user management. +> ⚠️ This plugin covers self-hosted OpenObserve only, and only the **Streams**, **Search**, **Cluster**, **Metrics**, **Traces**, **Pipelines**, **Incidents**, and **SLOs** API areas — it does not cover OpenObserve Cloud, ingestion, dashboards, reports, or user management. ## Setup @@ -30,12 +30,12 @@ On save, the plugin validates these by fetching your organization's stream list; ## What this plugin monitors - **Streams** — logs, metrics, and trace streams registered in your organization, with document counts, storage size, schema, and settings. -- **Search** — ad hoc SQL queries against a stream's raw log, metric, or trace records, and the most common values for chosen fields. +- **Search** — ad hoc SQL queries against a stream's raw log, metric, or trace records. - **Metrics** — PromQL range queries against metrics ingested into OpenObserve, including a guided metric/label/value picker for building a query without writing PromQL. - **Traces** — recent trace spans within a time range, for an individual trace stream. -- **Functions** — VRL data-transformation functions defined in the organization. - **Pipelines** — data-transformation pipelines and their execution history. - **Incidents** — alert incidents (groups of correlated alerts) and summary statistics. +- **SLOs** — Service Level Objectives, with current status (SLI, coverage, error budget remaining, burn rate). - **Cluster health** — pending compaction jobs per region/node, for clustered OpenObserve deployments. The out-of-the-box dashboards include an org-wide **Overview** plus a **Stream** perspective for drilling into an individual stream. @@ -44,10 +44,8 @@ The out-of-the-box dashboards include an org-wide **Overview** plus a **Stream** - **Streams** — streams of a chosen type (logs/metrics/traces), with name, document count, and size; account-wide. Backs import and is hidden from the tile editor. - **Stream Details** — streams of a chosen type (logs/metrics/traces), with document counts, storage size, schema, and settings; account-wide. -- **Functions** — VRL functions defined in the organization; account-wide. - **Search** — records matching a user-supplied SQL query against one stream; scoped to a stream. - **Logs Search** — records matching a user-supplied SQL query against any log stream, without picking a stream first; account-wide. -- **Stream Field Values** — the most common values for chosen fields in one stream; scoped to a stream. - **Traces Latest** — recent traces within a time range, with a span count and a link to view each trace in OpenObserve; scoped to a trace stream. An optional **Bucket interval** (e.g. `5m`) switches it to one row per time bucket (trace count plus avg/min/max duration) for charting finer than the tile editor's own hourly minimum. - **Metrics Query** — time series data matching a PromQL query; account-wide. - **Simple Metrics Query** — time series data for a metric, built from a guided picker (metric → label → values → aggregation) instead of hand-written PromQL; account-wide. @@ -56,6 +54,7 @@ The out-of-the-box dashboards include an org-wide **Overview** plus a **Stream** - **Pipeline History** — pipeline execution history within a time range; account-wide. - **Incidents** — alert incidents, with status, severity, and timestamps; account-wide. - **Incident Stats** — summary statistics for alert incidents; account-wide. May not be available on all self-hosted deployments (see Known limitations). +- **SLOs** — Service Level Objectives with current status (SLI, coverage, error budget remaining, burn rate); account-wide. Optional **Folder** filter. - **Cluster Info** — pending compaction jobs per region/node; account-wide. Only returns data on a clustered OpenObserve deployment (see Known limitations). ## What gets indexed @@ -74,4 +73,6 @@ The out-of-the-box dashboards include an org-wide **Overview** plus a **Stream** - **Traces Latest only applies to trace-type streams** — it isn't included on the Stream perspective dashboard (which covers all three stream types); add it manually on a dashboard scoped to a trace stream. - **Traces Latest's "View Trace" link format is unverified** — it's built from OpenObserve's documented `/web/traces/trace-details` route (`org_identifier`, `stream`, `trace_id`, `from`, `to` query params, with `from`/`to` in microseconds), but this plugin was built without being able to click through and confirm the link actually opens the right trace. If it doesn't resolve correctly, this is the first thing to check. - **Traces Latest's Bucket interval only buckets the traces actually fetched** — it doesn't change how many traces the API returns, only **Max rows**. On a high-volume trace stream, a short bucket width (e.g. `1m`) may need a correspondingly larger **Max rows** to see more than one or two buckets, which risks the response-size limit above — there's a real trade-off between bucket granularity and how much time the buckets actually cover. +- **SLOs show blank status figures until first measured, or when stale** — SLI, burn rate, error budget remaining, and the watermark/measuring-since timestamps are intentionally blank (not zero) when **No Data** is true, matching OpenObserve's own UI — a brand-new or stalled SLO isn't the same as one measured at 0%. +- **SLO per-group status isn't covered** — only the overall SLO list with its top-level status; drilling into a grouped SLO's per-group breakdown isn't implemented. - **Read-only** — the plugin never creates, modifies, or deletes anything in OpenObserve. From 89f79230651e71b975b15f76c90568d0a441a40e Mon Sep 17 00:00:00 2001 From: John Hayes Date: Thu, 22 Oct 2026 23:30:13 +0100 Subject: [PATCH 16/22] Restrict Traces Latest to last1hour to avoid OOMing the server /traces/latest runs a GROUP BY trace_id aggregation over every span in the chosen range before applying Max rows, so scan cost scales with the timeframe, not the row limit. A full-day range scanned ~1.66GB across 21 Parquet files and OOMKilled the OpenObserve pod (confirmed via its logs/exit code mid-session). Restricting to the last hour keeps this safe by default; verified live post-change with no crash. Co-Authored-By: Claude Sonnet 5 --- plugins/OpenObserve/v1/dataStreams/tracesLatest.json | 2 +- plugins/OpenObserve/v1/docs/README.md | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/OpenObserve/v1/dataStreams/tracesLatest.json b/plugins/OpenObserve/v1/dataStreams/tracesLatest.json index 9ec557eb..27c9e2be 100644 --- a/plugins/OpenObserve/v1/dataStreams/tracesLatest.json +++ b/plugins/OpenObserve/v1/dataStreams/tracesLatest.json @@ -109,5 +109,5 @@ }, { "pattern": ".*" } ], - "timeframes": true + "timeframes": ["last1hour"] } diff --git a/plugins/OpenObserve/v1/docs/README.md b/plugins/OpenObserve/v1/docs/README.md index 589d152b..bdfd570d 100644 --- a/plugins/OpenObserve/v1/docs/README.md +++ b/plugins/OpenObserve/v1/docs/README.md @@ -46,7 +46,7 @@ The out-of-the-box dashboards include an org-wide **Overview** plus a **Stream** - **Stream Details** — streams of a chosen type (logs/metrics/traces), with document counts, storage size, schema, and settings; account-wide. - **Search** — records matching a user-supplied SQL query against one stream; scoped to a stream. - **Logs Search** — records matching a user-supplied SQL query against any log stream, without picking a stream first; account-wide. -- **Traces Latest** — recent traces within a time range, with a span count and a link to view each trace in OpenObserve; scoped to a trace stream. An optional **Bucket interval** (e.g. `5m`) switches it to one row per time bucket (trace count plus avg/min/max duration) for charting finer than the tile editor's own hourly minimum. +- **Traces Latest** — recent traces within the last hour, with a span count and a link to view each trace in OpenObserve; scoped to a trace stream. An optional **Bucket interval** (e.g. `5m`) switches it to one row per time bucket (trace count plus avg/min/max duration) for charting finer than the tile editor's own hourly minimum. - **Metrics Query** — time series data matching a PromQL query; account-wide. - **Simple Metrics Query** — time series data for a metric, built from a guided picker (metric → label → values → aggregation) instead of hand-written PromQL; account-wide. - **Metric Names**, **Metric Labels**, **Label Values** — back the Simple Metrics Query picker's cascading dropdowns; hidden from the tile editor. @@ -71,6 +71,7 @@ The out-of-the-box dashboards include an org-wide **Overview** plus a **Stream** - **Very broad search timeframes may exceed the platform's response size limit** — narrow the timeframe or reduce **Max rows** if a Search, Logs Search, or Traces Latest tile fails to load. A Simple Metrics Query or Metrics Query with no label filter can hit the same limit on a high-cardinality metric over a wide timeframe — add a label filter or narrow the timeframe. - **Pipeline History's Query Took unit is inferred, not documented** — OpenObserve doesn't document the unit anywhere; it's shown as milliseconds, matching the `took` field's documented unit on the Search endpoint and the realistic magnitude seen in testing (single-digit ms for a lightweight scheduled query). Delay and Evaluation Took are shown as seconds, since OpenObserve names those fields explicitly (`delay_in_secs`, `evaluation_took_in_secs`). - **Traces Latest only applies to trace-type streams** — it isn't included on the Stream perspective dashboard (which covers all three stream types); add it manually on a dashboard scoped to a trace stream. +- **Traces Latest is fixed to the last hour** — `/traces/latest` runs a `GROUP BY trace_id` aggregation over every span in the chosen range before applying **Max rows**, so the scan cost scales with the timeframe, not the row limit. A wide range (e.g. a full day) can scan gigabytes of data and exhaust the server's memory. Restricted to `last1hour` to keep this safe by default. - **Traces Latest's "View Trace" link format is unverified** — it's built from OpenObserve's documented `/web/traces/trace-details` route (`org_identifier`, `stream`, `trace_id`, `from`, `to` query params, with `from`/`to` in microseconds), but this plugin was built without being able to click through and confirm the link actually opens the right trace. If it doesn't resolve correctly, this is the first thing to check. - **Traces Latest's Bucket interval only buckets the traces actually fetched** — it doesn't change how many traces the API returns, only **Max rows**. On a high-volume trace stream, a short bucket width (e.g. `1m`) may need a correspondingly larger **Max rows** to see more than one or two buckets, which risks the response-size limit above — there's a real trade-off between bucket granularity and how much time the buckets actually cover. - **SLOs show blank status figures until first measured, or when stale** — SLI, burn rate, error budget remaining, and the watermark/measuring-since timestamps are intentionally blank (not zero) when **No Data** is true, matching OpenObserve's own UI — a brand-new or stalled SLO isn't the same as one measured at 0%. From a99d5a24d5638637f19a11e3013d70882dbb5ea6 Mon Sep 17 00:00:00 2001 From: John Hayes Date: Tue, 27 Oct 2026 14:40:07 +0000 Subject: [PATCH 17/22] Add Alerts data stream, hide Cluster Info and Incidents streams Alerts lists the organization's alert rules from the v2 alerts API with current level, last outcome and evaluation timestamps (converted from microseconds), plus an optional folder filter. The Overview dashboard's Incidents section now shows Alerts instead, since Incidents, Incident Stats and Cluster Info are hidden from the tile editor. Co-Authored-By: Claude Opus 5.5 --- .../OpenObserve/v1/dataStreams/alerts.json | 69 +++++++++++++++++++ .../v1/dataStreams/clusterInfo.json | 1 + .../v1/dataStreams/incidentStats.json | 1 + .../OpenObserve/v1/dataStreams/incidents.json | 1 + .../v1/dataStreams/scripts/alerts.js | 29 ++++++++ .../overviewDashboard.dash.json | 24 +++---- plugins/OpenObserve/v1/docs/README.md | 15 ++-- plugins/OpenObserve/v1/metadata.json | 4 +- 8 files changed, 121 insertions(+), 23 deletions(-) create mode 100644 plugins/OpenObserve/v1/dataStreams/alerts.json create mode 100644 plugins/OpenObserve/v1/dataStreams/scripts/alerts.js diff --git a/plugins/OpenObserve/v1/dataStreams/alerts.json b/plugins/OpenObserve/v1/dataStreams/alerts.json new file mode 100644 index 00000000..6a8b1643 --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/alerts.json @@ -0,0 +1,69 @@ +{ + "name": "alerts", + "displayName": "Alerts", + "description": "Alert rules defined in the organization, with their current state and last evaluation outcome", + "tags": ["Alerts"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "../v2/{{dataSource.organization}}/alerts", + "getArgs": [{ "key": "folder", "value": "{{folder || null}}" }], + "postRequestScript": "alerts.js" + }, + "matches": "none", + "ui": [ + { + "type": "text", + "name": "folder", + "label": "Folder (optional)" + } + ], + "metadata": [ + { "name": "id", "displayName": "ID", "shape": "string" }, + { "name": "name", "displayName": "Name", "shape": "string", "role": "label" }, + { "name": "description", "displayName": "Description", "shape": "string" }, + { + "name": "level", + "displayName": "Level", + "shape": [ + "state", + { + "map": { + "success": ["ok"], + "warning": ["warning"], + "error": ["critical", "error", "firing"] + } + } + ] + }, + { + "name": "lastOutcome", + "displayName": "Last Outcome", + "shape": [ + "state", + { + "map": { + "success": ["normal"], + "error": ["error", "firing", "triggered"] + } + } + ] + }, + { "name": "enabled", "displayName": "Enabled", "shape": "boolean" }, + { "name": "priority", "displayName": "Priority", "shape": "number" }, + { "name": "alertType", "displayName": "Alert Type", "shape": "string" }, + { "name": "isRealTime", "displayName": "Real Time", "shape": "boolean" }, + { "name": "streamName", "displayName": "Stream", "shape": "string" }, + { "name": "streamType", "displayName": "Stream Type", "shape": "string" }, + { "name": "conditionType", "displayName": "Condition Type", "shape": "string" }, + { "name": "folderName", "displayName": "Folder", "shape": "string" }, + { "name": "owner", "displayName": "Owner", "shape": "string" }, + { "name": "tags", "displayName": "Tags", "shape": "string" }, + { "name": "levelSince", "displayName": "Level Since", "shape": "date" }, + { "name": "lastOutcomeAt", "displayName": "Last Evaluated", "shape": "date", "role": "timestamp" }, + { "name": "lastOutcomeSince", "displayName": "Last Outcome Since", "shape": "date" }, + { "name": "lastTriggeredAt", "displayName": "Last Triggered", "shape": "date" }, + { "name": "lastSatisfiedAt", "displayName": "Last Satisfied", "shape": "date" } + ], + "timeframes": false +} diff --git a/plugins/OpenObserve/v1/dataStreams/clusterInfo.json b/plugins/OpenObserve/v1/dataStreams/clusterInfo.json index 6686680a..e5ce8ee1 100644 --- a/plugins/OpenObserve/v1/dataStreams/clusterInfo.json +++ b/plugins/OpenObserve/v1/dataStreams/clusterInfo.json @@ -11,6 +11,7 @@ "errorHandling": { "type": "script", "script": "errorHandling/clusterInfo.js" } }, "matches": "none", + "visibility": { "type": "hidden" }, "metadata": [ { "name": "region", "displayName": "Region", "shape": "string" }, { "name": "node", "displayName": "Node", "shape": "string", "role": "label" }, diff --git a/plugins/OpenObserve/v1/dataStreams/incidentStats.json b/plugins/OpenObserve/v1/dataStreams/incidentStats.json index cd0c81f0..570726d5 100644 --- a/plugins/OpenObserve/v1/dataStreams/incidentStats.json +++ b/plugins/OpenObserve/v1/dataStreams/incidentStats.json @@ -11,6 +11,7 @@ "errorHandling": { "type": "script", "script": "errorHandling/incidentStats.js" } }, "matches": "none", + "visibility": { "type": "hidden" }, "metadata": [ { "name": "total_incidents", "displayName": "Total Incidents", "shape": "number" }, { "name": "open_incidents", "displayName": "Open Incidents", "shape": "number" }, diff --git a/plugins/OpenObserve/v1/dataStreams/incidents.json b/plugins/OpenObserve/v1/dataStreams/incidents.json index 787866c2..37bb676f 100644 --- a/plugins/OpenObserve/v1/dataStreams/incidents.json +++ b/plugins/OpenObserve/v1/dataStreams/incidents.json @@ -15,6 +15,7 @@ "pathToData": "incidents" }, "matches": "none", + "visibility": { "type": "hidden" }, "ui": [ { "type": "choiceChips", diff --git a/plugins/OpenObserve/v1/dataStreams/scripts/alerts.js b/plugins/OpenObserve/v1/dataStreams/scripts/alerts.js new file mode 100644 index 00000000..8e0f6096 --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/scripts/alerts.js @@ -0,0 +1,29 @@ +// dataStreams/scripts/alerts.js +// The v2 alerts list reports every timestamp in microseconds - convert to +// milliseconds here, keeping null (e.g. an alert never satisfied) as null. +// "tags" is an array, which can't be read via valueExpression even just to +// join it (see project memory), so that happens here too. +const usToMs = (us) => (us != null ? us / 1000 : null); + +result = (data.list || []).map((alert) => ({ + id: alert.alert_id, + name: alert.name, + description: alert.description || "", + level: alert.level, + lastOutcome: alert.last_outcome, + enabled: alert.enabled, + priority: alert.priority, + alertType: alert.alert_type, + isRealTime: alert.is_real_time, + streamName: alert.stream_name, + streamType: alert.stream_type, + conditionType: alert.condition ? alert.condition.type : null, + folderName: alert.folder_name, + owner: alert.owner || "", + tags: (alert.tags || []).join(", "), + levelSince: usToMs(alert.level_since), + lastOutcomeAt: usToMs(alert.last_outcome_at), + lastOutcomeSince: usToMs(alert.last_outcome_since), + lastTriggeredAt: usToMs(alert.last_triggered_at), + lastSatisfiedAt: usToMs(alert.last_satisfied_at), +})); diff --git a/plugins/OpenObserve/v1/defaultContent/overviewDashboard.dash.json b/plugins/OpenObserve/v1/defaultContent/overviewDashboard.dash.json index d38f40e5..41dbb56e 100644 --- a/plugins/OpenObserve/v1/defaultContent/overviewDashboard.dash.json +++ b/plugins/OpenObserve/v1/defaultContent/overviewDashboard.dash.json @@ -422,7 +422,7 @@ "description": "", "visualisation": { "config": { - "content": "Incidents", + "content": "Alerts", "fontSize": 16, "align": "center", "autoSize": true @@ -441,13 +441,13 @@ "z": 0, "config": { "_type": "tile/data-stream", - "title": "Incidents", + "title": "Alerts", "description": "", "timeframe": "none", "activePluginConfigIds": ["{{configId}}"], "dataStream": { - "id": "{{dataStreams.incidents}}", - "name": "incidents", + "id": "{{dataStreams.alerts}}", + "name": "alerts", "pluginConfigId": "{{configId}}", "group": { "by": [], "aggregate": [{ "type": "count" }] } }, @@ -457,7 +457,7 @@ "data-stream-scalar": { "value": "count", "comparisonColumn": "none", - "label": "Incidents" + "label": "Alerts" } } } @@ -474,23 +474,23 @@ "z": 0, "config": { "_type": "tile/data-stream", - "title": "Incidents", - "description": "Alert incidents in the organization, grouped from correlated alerts", + "title": "Alerts", + "description": "Alert rules in the organization, with their current state and last evaluation outcome", "timeframe": "none", "activePluginConfigIds": ["{{configId}}"], "dataStream": { - "id": "{{dataStreams.incidents}}", - "name": "incidents", + "id": "{{dataStreams.alerts}}", + "name": "alerts", "pluginConfigId": "{{configId}}", - "sort": { "by": [["created_at", "desc"]] } + "sort": { "by": [["lastOutcomeAt", "desc"]] } }, "visualisation": { "type": "data-stream-table", "config": { "data-stream-table": { "transpose": false, - "columnOrder": ["id", "title", "status", "severity", "alert_count", "created_at"], - "hiddenColumns": ["assigned_to", "updated_at", "first_alert_at", "last_alert_at", "resolved_at"] + "columnOrder": ["name", "level", "lastOutcome", "priority", "streamName", "lastOutcomeAt", "lastTriggeredAt"], + "hiddenColumns": ["id", "description", "enabled", "alertType", "isRealTime", "streamType", "conditionType", "folderName", "owner", "tags", "levelSince", "lastOutcomeSince", "lastSatisfiedAt"] } } } diff --git a/plugins/OpenObserve/v1/docs/README.md b/plugins/OpenObserve/v1/docs/README.md index bdfd570d..7eb820f3 100644 --- a/plugins/OpenObserve/v1/docs/README.md +++ b/plugins/OpenObserve/v1/docs/README.md @@ -1,6 +1,6 @@ -Monitor your self-hosted [OpenObserve](https://openobserve.ai) instance in SquaredUp — streams, log/metric/trace search, pipelines, incidents, SLOs, and cluster health — via the [OpenObserve API](https://openobserve.ai/docs/reference/api/). +Monitor your self-hosted [OpenObserve](https://openobserve.ai) instance in SquaredUp — streams, log/metric/trace search, pipelines, alerts, and SLOs — via the [OpenObserve API](https://openobserve.ai/docs/reference/api/). -> ⚠️ This plugin covers self-hosted OpenObserve only, and only the **Streams**, **Search**, **Cluster**, **Metrics**, **Traces**, **Pipelines**, **Incidents**, and **SLOs** API areas — it does not cover OpenObserve Cloud, ingestion, dashboards, reports, or user management. +> ⚠️ This plugin covers self-hosted OpenObserve only, and only the **Streams**, **Search**, **Metrics**, **Traces**, **Pipelines**, **Alerts**, and **SLOs** API areas — it does not cover OpenObserve Cloud, ingestion, dashboards, reports, or user management. ## Setup @@ -34,9 +34,8 @@ On save, the plugin validates these by fetching your organization's stream list; - **Metrics** — PromQL range queries against metrics ingested into OpenObserve, including a guided metric/label/value picker for building a query without writing PromQL. - **Traces** — recent trace spans within a time range, for an individual trace stream. - **Pipelines** — data-transformation pipelines and their execution history. -- **Incidents** — alert incidents (groups of correlated alerts) and summary statistics. +- **Alerts** — alert rules, with their current level and last evaluation outcome. - **SLOs** — Service Level Objectives, with current status (SLI, coverage, error budget remaining, burn rate). -- **Cluster health** — pending compaction jobs per region/node, for clustered OpenObserve deployments. The out-of-the-box dashboards include an org-wide **Overview** plus a **Stream** perspective for drilling into an individual stream. @@ -52,10 +51,9 @@ The out-of-the-box dashboards include an org-wide **Overview** plus a **Stream** - **Metric Names**, **Metric Labels**, **Label Values** — back the Simple Metrics Query picker's cascading dropdowns; hidden from the tile editor. - **Pipelines** — data-transformation pipelines defined in the organization, with node/edge counts and last error; account-wide. - **Pipeline History** — pipeline execution history within a time range; account-wide. -- **Incidents** — alert incidents, with status, severity, and timestamps; account-wide. -- **Incident Stats** — summary statistics for alert incidents; account-wide. May not be available on all self-hosted deployments (see Known limitations). +- **Alerts** — alert rules defined in the organization, with current level, last outcome, priority, source stream, and when each was last evaluated/triggered; account-wide. Optional **Folder** filter. - **SLOs** — Service Level Objectives with current status (SLI, coverage, error budget remaining, burn rate); account-wide. Optional **Folder** filter. -- **Cluster Info** — pending compaction jobs per region/node; account-wide. Only returns data on a clustered OpenObserve deployment (see Known limitations). +- **Incidents**, **Incident Stats**, **Cluster Info** — hidden from the tile editor. ## What gets indexed @@ -65,8 +63,6 @@ The out-of-the-box dashboards include an org-wide **Overview** plus a **Stream** ## Known limitations -- **Cluster Info requires a clustered deployment** — on a standalone single-node OpenObserve instance (the common self-hosted setup) this endpoint returns a 404; the plugin surfaces this as a clear message rather than raw error, and no default dashboard tile uses it. Clustered deployments can add their own tile. -- **Incident Stats may not be available on your deployment** — this endpoint returned "Not Supported" on the instance this plugin was tested against, likely an edition/license gate; the plugin surfaces this as a clear message, and no default dashboard tile uses it. - **Search and Logs Search require the stream name typed manually** — the SQL's `FROM` clause isn't auto-filled; default tiles ship with placeholder/example SQL that may need editing to match your streams. - **Very broad search timeframes may exceed the platform's response size limit** — narrow the timeframe or reduce **Max rows** if a Search, Logs Search, or Traces Latest tile fails to load. A Simple Metrics Query or Metrics Query with no label filter can hit the same limit on a high-cardinality metric over a wide timeframe — add a label filter or narrow the timeframe. - **Pipeline History's Query Took unit is inferred, not documented** — OpenObserve doesn't document the unit anywhere; it's shown as milliseconds, matching the `took` field's documented unit on the Search endpoint and the realistic magnitude seen in testing (single-digit ms for a lightweight scheduled query). Delay and Evaluation Took are shown as seconds, since OpenObserve names those fields explicitly (`delay_in_secs`, `evaluation_took_in_secs`). @@ -75,5 +71,6 @@ The out-of-the-box dashboards include an org-wide **Overview** plus a **Stream** - **Traces Latest's "View Trace" link format is unverified** — it's built from OpenObserve's documented `/web/traces/trace-details` route (`org_identifier`, `stream`, `trace_id`, `from`, `to` query params, with `from`/`to` in microseconds), but this plugin was built without being able to click through and confirm the link actually opens the right trace. If it doesn't resolve correctly, this is the first thing to check. - **Traces Latest's Bucket interval only buckets the traces actually fetched** — it doesn't change how many traces the API returns, only **Max rows**. On a high-volume trace stream, a short bucket width (e.g. `1m`) may need a correspondingly larger **Max rows** to see more than one or two buckets, which risks the response-size limit above — there's a real trade-off between bucket granularity and how much time the buckets actually cover. - **SLOs show blank status figures until first measured, or when stale** — SLI, burn rate, error budget remaining, and the watermark/measuring-since timestamps are intentionally blank (not zero) when **No Data** is true, matching OpenObserve's own UI — a brand-new or stalled SLO isn't the same as one measured at 0%. +- **Alerts show a blank Level until an alert has a recorded state** — an alert whose last evaluation errored can have no level yet; check **Last Outcome** for those. - **SLO per-group status isn't covered** — only the overall SLO list with its top-level status; drilling into a grouped SLO's per-group breakdown isn't implemented. - **Read-only** — the plugin never creates, modifies, or deletes anything in OpenObserve. diff --git a/plugins/OpenObserve/v1/metadata.json b/plugins/OpenObserve/v1/metadata.json index 85175faf..0d7b4d55 100644 --- a/plugins/OpenObserve/v1/metadata.json +++ b/plugins/OpenObserve/v1/metadata.json @@ -6,13 +6,13 @@ "name": "SquaredUp Labs", "type": "labs" }, - "description": "Monitor streams, run log search, and check functions and cluster health in your self-hosted OpenObserve instance.", + "description": "Monitor streams, alerts, pipelines, and SLOs, and search logs, metrics, and traces in your self-hosted OpenObserve instance.", "category": "Monitoring", "type": "hybrid", "schemaVersion": "2.1", "importNotSupported": false, "restrictedToPlatforms": [], - "keywords": ["observability", "logs", "log search", "openobserve", "self-hosted"], + "keywords": ["observability", "logs", "log search", "openobserve", "self-hosted", "alerts"], "objectTypes": ["OpenObserve Stream"], "links": [ { From 639b1f05e3eed3110758315d768b7530328a0c69 Mon Sep 17 00:00:00 2001 From: John Hayes Date: Tue, 27 Oct 2026 14:43:54 +0000 Subject: [PATCH 18/22] Retag hidden Cluster Info and Incidents streams Hidden streams still contribute their tags as headers in the data stream picker, leaving empty Cluster and Incidents groups. Fold Incidents and Incident Stats into Alerts, and Cluster Info into Streams. Co-Authored-By: Claude Opus 5.5 --- plugins/OpenObserve/v1/dataStreams/clusterInfo.json | 2 +- plugins/OpenObserve/v1/dataStreams/incidentStats.json | 2 +- plugins/OpenObserve/v1/dataStreams/incidents.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/OpenObserve/v1/dataStreams/clusterInfo.json b/plugins/OpenObserve/v1/dataStreams/clusterInfo.json index e5ce8ee1..e7f4b372 100644 --- a/plugins/OpenObserve/v1/dataStreams/clusterInfo.json +++ b/plugins/OpenObserve/v1/dataStreams/clusterInfo.json @@ -2,7 +2,7 @@ "name": "clusterInfo", "displayName": "Cluster Info", "description": "Pending compaction jobs per node, grouped by region, for the OpenObserve cluster", - "tags": ["Cluster"], + "tags": ["Streams"], "baseDataSourceName": "httpRequestUnscoped", "config": { "httpMethod": "get", diff --git a/plugins/OpenObserve/v1/dataStreams/incidentStats.json b/plugins/OpenObserve/v1/dataStreams/incidentStats.json index 570726d5..c99d96c5 100644 --- a/plugins/OpenObserve/v1/dataStreams/incidentStats.json +++ b/plugins/OpenObserve/v1/dataStreams/incidentStats.json @@ -2,7 +2,7 @@ "name": "incidentStats", "displayName": "Incident Stats", "description": "Summary statistics for alert incidents in the organization", - "tags": ["Incidents"], + "tags": ["Alerts"], "baseDataSourceName": "httpRequestUnscoped", "config": { "httpMethod": "get", diff --git a/plugins/OpenObserve/v1/dataStreams/incidents.json b/plugins/OpenObserve/v1/dataStreams/incidents.json index 37bb676f..4bf76bc4 100644 --- a/plugins/OpenObserve/v1/dataStreams/incidents.json +++ b/plugins/OpenObserve/v1/dataStreams/incidents.json @@ -2,7 +2,7 @@ "name": "incidents", "displayName": "Incidents", "description": "Alert incidents in the organization, groups of correlated alerts", - "tags": ["Incidents"], + "tags": ["Alerts"], "baseDataSourceName": "httpRequestUnscoped", "config": { "httpMethod": "get", From 62f2611db027c07936223f91a6649e1ad820d45e Mon Sep 17 00:00:00 2001 From: John Hayes Date: Tue, 27 Oct 2026 18:40:21 +0000 Subject: [PATCH 19/22] Fix Pipelines Last Error formatting and Paused At unit last_error is absent on healthy pipelines, and the null failed to format in its json-shaped column. Make it a string column: empty when absent, the text itself if a string, compact JSON otherwise. paused_at is in microseconds, so convert it and show it as a date. Co-Authored-By: Claude Opus 5.5 --- plugins/OpenObserve/v1/dataStreams/pipelines.json | 4 ++-- .../OpenObserve/v1/dataStreams/scripts/pipelines.js | 10 ++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/plugins/OpenObserve/v1/dataStreams/pipelines.json b/plugins/OpenObserve/v1/dataStreams/pipelines.json index e36ff222..6ea78098 100644 --- a/plugins/OpenObserve/v1/dataStreams/pipelines.json +++ b/plugins/OpenObserve/v1/dataStreams/pipelines.json @@ -16,8 +16,8 @@ { "name": "description", "displayName": "Description", "shape": "string" }, { "name": "enabled", "displayName": "Enabled", "shape": "boolean" }, { "name": "version", "displayName": "Version", "shape": "number" }, - { "name": "paused_at", "displayName": "Paused At", "shape": "number" }, - { "name": "last_error", "displayName": "Last Error", "shape": "json" }, + { "name": "paused_at", "displayName": "Paused At", "shape": "date" }, + { "name": "last_error", "displayName": "Last Error", "shape": "string" }, { "name": "source", "displayName": "Source", "shape": "json" }, { "name": "sourceStreamName", "displayName": "Source Stream Name", "shape": "string" }, { "name": "sourceStreamType", "displayName": "Source Stream Type", "shape": "string" }, diff --git a/plugins/OpenObserve/v1/dataStreams/scripts/pipelines.js b/plugins/OpenObserve/v1/dataStreams/scripts/pipelines.js index 077bea72..ec2ffe25 100644 --- a/plugins/OpenObserve/v1/dataStreams/scripts/pipelines.js +++ b/plugins/OpenObserve/v1/dataStreams/scripts/pipelines.js @@ -3,6 +3,12 @@ // ("[object Array]" or a JSON-text blob), so .length there counts characters, not // elements. Compute real counts here, and stringify the nested json-shaped fields // so they render as JSON rather than "[object Object]". +// last_error is absent on healthy pipelines and its shape is undocumented, so +// it's a plain string column (a null in a json-shaped column fails to format): +// empty when absent, the text itself if a string, compact JSON otherwise. +// paused_at is in microseconds, like the other v2 timestamps. +const errorText = (err) => (err == null ? "" : typeof err === "string" ? err : JSON.stringify(err)); + result = (data.list || []).map((p) => { const nodes = p.nodes || []; const edges = p.edges || []; @@ -12,8 +18,8 @@ result = (data.list || []).map((p) => { description: p.description || "", enabled: p.enabled, version: p.version, - paused_at: p.paused_at != null ? p.paused_at : null, - last_error: p.last_error != null ? JSON.stringify(p.last_error) : null, + paused_at: p.paused_at != null ? p.paused_at / 1000 : null, + last_error: errorText(p.last_error), source: p.source != null ? JSON.stringify(p.source) : null, sourceStreamName: (p.source && p.source.stream_name) || "", sourceStreamType: (p.source && p.source.stream_type) || "", From 39c85d951f6a1886cdca8224d47726456d9c9bb8 Mon Sep 17 00:00:00 2001 From: John Hayes Date: Tue, 27 Oct 2026 19:11:38 +0000 Subject: [PATCH 20/22] Fix Pipeline History columns that failed to format A column that is null in every row fails to convert to its shape in the UI. Coerce Error to "" on successful runs, and drop Delay and Evaluation Took, which were never populated (no honest stand-in value). The Overview dashboard's Pipeline History table shows Query Took in Delay's place, and the README notes the omission. Co-Authored-By: Claude Opus 5.5 --- plugins/OpenObserve/v1/dataStreams/pipelineHistory.json | 2 -- .../OpenObserve/v1/dataStreams/scripts/pipelineHistory.js | 5 +++++ .../v1/defaultContent/overviewDashboard.dash.json | 4 ++-- plugins/OpenObserve/v1/docs/README.md | 3 ++- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/plugins/OpenObserve/v1/dataStreams/pipelineHistory.json b/plugins/OpenObserve/v1/dataStreams/pipelineHistory.json index ba70284c..fa5cdcd9 100644 --- a/plugins/OpenObserve/v1/dataStreams/pipelineHistory.json +++ b/plugins/OpenObserve/v1/dataStreams/pipelineHistory.json @@ -48,9 +48,7 @@ { "name": "is_realtime", "displayName": "Is Realtime", "shape": "boolean" }, { "name": "is_silenced", "displayName": "Is Silenced", "shape": "boolean" }, { "name": "retries", "displayName": "Retries", "shape": "number" }, - { "name": "delay_in_secs", "displayName": "Delay", "shape": "seconds" }, { "name": "error", "displayName": "Error", "shape": "string" }, - { "name": "evaluation_took_in_secs", "displayName": "Evaluation Took", "shape": "seconds" }, { "name": "query_took", "displayName": "Query Took (ms, estimated)", "shape": "milliseconds" }, { "name": "source_node", "displayName": "Source Node", "shape": "string" } ], diff --git a/plugins/OpenObserve/v1/dataStreams/scripts/pipelineHistory.js b/plugins/OpenObserve/v1/dataStreams/scripts/pipelineHistory.js index e1ce1a8b..9802de81 100644 --- a/plugins/OpenObserve/v1/dataStreams/scripts/pipelineHistory.js +++ b/plugins/OpenObserve/v1/dataStreams/scripts/pipelineHistory.js @@ -4,7 +4,12 @@ // class of bug fixed in tracesLatest's trace_id link. Compute it as a real // field here instead, matching the proven pattern used elsewhere in this // plugin. +// "error" is null on every successful run, and a column that's null in every +// row fails to convert to its shape ("Failed to convert to string"), so it's +// coerced to "". delay_in_secs/evaluation_took_in_secs were always null on the +// instance tested (no honest stand-in value), so they aren't exposed. result = (data.hits || []).map((entry) => ({ ...entry, timestamp: entry.timestamp / 1000, + error: entry.error == null ? "" : entry.error, })); diff --git a/plugins/OpenObserve/v1/defaultContent/overviewDashboard.dash.json b/plugins/OpenObserve/v1/defaultContent/overviewDashboard.dash.json index 41dbb56e..adedb6bc 100644 --- a/plugins/OpenObserve/v1/defaultContent/overviewDashboard.dash.json +++ b/plugins/OpenObserve/v1/defaultContent/overviewDashboard.dash.json @@ -400,8 +400,8 @@ "config": { "data-stream-table": { "transpose": false, - "columnOrder": ["pipeline_name", "status", "timestamp", "retries", "delay_in_secs", "error"], - "hiddenColumns": ["is_realtime", "is_silenced", "evaluation_took_in_secs", "query_took", "source_node"] + "columnOrder": ["pipeline_name", "status", "timestamp", "retries", "query_took", "error"], + "hiddenColumns": ["is_realtime", "is_silenced", "source_node"] } } } diff --git a/plugins/OpenObserve/v1/docs/README.md b/plugins/OpenObserve/v1/docs/README.md index 7eb820f3..f6e0a16b 100644 --- a/plugins/OpenObserve/v1/docs/README.md +++ b/plugins/OpenObserve/v1/docs/README.md @@ -65,7 +65,8 @@ The out-of-the-box dashboards include an org-wide **Overview** plus a **Stream** - **Search and Logs Search require the stream name typed manually** — the SQL's `FROM` clause isn't auto-filled; default tiles ship with placeholder/example SQL that may need editing to match your streams. - **Very broad search timeframes may exceed the platform's response size limit** — narrow the timeframe or reduce **Max rows** if a Search, Logs Search, or Traces Latest tile fails to load. A Simple Metrics Query or Metrics Query with no label filter can hit the same limit on a high-cardinality metric over a wide timeframe — add a label filter or narrow the timeframe. -- **Pipeline History's Query Took unit is inferred, not documented** — OpenObserve doesn't document the unit anywhere; it's shown as milliseconds, matching the `took` field's documented unit on the Search endpoint and the realistic magnitude seen in testing (single-digit ms for a lightweight scheduled query). Delay and Evaluation Took are shown as seconds, since OpenObserve names those fields explicitly (`delay_in_secs`, `evaluation_took_in_secs`). +- **Pipeline History's Query Took unit is inferred, not documented** — OpenObserve doesn't document the unit anywhere; it's shown as milliseconds, matching the `took` field's documented unit on the Search endpoint and the realistic magnitude seen in testing (single-digit ms for a lightweight scheduled query). +- **Pipeline History doesn't include Delay or Evaluation Took** — OpenObserve's history API has `delay_in_secs` and `evaluation_took_in_secs` fields, but they were empty on every run of the instance this plugin was tested against, so they aren't exposed as columns. - **Traces Latest only applies to trace-type streams** — it isn't included on the Stream perspective dashboard (which covers all three stream types); add it manually on a dashboard scoped to a trace stream. - **Traces Latest is fixed to the last hour** — `/traces/latest` runs a `GROUP BY trace_id` aggregation over every span in the chosen range before applying **Max rows**, so the scan cost scales with the timeframe, not the row limit. A wide range (e.g. a full day) can scan gigabytes of data and exhaust the server's memory. Restricted to `last1hour` to keep this safe by default. - **Traces Latest's "View Trace" link format is unverified** — it's built from OpenObserve's documented `/web/traces/trace-details` route (`org_identifier`, `stream`, `trace_id`, `from`, `to` query params, with `from`/`to` in microseconds), but this plugin was built without being able to click through and confirm the link actually opens the right trace. If it doesn't resolve correctly, this is the first thing to check. From 8885897d283515921c59201740cdfdd2b8a1073c Mon Sep 17 00:00:00 2001 From: John Hayes Date: Wed, 4 Nov 2026 23:48:08 +0000 Subject: [PATCH 21/22] Replace test server IP with a generic example URL The URL field's placeholder and the README example used the IP of the instance the plugin was tested against. Co-Authored-By: Claude Opus 5.5 --- plugins/OpenObserve/v1/docs/README.md | 2 +- plugins/OpenObserve/v1/ui.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/OpenObserve/v1/docs/README.md b/plugins/OpenObserve/v1/docs/README.md index f6e0a16b..8a3a1b97 100644 --- a/plugins/OpenObserve/v1/docs/README.md +++ b/plugins/OpenObserve/v1/docs/README.md @@ -19,7 +19,7 @@ You will need your OpenObserve instance's **URL**, **port**, **organization iden | Field | What it is | Where to find it | Required | | ----- | ---------- | ----------------- | -------- | -| **URL** | The base URL of your OpenObserve instance, including the scheme (e.g. `http://51.8.18.48`) — no trailing slash or port. | The address you use to sign in to OpenObserve. | Yes | +| **URL** | The base URL of your OpenObserve instance, including the scheme (e.g. `http://openobserve.example.com`) — no trailing slash or port. | The address you use to sign in to OpenObserve. | Yes | | **Port** | The port your OpenObserve instance listens on. | Defaults to `5080`, OpenObserve's standard port. | Yes | | **Organization** | The organization identifier used in every API call. | Defaults to `default`; check **IAM → Organizations** if you use a different one. | Yes | | **API Key** | The full base64-encoded `username:password`/`username:token` pair, sent unmodified as `Authorization: Basic `. | Built from a Service Account's login identity + token, or a user's email + password (see Setup step 4). | Yes | diff --git a/plugins/OpenObserve/v1/ui.json b/plugins/OpenObserve/v1/ui.json index 39356ce8..cccfd7f7 100644 --- a/plugins/OpenObserve/v1/ui.json +++ b/plugins/OpenObserve/v1/ui.json @@ -3,7 +3,7 @@ "type": "url", "name": "url", "label": "URL", - "placeholder": "http://51.8.18.48", + "placeholder": "http://openobserve.example.com", "help": "The base URL of your self-hosted OpenObserve instance, **including the scheme** (`http://` or `https://`), without a trailing slash or port — the port is set separately below.", "validation": { "required": true From 4e42744cf7d9b65e6ba0ee7e38d62517cce8170e Mon Sep 17 00:00:00 2001 From: John Hayes Date: Fri, 6 Nov 2026 14:49:11 +0000 Subject: [PATCH 22/22] Address CodeRabbit review on OpenObserve plugin - Emit all timestamps as ISO 8601 strings - Add a series column so the metrics graph splits per series - Move search's converted time to recordTime to avoid key collisions - Escape Simple Metrics Query values for PromQL regex matching - Prefix colliding metric labels with label_ - Rename the Stream source type without the plugin prefix - Reject URLs with a path or port, and fix the auth error message Co-Authored-By: Claude Opus 5.5 --- plugins/OpenObserve/v1/configValidation.json | 2 +- plugins/OpenObserve/v1/custom_types.json | 2 +- .../OpenObserve/v1/dataStreams/incidents.json | 10 +++---- .../v1/dataStreams/logsSearch.json | 2 +- .../v1/dataStreams/metricsQuery.json | 1 + .../v1/dataStreams/scripts/alerts.js | 14 +++++----- .../v1/dataStreams/scripts/metricsQuery.js | 28 +++++++++++++++---- .../v1/dataStreams/scripts/pipelineHistory.js | 2 +- .../v1/dataStreams/scripts/pipelines.js | 4 +-- .../v1/dataStreams/scripts/search.js | 9 ++++-- .../v1/dataStreams/scripts/slos.js | 9 ++++-- .../v1/dataStreams/scripts/tracesLatest.js | 9 ++++-- .../OpenObserve/v1/dataStreams/search.json | 4 +-- .../v1/dataStreams/simpleMetricsQuery.json | 7 ++++- .../v1/dataStreams/tracesLatest.json | 2 +- .../overviewDashboard.dash.json | 4 +-- .../defaultContent/streamDashboard.dash.json | 2 +- .../v1/indexDefinitions/default.json | 6 ++-- plugins/OpenObserve/v1/metadata.json | 2 +- plugins/OpenObserve/v1/scopes.json | 2 +- plugins/OpenObserve/v1/ui.json | 6 +++- 21 files changed, 81 insertions(+), 46 deletions(-) diff --git a/plugins/OpenObserve/v1/configValidation.json b/plugins/OpenObserve/v1/configValidation.json index f6211c7d..04a960eb 100644 --- a/plugins/OpenObserve/v1/configValidation.json +++ b/plugins/OpenObserve/v1/configValidation.json @@ -4,7 +4,7 @@ "displayName": "Authenticate", "dataStream": { "name": "streams" }, "required": true, - "error": "Could not authenticate. Check your region, organization, email, and password/token.", + "error": "Could not authenticate. Check your URL, port, organization, and API Key.", "success": "Connected successfully." } ] diff --git a/plugins/OpenObserve/v1/custom_types.json b/plugins/OpenObserve/v1/custom_types.json index 20930744..4d6ec7ed 100644 --- a/plugins/OpenObserve/v1/custom_types.json +++ b/plugins/OpenObserve/v1/custom_types.json @@ -1,7 +1,7 @@ [ { "name": "OpenObserve Stream", - "sourceType": "OpenObserve Stream", + "sourceType": "Stream", "icon": "stream", "singular": "Stream", "plural": "Streams" diff --git a/plugins/OpenObserve/v1/dataStreams/incidents.json b/plugins/OpenObserve/v1/dataStreams/incidents.json index 4bf76bc4..d6174787 100644 --- a/plugins/OpenObserve/v1/dataStreams/incidents.json +++ b/plugins/OpenObserve/v1/dataStreams/incidents.json @@ -66,31 +66,31 @@ "displayName": "Created At", "shape": "date", "role": "timestamp", - "valueExpression": "{{ $['created_at'] / 1000 }}" + "valueExpression": "{{ new Date($['created_at'] / 1000).toISOString() }}" }, { "name": "updated_at", "displayName": "Updated At", "shape": "date", - "valueExpression": "{{ $['updated_at'] / 1000 }}" + "valueExpression": "{{ new Date($['updated_at'] / 1000).toISOString() }}" }, { "name": "first_alert_at", "displayName": "First Alert At", "shape": "date", - "valueExpression": "{{ $['first_alert_at'] / 1000 }}" + "valueExpression": "{{ new Date($['first_alert_at'] / 1000).toISOString() }}" }, { "name": "last_alert_at", "displayName": "Last Alert At", "shape": "date", - "valueExpression": "{{ $['last_alert_at'] / 1000 }}" + "valueExpression": "{{ new Date($['last_alert_at'] / 1000).toISOString() }}" }, { "name": "resolved_at", "displayName": "Resolved At", "shape": "date", - "valueExpression": "{{ $['resolved_at'] ? $['resolved_at'] / 1000 : null }}" + "valueExpression": "{{ $['resolved_at'] ? new Date($['resolved_at'] / 1000).toISOString() : null }}" } ], "timeframes": false diff --git a/plugins/OpenObserve/v1/dataStreams/logsSearch.json b/plugins/OpenObserve/v1/dataStreams/logsSearch.json index c7b437b9..7225a2a1 100644 --- a/plugins/OpenObserve/v1/dataStreams/logsSearch.json +++ b/plugins/OpenObserve/v1/dataStreams/logsSearch.json @@ -39,7 +39,7 @@ ], "metadata": [ { - "name": "timestamp", + "name": "recordTime", "displayName": "Timestamp", "role": "timestamp", "shape": "date" diff --git a/plugins/OpenObserve/v1/dataStreams/metricsQuery.json b/plugins/OpenObserve/v1/dataStreams/metricsQuery.json index 3c7f08fe..7fefc7df 100644 --- a/plugins/OpenObserve/v1/dataStreams/metricsQuery.json +++ b/plugins/OpenObserve/v1/dataStreams/metricsQuery.json @@ -34,6 +34,7 @@ ], "metadata": [ { "name": "name", "displayName": "Name", "shape": "string", "role": "label" }, + { "name": "series", "displayName": "Series", "shape": "string" }, { "name": "timestamp", "displayName": "Timestamp", "shape": "date", "role": "timestamp" }, { "name": "value", "displayName": "Value", "shape": "number" }, { "pattern": ".*" } diff --git a/plugins/OpenObserve/v1/dataStreams/scripts/alerts.js b/plugins/OpenObserve/v1/dataStreams/scripts/alerts.js index 8e0f6096..36095f59 100644 --- a/plugins/OpenObserve/v1/dataStreams/scripts/alerts.js +++ b/plugins/OpenObserve/v1/dataStreams/scripts/alerts.js @@ -1,9 +1,9 @@ // dataStreams/scripts/alerts.js // The v2 alerts list reports every timestamp in microseconds - convert to -// milliseconds here, keeping null (e.g. an alert never satisfied) as null. +// ISO 8601 here, keeping null (e.g. an alert never satisfied) as null. // "tags" is an array, which can't be read via valueExpression even just to // join it (see project memory), so that happens here too. -const usToMs = (us) => (us != null ? us / 1000 : null); +const usToIso = (us) => (us != null ? new Date(us / 1000).toISOString() : null); result = (data.list || []).map((alert) => ({ id: alert.alert_id, @@ -21,9 +21,9 @@ result = (data.list || []).map((alert) => ({ folderName: alert.folder_name, owner: alert.owner || "", tags: (alert.tags || []).join(", "), - levelSince: usToMs(alert.level_since), - lastOutcomeAt: usToMs(alert.last_outcome_at), - lastOutcomeSince: usToMs(alert.last_outcome_since), - lastTriggeredAt: usToMs(alert.last_triggered_at), - lastSatisfiedAt: usToMs(alert.last_satisfied_at), + levelSince: usToIso(alert.level_since), + lastOutcomeAt: usToIso(alert.last_outcome_at), + lastOutcomeSince: usToIso(alert.last_outcome_since), + lastTriggeredAt: usToIso(alert.last_triggered_at), + lastSatisfiedAt: usToIso(alert.last_satisfied_at), })); diff --git a/plugins/OpenObserve/v1/dataStreams/scripts/metricsQuery.js b/plugins/OpenObserve/v1/dataStreams/scripts/metricsQuery.js index 9a3262f8..16e70cdb 100644 --- a/plugins/OpenObserve/v1/dataStreams/scripts/metricsQuery.js +++ b/plugins/OpenObserve/v1/dataStreams/scripts/metricsQuery.js @@ -1,13 +1,29 @@ // dataStreams/scripts/metricsQuery.js // Spread each series' labels as individual top-level columns (named exactly // as the label) instead of one combined JSON blob, with the reserved -// "__name__" label pulled out separately as "name". -result = ((data.data && data.data.result) || []).flatMap((series) => { - const { __name__, ...labels } = series.metric || {}; - return (series.values || []).map(([ts, val]) => ({ +// "__name__" label pulled out separately as "name". A label that would +// collide with one of this stream's own columns is kept as "label_" +// instead of overwriting it (cAdvisor metrics, for one, carry a "name" +// label). "series" identifies each distinct label set - PromQL-style, e.g. +// up{instance="a", job="b"} - for a line graph to split on. +const RESERVED = ["name", "series", "timestamp", "value"]; + +result = ((data.data && data.data.result) || []).flatMap((s) => { + const { __name__, ...labels } = s.metric || {}; + const labelColumns = {}; + for (const [key, val] of Object.entries(labels)) { + labelColumns[RESERVED.includes(key) ? `label_${key}` : key] = val; + } + const labelSet = Object.keys(labels) + .sort() + .map((key) => `${key}="${labels[key]}"`) + .join(", "); + const series = (__name__ || "") + (labelSet ? `{${labelSet}}` : ""); + return (s.values || []).map(([ts, val]) => ({ + ...labelColumns, name: __name__ || "", - ...labels, - timestamp: ts * 1000, + series, + timestamp: new Date(ts * 1000).toISOString(), value: parseFloat(val), })); }); diff --git a/plugins/OpenObserve/v1/dataStreams/scripts/pipelineHistory.js b/plugins/OpenObserve/v1/dataStreams/scripts/pipelineHistory.js index 9802de81..570c99b4 100644 --- a/plugins/OpenObserve/v1/dataStreams/scripts/pipelineHistory.js +++ b/plugins/OpenObserve/v1/dataStreams/scripts/pipelineHistory.js @@ -10,6 +10,6 @@ // instance tested (no honest stand-in value), so they aren't exposed. result = (data.hits || []).map((entry) => ({ ...entry, - timestamp: entry.timestamp / 1000, + timestamp: new Date(entry.timestamp / 1000).toISOString(), error: entry.error == null ? "" : entry.error, })); diff --git a/plugins/OpenObserve/v1/dataStreams/scripts/pipelines.js b/plugins/OpenObserve/v1/dataStreams/scripts/pipelines.js index ec2ffe25..74c5cab8 100644 --- a/plugins/OpenObserve/v1/dataStreams/scripts/pipelines.js +++ b/plugins/OpenObserve/v1/dataStreams/scripts/pipelines.js @@ -6,7 +6,7 @@ // last_error is absent on healthy pipelines and its shape is undocumented, so // it's a plain string column (a null in a json-shaped column fails to format): // empty when absent, the text itself if a string, compact JSON otherwise. -// paused_at is in microseconds, like the other v2 timestamps. +// paused_at is in microseconds, like the other v2 timestamps - converted to ISO 8601. const errorText = (err) => (err == null ? "" : typeof err === "string" ? err : JSON.stringify(err)); result = (data.list || []).map((p) => { @@ -18,7 +18,7 @@ result = (data.list || []).map((p) => { description: p.description || "", enabled: p.enabled, version: p.version, - paused_at: p.paused_at != null ? p.paused_at / 1000 : null, + paused_at: p.paused_at != null ? new Date(p.paused_at / 1000).toISOString() : null, last_error: errorText(p.last_error), source: p.source != null ? JSON.stringify(p.source) : null, sourceStreamName: (p.source && p.source.stream_name) || "", diff --git a/plugins/OpenObserve/v1/dataStreams/scripts/search.js b/plugins/OpenObserve/v1/dataStreams/scripts/search.js index 58648c76..b8e13a4c 100644 --- a/plugins/OpenObserve/v1/dataStreams/scripts/search.js +++ b/plugins/OpenObserve/v1/dataStreams/scripts/search.js @@ -3,9 +3,12 @@ // A row's "_timestamp" field (leading underscore) is invisible to // metadata valueExpression lookups ($['_timestamp'] / $._timestamp both // resolve to undefined even though the key is present in the raw row and -// declared in metadata) - so the microseconds -> milliseconds conversion -// for the date-shaped timestamp column has to happen here instead. +// declared in metadata) - so the microseconds -> ISO 8601 conversion for the +// date-shaped column has to happen here instead. It's written to +// "recordTime" rather than "timestamp" so it can't overwrite a column the +// user's SQL selects as "timestamp", and stays null when the query doesn't +// return "_timestamp" (e.g. an aggregate like SELECT count(*)). result = (data.hits || []).map((row) => ({ ...row, - timestamp: row._timestamp / 1000, + recordTime: row._timestamp != null ? new Date(row._timestamp / 1000).toISOString() : null, })); diff --git a/plugins/OpenObserve/v1/dataStreams/scripts/slos.js b/plugins/OpenObserve/v1/dataStreams/scripts/slos.js index b7f87c77..8cd86d82 100644 --- a/plugins/OpenObserve/v1/dataStreams/scripts/slos.js +++ b/plugins/OpenObserve/v1/dataStreams/scripts/slos.js @@ -6,6 +6,9 @@ // valueExpression even just to join them (see project memory), so that // happens here. "config" is a free-form object shaped by sli_type - kept as // a JSON blob rather than modelled field-by-field. +// computed_at/watermark_end/measuring_since are Unix seconds. +const secsToIso = (secs) => (secs != null ? new Date(secs * 1000).toISOString() : null); + result = (data.list || []).map((slo) => { const status = slo.status; return { @@ -31,9 +34,9 @@ result = (data.list || []).map((slo) => { burnRate: status ? status.burn_rate : null, errorBudgetRemaining: status ? status.error_budget_remaining : null, timeToExhaustSecs: status ? status.time_to_exhaust_secs : null, - computedAt: status && status.computed_at != null ? status.computed_at * 1000 : null, - watermarkEnd: status && status.watermark_end != null ? status.watermark_end * 1000 : null, - measuringSince: status && status.measuring_since != null ? status.measuring_since * 1000 : null, + computedAt: status ? secsToIso(status.computed_at) : null, + watermarkEnd: status ? secsToIso(status.watermark_end) : null, + measuringSince: status ? secsToIso(status.measuring_since) : null, staleWatermark: status ? status.stale_watermark : null, }; }); diff --git a/plugins/OpenObserve/v1/dataStreams/scripts/tracesLatest.js b/plugins/OpenObserve/v1/dataStreams/scripts/tracesLatest.js index 6cbf0bc0..501aa52f 100644 --- a/plugins/OpenObserve/v1/dataStreams/scripts/tracesLatest.js +++ b/plugins/OpenObserve/v1/dataStreams/scripts/tracesLatest.js @@ -20,6 +20,9 @@ // editor's native Group-by buckets no finer than byHour, so an optional // "bucketInterval" param (e.g. "5m") switches this stream to pre-aggregated // rows - one per fixed-width time window - computed here instead. +// +// start_time/end_time are nanoseconds: / 1e6 gives milliseconds, the same +// unit as bucketMs, then dates are emitted as ISO 8601. const ds = (context.dataSources && context.dataSources[0]) || {}; const webBase = [ds.url, ds.port].filter(Boolean).join(":"); const streamName = (context.objects && context.objects[0] && context.objects[0].name) || ""; @@ -44,7 +47,7 @@ if (bucketMs) { result = Array.from(buckets.entries()) .sort((a, b) => a[0] - b[0]) .map(([bucketStart, durations]) => ({ - startTime: bucketStart, + startTime: new Date(bucketStart).toISOString(), traceCount: durations.length, avgDurationMs: durations.reduce((a, b) => a + b, 0) / durations.length, minDurationMs: Math.min(...durations), @@ -57,8 +60,8 @@ if (bucketMs) { const toUs = Math.floor(row.end_time / 1000); return { ...rest, - startTime: row.start_time / 1000000, - endTime: row.end_time / 1000000, + startTime: new Date(row.start_time / 1000000).toISOString(), + endTime: new Date(row.end_time / 1000000).toISOString(), durationMs: row.duration / 1000, spanCount: (spans && spans[0]) || 0, serviceCount: (service_name || []).length, diff --git a/plugins/OpenObserve/v1/dataStreams/search.json b/plugins/OpenObserve/v1/dataStreams/search.json index 484bf0d7..a03e6e59 100644 --- a/plugins/OpenObserve/v1/dataStreams/search.json +++ b/plugins/OpenObserve/v1/dataStreams/search.json @@ -20,7 +20,7 @@ }, "postRequestScript": "search.js" }, - "matches": { "sourceType": { "type": "oneOf", "values": ["OpenObserve Stream"] } }, + "matches": { "sourceType": { "type": "oneOf", "values": ["Stream"] } }, "ui": [ { "type": "textarea", @@ -39,7 +39,7 @@ ], "metadata": [ { - "name": "timestamp", + "name": "recordTime", "displayName": "Timestamp", "role": "timestamp", "shape": "date" diff --git a/plugins/OpenObserve/v1/dataStreams/simpleMetricsQuery.json b/plugins/OpenObserve/v1/dataStreams/simpleMetricsQuery.json index 863129b2..9578b095 100644 --- a/plugins/OpenObserve/v1/dataStreams/simpleMetricsQuery.json +++ b/plugins/OpenObserve/v1/dataStreams/simpleMetricsQuery.json @@ -12,7 +12,7 @@ "getArgs": [ { "key": "query", - "value": "{{ (aggregation && aggregation !== 'none' ? aggregation + '(' : '') + metric + (label && values && values.length ? '{' + label + '=~\"' + values.join('|') + '\"}' : '') + (aggregation && aggregation !== 'none' ? ')' : '') }}" + "value": "{{ (aggregation && aggregation !== 'none' ? aggregation + '(' : '') + metric + (label && values && values.length ? '{' + label + '=~\"' + values.map((v) => String(v).split('').map((c) => c === String.fromCharCode(92) ? String.fromCharCode(92, 92, 92, 92) : '.*+?^$(){}|[]'.includes(c) ? String.fromCharCode(92, 92) + c : c === '\"' ? String.fromCharCode(92) + c : c).join('')).join('|') + '\"}' : '') + (aggregation && aggregation !== 'none' ? ')' : '') }}" }, { "key": "start", @@ -135,6 +135,11 @@ "shape": "string", "role": "label" }, + { + "name": "series", + "displayName": "Series", + "shape": "string" + }, { "name": "timestamp", "displayName": "Timestamp", diff --git a/plugins/OpenObserve/v1/dataStreams/tracesLatest.json b/plugins/OpenObserve/v1/dataStreams/tracesLatest.json index 27c9e2be..c9fab9c3 100644 --- a/plugins/OpenObserve/v1/dataStreams/tracesLatest.json +++ b/plugins/OpenObserve/v1/dataStreams/tracesLatest.json @@ -17,7 +17,7 @@ "postRequestScript": "tracesLatest.js" }, "matches": { - "sourceType": { "type": "oneOf", "values": ["OpenObserve Stream"] }, + "sourceType": { "type": "oneOf", "values": ["Stream"] }, "stream_type": { "type": "equals", "value": "traces" } }, "ui": [ diff --git a/plugins/OpenObserve/v1/defaultContent/overviewDashboard.dash.json b/plugins/OpenObserve/v1/defaultContent/overviewDashboard.dash.json index adedb6bc..99eecd6a 100644 --- a/plugins/OpenObserve/v1/defaultContent/overviewDashboard.dash.json +++ b/plugins/OpenObserve/v1/defaultContent/overviewDashboard.dash.json @@ -546,7 +546,7 @@ "data-stream-line-graph": { "xAxisColumn": "timestamp", "yAxisColumn": ["value"], - "seriesColumn": "labels", + "seriesColumn": "series", "showLegend": true, "legendPosition": "bottom", "yAxisLabel": "Value", @@ -602,7 +602,7 @@ "sql": "SELECT * FROM \"default\" LIMIT 50", "size": 50 }, - "sort": { "by": [["timestamp", "desc"]] } + "sort": { "by": [["recordTime", "desc"]] } }, "visualisation": { "type": "data-stream-table", diff --git a/plugins/OpenObserve/v1/defaultContent/streamDashboard.dash.json b/plugins/OpenObserve/v1/defaultContent/streamDashboard.dash.json index f32292a8..5225cca3 100644 --- a/plugins/OpenObserve/v1/defaultContent/streamDashboard.dash.json +++ b/plugins/OpenObserve/v1/defaultContent/streamDashboard.dash.json @@ -72,7 +72,7 @@ "sql": "SELECT * FROM \"placeholder\" LIMIT 50", "size": 50 }, - "sort": { "by": [["timestamp", "desc"]] } + "sort": { "by": [["recordTime", "desc"]] } }, "scope": { "scope": "{{scopes.[Streams]}}", diff --git a/plugins/OpenObserve/v1/indexDefinitions/default.json b/plugins/OpenObserve/v1/indexDefinitions/default.json index 780815b9..38313e6c 100644 --- a/plugins/OpenObserve/v1/indexDefinitions/default.json +++ b/plugins/OpenObserve/v1/indexDefinitions/default.json @@ -7,7 +7,7 @@ "objectMapping": { "id": "sourceId", "name": "name", - "type": { "value": "OpenObserve Stream" }, + "type": { "value": "Stream" }, "properties": [ "stream_type", { "docCount": "stats.doc_num" }, @@ -23,7 +23,7 @@ "objectMapping": { "id": "sourceId", "name": "name", - "type": { "value": "OpenObserve Stream" }, + "type": { "value": "Stream" }, "properties": [ "stream_type", { "docCount": "stats.doc_num" }, @@ -39,7 +39,7 @@ "objectMapping": { "id": "sourceId", "name": "name", - "type": { "value": "OpenObserve Stream" }, + "type": { "value": "Stream" }, "properties": [ "stream_type", { "docCount": "stats.doc_num" }, diff --git a/plugins/OpenObserve/v1/metadata.json b/plugins/OpenObserve/v1/metadata.json index 0d7b4d55..05bd6d6b 100644 --- a/plugins/OpenObserve/v1/metadata.json +++ b/plugins/OpenObserve/v1/metadata.json @@ -13,7 +13,7 @@ "importNotSupported": false, "restrictedToPlatforms": [], "keywords": ["observability", "logs", "log search", "openobserve", "self-hosted", "alerts"], - "objectTypes": ["OpenObserve Stream"], + "objectTypes": ["Stream"], "links": [ { "category": "documentation", diff --git a/plugins/OpenObserve/v1/scopes.json b/plugins/OpenObserve/v1/scopes.json index 449f527b..8d881de7 100644 --- a/plugins/OpenObserve/v1/scopes.json +++ b/plugins/OpenObserve/v1/scopes.json @@ -2,7 +2,7 @@ { "name": "Streams", "matches": { - "sourceType": { "type": "oneOf", "values": ["OpenObserve Stream"] } + "sourceType": { "type": "oneOf", "values": ["Stream"] } }, "variable": { "name": "Stream", diff --git a/plugins/OpenObserve/v1/ui.json b/plugins/OpenObserve/v1/ui.json index cccfd7f7..fb355c78 100644 --- a/plugins/OpenObserve/v1/ui.json +++ b/plugins/OpenObserve/v1/ui.json @@ -6,7 +6,11 @@ "placeholder": "http://openobserve.example.com", "help": "The base URL of your self-hosted OpenObserve instance, **including the scheme** (`http://` or `https://`), without a trailing slash or port — the port is set separately below.", "validation": { - "required": true + "required": true, + "pattern": { + "value": "^https?://[^/:?#]+/?$", + "message": "Enter just the scheme and host, e.g. http://openobserve.example.com, with no path or port. The port is set separately below." + } } }, {