diff --git a/plugins/OpenObserve/v1/configValidation.json b/plugins/OpenObserve/v1/configValidation.json new file mode 100644 index 00000000..04a960eb --- /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 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 new file mode 100644 index 00000000..4d6ec7ed --- /dev/null +++ b/plugins/OpenObserve/v1/custom_types.json @@ -0,0 +1,9 @@ +[ + { + "name": "OpenObserve Stream", + "sourceType": "Stream", + "icon": "stream", + "singular": "Stream", + "plural": "Streams" + } +] 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 new file mode 100644 index 00000000..e7f4b372 --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/clusterInfo.json @@ -0,0 +1,21 @@ +{ + "name": "clusterInfo", + "displayName": "Cluster Info", + "description": "Pending compaction jobs per node, grouped by region, for the OpenObserve cluster", + "tags": ["Streams"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "cluster_info", + "postRequestScript": "clusterInfo.js", + "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" }, + { "name": "pendingJobs", "displayName": "Pending Compaction Jobs", "shape": "number" } + ], + "timeframes": false +} diff --git a/plugins/OpenObserve/v1/dataStreams/incidentStats.json b/plugins/OpenObserve/v1/dataStreams/incidentStats.json new file mode 100644 index 00000000..c99d96c5 --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/incidentStats.json @@ -0,0 +1,26 @@ +{ + "name": "incidentStats", + "displayName": "Incident Stats", + "description": "Summary statistics for alert incidents in the organization", + "tags": ["Alerts"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "../v2/{{dataSource.organization}}/alerts/incidents/stats", + "postRequestScript": "incidentStats.js", + "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" }, + { "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..d6174787 --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/incidents.json @@ -0,0 +1,97 @@ +{ + "name": "incidents", + "displayName": "Incidents", + "description": "Alert incidents in the organization, groups of correlated alerts", + "tags": ["Alerts"], + "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", + "visibility": { "type": "hidden" }, + "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": "{{ new Date($['created_at'] / 1000).toISOString() }}" + }, + { + "name": "updated_at", + "displayName": "Updated At", + "shape": "date", + "valueExpression": "{{ new Date($['updated_at'] / 1000).toISOString() }}" + }, + { + "name": "first_alert_at", + "displayName": "First Alert At", + "shape": "date", + "valueExpression": "{{ new Date($['first_alert_at'] / 1000).toISOString() }}" + }, + { + "name": "last_alert_at", + "displayName": "Last Alert At", + "shape": "date", + "valueExpression": "{{ new Date($['last_alert_at'] / 1000).toISOString() }}" + }, + { + "name": "resolved_at", + "displayName": "Resolved At", + "shape": "date", + "valueExpression": "{{ $['resolved_at'] ? new Date($['resolved_at'] / 1000).toISOString() : null }}" + } + ], + "timeframes": false +} 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/logsSearch.json b/plugins/OpenObserve/v1/dataStreams/logsSearch.json new file mode 100644 index 00000000..7225a2a1 --- /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", + "validation": { "required": true } + }, + { + "type": "number", + "name": "size", + "label": "Max rows", + "defaultValue": 100 + } + ], + "metadata": [ + { + "name": "recordTime", + "displayName": "Timestamp", + "role": "timestamp", + "shape": "date" + }, + { "pattern": ".*" } + ], + "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/metricsQuery.json b/plugins/OpenObserve/v1/dataStreams/metricsQuery.json new file mode 100644 index 00000000..7fefc7df --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/metricsQuery.json @@ -0,0 +1,43 @@ +{ + "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": "code", + "name": "promql", + "label": "PromQL Query", + "language": "promql", + "placeholder": "up or rate(http_requests_total[5m])", + "validation": { "required": true } + }, + { + "type": "text", + "name": "step", + "label": "Step (e.g. 60s, 5m)", + "defaultValue": "60s" + } + ], + "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": ".*" } + ], + "timeframes": true +} diff --git a/plugins/OpenObserve/v1/dataStreams/pipelineHistory.json b/plugins/OpenObserve/v1/dataStreams/pipelineHistory.json new file mode 100644 index 00000000..fa5cdcd9 --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/pipelineHistory.json @@ -0,0 +1,56 @@ +{ + "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}}" } + ], + "postRequestScript": "pipelineHistory.js" + }, + "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" + }, + { "name": "is_realtime", "displayName": "Is Realtime", "shape": "boolean" }, + { "name": "is_silenced", "displayName": "Is Silenced", "shape": "boolean" }, + { "name": "retries", "displayName": "Retries", "shape": "number" }, + { "name": "error", "displayName": "Error", "shape": "string" }, + { "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/pipelines.json b/plugins/OpenObserve/v1/dataStreams/pipelines.json new file mode 100644 index 00000000..6ea78098 --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/pipelines.json @@ -0,0 +1,28 @@ +{ + "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": "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" }, + { "name": "nodeCount", "displayName": "Node Count", "shape": "number" }, + { "name": "edgeCount", "displayName": "Edge Count", "shape": "number" } + ], + "timeframes": false +} diff --git a/plugins/OpenObserve/v1/dataStreams/scripts/alerts.js b/plugins/OpenObserve/v1/dataStreams/scripts/alerts.js new file mode 100644 index 00000000..36095f59 --- /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 +// 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 usToIso = (us) => (us != null ? new Date(us / 1000).toISOString() : 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: 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/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/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/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/scripts/metricsQuery.js b/plugins/OpenObserve/v1/dataStreams/scripts/metricsQuery.js new file mode 100644 index 00000000..16e70cdb --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/scripts/metricsQuery.js @@ -0,0 +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". 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__ || "", + 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 new file mode 100644 index 00000000..570c99b4 --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/scripts/pipelineHistory.js @@ -0,0 +1,15 @@ +// 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. +// "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: 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 new file mode 100644 index 00000000..74c5cab8 --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/scripts/pipelines.js @@ -0,0 +1,29 @@ +// 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]". +// 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 - converted to ISO 8601. +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 || []; + return { + pipeline_id: p.pipeline_id, + name: p.name, + description: p.description || "", + enabled: p.enabled, + version: p.version, + 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) || "", + sourceStreamType: (p.source && p.source.stream_type) || "", + nodeCount: nodes.length, + edgeCount: edges.length, + }; +}); diff --git a/plugins/OpenObserve/v1/dataStreams/scripts/search.js b/plugins/OpenObserve/v1/dataStreams/scripts/search.js new file mode 100644 index 00000000..b8e13a4c --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/scripts/search.js @@ -0,0 +1,14 @@ +// 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 -> 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, + 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 new file mode 100644 index 00000000..8cd86d82 --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/scripts/slos.js @@ -0,0 +1,42 @@ +// 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. +// 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 { + 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 ? 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 new file mode 100644 index 00000000..501aa52f --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/scripts/tracesLatest.js @@ -0,0 +1,73 @@ +// dataStreams/scripts/tracesLatest.js +// +// "spans" is a 2-element array ([spanCount, ...]), not a list of individual +// 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). +// +// 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. +// +// 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) || ""; + +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: new Date(bucketStart).toISOString(), + 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: 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, + 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/search.json b/plugins/OpenObserve/v1/dataStreams/search.json new file mode 100644 index 00000000..a03e6e59 --- /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": ["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", + "validation": { "required": true } + }, + { + "type": "number", + "name": "size", + "label": "Max rows", + "defaultValue": 100 + } + ], + "metadata": [ + { + "name": "recordTime", + "displayName": "Timestamp", + "role": "timestamp", + "shape": "date" + }, + { "pattern": ".*" } + ], + "timeframes": true +} diff --git a/plugins/OpenObserve/v1/dataStreams/simpleMetricsQuery.json b/plugins/OpenObserve/v1/dataStreams/simpleMetricsQuery.json new file mode 100644 index 00000000..9578b095 --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/simpleMetricsQuery.json @@ -0,0 +1,157 @@ +{ + "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.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", + "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": "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": ".*" } + ], + "timeframes": true +} \ No newline at end of file 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/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/streams.json b/plugins/OpenObserve/v1/dataStreams/streams.json new file mode 100644 index 00000000..c6a139fe --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/streams.json @@ -0,0 +1,53 @@ +{ + "name": "streams", + "displayName": "Streams", + "description": "Streams registered in the organization, with stats and settings", + "tags": ["Streams"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "streams", + "getArgs": [ + { "key": "type", "value": "{{type}}" }, + { "key": "fetchSchema", "value": "false" } + ], + "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" }, + { "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/dataStreams/tracesLatest.json b/plugins/OpenObserve/v1/dataStreams/tracesLatest.json new file mode 100644 index 00000000..c9fab9c3 --- /dev/null +++ b/plugins/OpenObserve/v1/dataStreams/tracesLatest.json @@ -0,0 +1,113 @@ +{ + "name": "tracesLatest", + "displayName": "Traces Latest", + "description": "Recent traces 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": ["Stream"] }, + "stream_type": { "type": "equals", "value": "traces" } + }, + "ui": [ + { + "type": "number", + "name": "size", + "label": "Max rows", + "defaultValue": 50 + }, + { + "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": [ + { "name": "start_time", "visible": false }, + { "name": "end_time", "visible": false }, + { "name": "duration", "visible": false }, + { "name": "traceUrl", "visible": false }, + { + "name": "startTime", + "displayName": "Start Time", + "role": "timestamp", + "shape": "date" + }, + { + "name": "endTime", + "displayName": "End Time", + "shape": "date" + }, + { + "name": "durationMs", + "displayName": "Duration (ms)", + "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", + "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", + "valueExpression": "{{ $['traceUrl'] }}", + "shape": ["url", { "label": "View Trace" }] + }, + { "pattern": ".*" } + ], + "timeframes": ["last1hour"] +} 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..99eecd6a --- /dev/null +++ b/plugins/OpenObserve/v1/defaultContent/overviewDashboard.dash.json @@ -0,0 +1,619 @@ +{ + "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": "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": "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", "query_took", "error"], + "hiddenColumns": ["is_realtime", "is_silenced", "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": "Alerts", + "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": "Alerts", + "description": "", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.alerts}}", + "name": "alerts", + "pluginConfigId": "{{configId}}", + "group": { "by": [], "aggregate": [{ "type": "count" }] } + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count", + "comparisonColumn": "none", + "label": "Alerts" + } + } + } + } + }, + { + "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": "Alerts", + "description": "Alert rules in the organization, with their current state and last evaluation outcome", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.alerts}}", + "name": "alerts", + "pluginConfigId": "{{configId}}", + "sort": { "by": [["lastOutcomeAt", "desc"]] } + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": ["name", "level", "lastOutcome", "priority", "streamName", "lastOutcomeAt", "lastTriggeredAt"], + "hiddenColumns": ["id", "description", "enabled", "alertType", "isRealTime", "streamType", "conditionType", "folderName", "owner", "tags", "levelSince", "lastOutcomeSince", "lastSatisfiedAt"] + } + } + } + } + }, + { + "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": "series", + "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": [["recordTime", "desc"]] } + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false + } + } + } + } + } + ] + } +} diff --git a/plugins/OpenObserve/v1/defaultContent/streamDashboard.dash.json b/plugins/OpenObserve/v1/defaultContent/streamDashboard.dash.json new file mode 100644 index 00000000..5225cca3 --- /dev/null +++ b/plugins/OpenObserve/v1/defaultContent/streamDashboard.dash.json @@ -0,0 +1,95 @@ +{ + "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": [["recordTime", "desc"]] } + }, + "scope": { + "scope": "{{scopes.[Streams]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Stream]}}" + }, + "variables": ["{{variables.[Stream]}}"], + "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 new file mode 100644 index 00000000..8a3a1b97 --- /dev/null +++ b/plugins/OpenObserve/v1/docs/README.md @@ -0,0 +1,77 @@ +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**, **Metrics**, **Traces**, **Pipelines**, **Alerts**, and **SLOs** API areas — it does not cover OpenObserve Cloud, ingestion, dashboards, reports, or user management. + +## Setup + +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 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@example.com:mytoken" | base64 + ``` +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 | +| ----- | ---------- | ----------------- | -------- | +| **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 | +| **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 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. +- **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. +- **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). + +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. +- **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 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. +- **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. +- **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. +- **Incidents**, **Incident Stats**, **Cluster Info** — hidden from the tile editor. + +## 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 + +- **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). +- **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. +- **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/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/indexDefinitions/default.json b/plugins/OpenObserve/v1/indexDefinitions/default.json new file mode 100644 index 00000000..38313e6c --- /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": "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": "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": "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 new file mode 100644 index 00000000..05bd6d6b --- /dev/null +++ b/plugins/OpenObserve/v1/metadata.json @@ -0,0 +1,42 @@ +{ + "name": "openobserve", + "displayName": "OpenObserve", + "version": "1.0.0", + "author": { + "name": "SquaredUp Labs", + "type": "labs" + }, + "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", "alerts"], + "objectTypes": ["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": "{{url}}:{{port}}/api/{{organization}}", + "authMode": "none", + "headers": [ + { "key": "Authorization", "value": "Basic {{apiKey}}" } + ], + "queryArgs": [], + "ignoreCertificateErrors": "{{ignoreCertificateErrors}}" + } + } +} diff --git a/plugins/OpenObserve/v1/scopes.json b/plugins/OpenObserve/v1/scopes.json new file mode 100644 index 00000000..8d881de7 --- /dev/null +++ b/plugins/OpenObserve/v1/scopes.json @@ -0,0 +1,14 @@ +[ + { + "name": "Streams", + "matches": { + "sourceType": { "type": "oneOf", "values": ["Stream"] } + }, + "variable": { + "name": "Stream", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + } +] diff --git a/plugins/OpenObserve/v1/ui.json b/plugins/OpenObserve/v1/ui.json new file mode 100644 index 00000000..fb355c78 --- /dev/null +++ b/plugins/OpenObserve/v1/ui.json @@ -0,0 +1,52 @@ +[ + { + "type": "url", + "name": "url", + "label": "URL", + "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, + "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." + } + } + }, + { + "type": "number", + "name": "port", + "label": "Port", + "defaultValue": 5080, + "help": "The port your OpenObserve instance listens on.", + "validation": { + "required": true + } + }, + { + "type": "text", + "name": "organization", + "label": "Organization", + "defaultValue": "default", + "placeholder": "default", + "help": "Your OpenObserve organization identifier.", + "validation": { + "required": true + } + }, + { + "type": "password", + "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." + } +]