[FEATURE] Add OpenSearch datasource and log query plugin with PPL support#641
[FEATURE] Add OpenSearch datasource and log query plugin with PPL support#641Oliver-ke wants to merge 26 commits into
Conversation
Signed-off-by: Azorji Kelechi Oliver <kelechioliver96@gmail.com>
05cd7bf to
bc65cad
Compare
Signed-off-by: Azorji Kelechi Oliver <kelechioliver96@gmail.com>
The bound was being appended at the end of the user query, which fails once the user pipeline drops @timestamp from the schema (stats, fields, top). Tests now describe the intended ordering. Implementation follows. Signed-off-by: Azorji Kelechi Oliver <kelechioliver96@gmail.com>
Signed-off-by: Azorji Kelechi Oliver <kelechioliver96@gmail.com>
Signed-off-by: Azorji Kelechi Oliver <kelechioliver96@gmail.com>
Signed-off-by: Azorji Kelechi Oliver <kelechioliver96@gmail.com>
…cker file Signed-off-by: Azorji Kelechi Oliver <kelechioliver96@gmail.com>
0963d7f to
496c296
Compare
Signed-off-by: Azorji Kelechi Oliver <kelechioliver96@gmail.com>
Signed-off-by: Azorji Kelechi Oliver <kelechioliver96@gmail.com>
Signed-off-by: Azorji Kelechi Oliver <kelechioliver96@gmail.com>
Signed-off-by: Azorji Kelechi Oliver <kelechioliver96@gmail.com>
Signed-off-by: Azorji Kelechi Oliver <kelechioliver96@gmail.com>
…search-plugin chore(plugins): update with remote main branch
|
Hi @Oliver-ke, |
Sure, I'd share my testing.md file shortly |
|
TESTING.md Testing the OpenSearch pluginManual QA steps. Verifies datasource, log query, and trace → logs pivot. Prerequisites
1. Run unit testsInstall once at the workspace root so cd plugins
npm install
cd opensearch
npm test
npm run type-check
npm run lintValidate the plugin schemas with cd plugins/opensearch
percli plugin lint
percli plugin test-schemasGo SDK: cd plugins/opensearch
go test ./sdk/...Expect all green. No skipped suites. 2. Start a local OpenSearchUse the bundled compose file (also boots OpenSearch Dashboards on cd docs/examples
docker compose up -d
docker compose logs -f opensearch # watch boot, Ctrl+C once "started" appearsWait until ready: curl -s http://localhost:9200/_cluster/health | jq .statusExpect Teardown when done: docker compose down -v3. Seed sample log dataIndex two log documents with a shared curl -X POST "http://localhost:9200/otel-logs-2026.04.29/_doc" \
-H 'Content-Type: application/json' -d '{
"@timestamp": "2026-04-29T10:00:00Z",
"body": "request received",
"traceId": "abc123",
"severity": "INFO"
}'
curl -X POST "http://localhost:9200/otel-logs-2026.04.29/_doc" \
-H 'Content-Type: application/json' -d '{
"@timestamp": "2026-04-29T10:00:01Z",
"body": "request failed",
"traceId": "abc123",
"severity": "ERROR"
}'
curl -X POST "http://localhost:9200/otel-logs-2026.04.29/_refresh"Sanity-check PPL endpoint directly: curl -X POST "http://localhost:9200/_plugins/_ppl" \
-H 'Content-Type: application/json' \
-d '{"query":"source=otel-logs-* | where traceId='\''abc123'\''"}'Expect 2 datarows in response. 4. Build and serve the plugincd plugins/opensearch
npm run devDev server listens on 5. Register the datasource in PersesThe current Perses UI does not accept raw YAML for datasource creation, so create Option A — via the UI formNavigate to Admin → Global Datasources → Add Datasource. Fill in:
Save. Option B — via
|
| Case | Input | Expected |
|---|---|---|
| Empty query | `` | No request, no error toast |
| Bad PPL | source=foo | invalid |
Error banner with PPL error body (SyntaxCheckException) |
| Wrong index | source=does-not-exist |
Error banner with IndexNotFoundException from OpenSearch (the PPL endpoint returns 404; the plugin surfaces that as an OpenSearchPPLError) |
| Unreachable DS | stop docker, re-run query | Error banner, retry works after restart |
| Aggregation (stats) | source=otel-logs-* | stats count() by service |
Returns one row per service, no error |
| Field projection | source=otel-logs-* | fields service, body | head 10 |
Returns 10 rows with only those two columns |
7. Field overrides
Index a doc with non-standard field names:
curl -X POST "http://localhost:9200/legacy-logs/_doc" \
-H 'Content-Type: application/json' -d '{
"time": "2026-04-29T10:05:00Z",
"message": "legacy log line",
"trace_id": "abc123"
}'
curl -X POST "http://localhost:9200/legacy-logs/_refresh"In the query spec, set:
timestampField: time
messageField: message
query: source=legacy-logs | where trace_id='abc123'Expect row to render with time as timestamp and message as body. Without
the overrides, the row still renders without crashing: missing timestamp falls
back to epoch 0, and missing message falls back to a JSON dump of the row.
8. Trace → logs pivot
Load docs/examples/trace-to-logs.json as a dashboard. Requires a Tempo (or Jaeger) datasource also configured.
- Set
traceIdvariable toabc123. - Logs panel re-runs and shows the 2 seeded rows.
- Change variable to
xyz999. Logs panel goes empty without error. - Swap Tempo panel for Jaeger per
docs/examples/README.md. Logs panel unchanged.
9. Cleanup
cd plugins/opensearch/docs/examples
docker compose down -v
looks like a comprehensive test plan |
|
Hi @Oliver-ke, first of all great work! I have tested against a local OpenSearch with the opensearch_dashboards_sample_data_logs sample. I did not review the source-code. I have only tested the functionality and below are my findings. Bugs/Improvements
Screenshot 1
Screenshot 2Working after I add Timestamp field (optional) placeholder @timestamp implies the plugin will fall back gracefully if left blank. it doesn't
6.. PPL Query field is a plain textarea. It has no syntax highlighting, no autocomplete, no Query Examples disclosure (Please compare ClickHouse query editor in the same panel type).
http://localhost:9200/_cat/indices?v
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size
green open otel-v1-apm-service-map-sample Nio8tHEHTWK3uNI4PHmyXA 1 0 49 0 17.7kb 17.7kb
yellow open otel-logs-2026.04.29 _3tNZKIVTU2eseJhMHb_cw 1 1 4 0 17kb 17kb
green open otel-v1-apm-span-sample 14q5nFcqQq-OwewznwFAZQ 1 0 13061 0 6.1mb 6.1mb
green open .ql-datasources KdnzV0EoT96Y1XpY9NfQ7A 1 0 0 0 208b 208b
green open top_queries-2026.05.22-84957 hi43N36cRxqNUGtFImwVSg 1 0 16 2 104.1kb 104.1kb
green open ss4o_metrics-otel-sample 2Z4CkH5JTGWQXW6NZgHc7A 1 0 39923 0 4mb 4mb
green open .kibana_1 uKgkLwTLThyWFoSS5ztn2Q 1 0 246 1 155.6kb 155.6kb
green open ss4o_logs-otel-sample EkQueqJiQJmMWszbrNRe-Q 1 0 16286 0 5.5mb 5.5mb
green open .plugins-ml-config QmvGWldgQ_-5TnMuA35KeQ 1 0 1 0 4kb 4kb
green open .opensearch-observability BqmQfOVnSNaeth8GcjBEkw 1 0 0 0 208b 208b
green open opensearch_dashboards_sample_data_logs OgFiObqIR4GAuT_GPovfFA 1 0 14074 0 7.4mb 7.4mb
green open opensearch_dashboards_sample_data_flights 1jp6tGfmSuqBK1YUjKYmNA 1 0 13059 0 5.5mb 5.5mb
green open opensearch_dashboards_sample_data_ecommerce dxMt1PfqSP-9-4G5Cd-WbQ 1 0 4675 0 3.9mb 3.9mb8 Error banner reads OpenSearch PPL request failed (400): Invalid Query — the actual details field from the response (e.g. can't resolve Symbol(name=@timestamp)) is dropped, removing the important info.
|
@ibakshay Thanks for the feedback. |
|
@ibakshay Had to go through your feedbacks again. The following feedback (3, 8, 10, 11) has been clearly addressed on this PR. While 1, 2, and 6 have been partially addressed due to the increasing scope. Issues 4 and 7 also deferred as they result in a larger scope, while 5 and 9 are outside the plugin repo (Perses hosted UI). In summary, all feedback in scope for this PR, at least for the initial release, has been addressed. Further enhancements can be added in the coming PR's. |
…search-plugin chore: update with upstream remote
…ins into feature/opensearch-plugin chore: update with remote feature branch
I think I did not build properly before. Everything works as it is supposed to be now. Thank you for the clarification. |
…search-plugin chore: update with remote main branch
Signed-off-by: Kelechi Oliver Azorji <kelechioliver96@gmail.com>
9435174 to
073bda2
Compare
|
Resolved all outstanding comments. Bump perses version |
…search-plugin chore: update with remote main
|
Any new feedback on this PR? |
Signed-off-by: Kelechi Oliver Azorji <kelechioliver96@gmail.com>
|
PR has been updated based on feedbacks |
|
Please kindly review |
| context: LogQueryContext, | ||
| abortSignal?: AbortSignal | ||
| ) => { | ||
| if (!spec.query) { |
There was a problem hiding this comment.
we might need to trim this first. A query with might generate invalid PPL
| // OpenSearch often puts a generic phrase in `reason` ("Invalid Query") and the | ||
| // actionable text in `details` ("can't resolve Symbol(name=@timestamp)"). Surface | ||
| // both so the user can diagnose the failure. | ||
| const suffix = reason && details && reason !== details ? `${reason} — ${details}` : (reason ?? details); |
There was a problem hiding this comment.
empty string reason will not include the details:
| const suffix = reason && details && reason !== details ? `${reason} — ${details}` : (reason ?? details); | |
| const suffix = reason && details && reason !== details ? `${reason} — ${details}` : (reason || details); |
| let trimmed = userQuery.trim(); | ||
|
|
||
| if (index && !/^(?:search\s+)?source\s*=/i.test(trimmed)) { | ||
| trimmed = `source=${index} | ${trimmed}`; |
There was a problem hiding this comment.
should index be escaped with escapeIdentifier?
| const examplesStyle: React.CSSProperties = { | ||
| fontSize: '11px', | ||
| color: '#777', | ||
| backgroundColor: '#f5f5f5', |
There was a problem hiding this comment.
We should avoid using harcoded colors, this won't work for dark mode cases and won't match the general style, prefer using theme colors.
| PluginKind = "OpenSearchDatasource" | ||
| ) | ||
|
|
||
| type PluginSpec struct { |
There was a problem hiding this comment.
We don't need to duplicate the HTTPProxy fields, we can import them from the existing https://github.com/perses/spec/blob/main/go/datasource/datasource.go#L39
|
|
||
| const sourceClause = trimmed.slice(0, firstPipe).trimEnd(); | ||
| const rest = trimmed.slice(firstPipe + 1).trimStart(); | ||
| return `${sourceClause} | ${bound} | ${rest}`; |
There was a problem hiding this comment.
rest or other variables might be empty. I suggest using join with a filter, something like
parts.filter(notEmpty).join(' | ')
| if (typeof v === 'number') { | ||
| // Heuristic: anything past year ~5138 in seconds-since-epoch (1e11s) must | ||
| // really be milliseconds-since-epoch. OpenSearch usually returns ms here. | ||
| return v > 1e11 ? v / 1000 : v; |
There was a problem hiding this comment.
The heuristic lacks coverage for nano or microseconds, as this values need a different division factor. Specially for logs which might have nano second precision, the current heuristic will parse these values way far in the future. I suggest we add some test for this function.





Description
Adds an OpenSearch datasource + log query plugin
These changes includes:
remaining columns (e.g. traceId) as labels.
opensearch/docs/examples/trace-to-logs.json (Tempo + OpenSearch; Jaeger swap documented).
and 2 Go tests (builder JSON shape + omitempty for optional fields).
Screenshots
DataSource

Checklist
[<catalog_entry>] <commit message>naming convention using one of thefollowing
catalog_entryvalues:FEATURE,ENHANCEMENT,BUGFIX,BREAKINGCHANGE,DOC,IGNORE.UI Changes