Commit Graph
3438 Commits
Author SHA1 Message Date
Amey PawarandHengfei Yang 5fdff20e5c fix(alerts): keep every label set on the OTLP and JSON metrics paths (#14085)
Co-authored-by: Hengfei Yang <hengfei.yang@gmail.com>
2026-09-03 07:34:40 +00:00
Amey PawarandHengfei Yang f1af2562bd docs(alerts): stop promising the create body's folder_id places the alert (#14121)
Co-authored-by: Hengfei Yang <hengfei.yang@gmail.com>
2026-09-03 07:27:49 +00:00
Amey PawarandHengfei Yang b892abc5d1 fix(promql): return an error for a range selector with no metric name (#14115)
Co-authored-by: Hengfei Yang <hengfei.yang@gmail.com>
2026-09-03 07:12:29 +00:00
udevandHengfei Yang e06fe8e686 fix: register experiment row sort OpenAPI schema (#14128)
Co-authored-by: Hengfei Yang <hengfei.yang@gmail.com>
2026-09-03 03:07:13 +00:00
Huaijin c373875803 fix(compactor): keep legacy metrics files out of hash-ordered merges (#14123) 2026-09-02 23:00:16 +00:00
Harsh Mahajan a134e88366 fix: duplicate trigger reporting (#14021) 2026-09-02 19:53:43 +00:00
HuaijinandHengfei Yang 665e2f72f7 feat(metrics): register hash-sorted metrics tables and prune band scans (#14096)
Co-authored-by: Hengfei Yang <hengfei.yang@gmail.com>
2026-09-02 13:28:24 +00:00
Huaijin 0df6afbb23 refactor(promql): fold fused aggregation through a series-source contract (#14097) 2026-09-02 13:12:00 +00:00
76bdf957ce fix: ai obs issues (#14114)
Co-authored-by: sai nikhil kethe <nikhil@openobserve.ai>
Co-authored-by: Shrinath Rao <shnath@openobserve.ai>
2026-09-02 18:59:44 +05:30
Shrinath Raoandhengfeiyang ec0b08dcee fix: stop quick mode dropping the RUM identity fields, and deflake 5 UI tests (#14113)
Co-authored-by: hengfeiyang <hengfei.yang@gmail.com>
2026-09-02 12:11:26 +00:00
Hengfei Yang 8ed8ec0509 fix: reject client-forged session trust markers in auth (#14105) 2026-09-02 10:31:11 +00:00
Hengfei Yang c14bf509ac fix: answer an unknown pattern id in stream settings with 400 (#14093) 2026-09-02 04:49:22 +00:00
Hengfei Yang 2feb1f7f1d fix: stop two handler panics the API fuzzer reaches through query params (#14089) 2026-09-02 02:19:31 +00:00
Prabhat Sharma 478824a41e feat(alerts): add curated alerts from the library in bulk (#14054) 2026-09-02 00:54:25 +00:00
1329d6a81a feat: pending period for alerts (#14009)
Signed-off-by: Yashodhan Joshi <yjdoc2@gmail.com>
Co-authored-by: Yashodhan Joshi <yjdoc2@gmail.com>
Co-authored-by: ktx-akshay <akshay.patil@kiara.tech>
2026-09-01 17:51:47 +00:00
Hengfei Yang 9a89aee4b2 fix: keep search trace collection independent of log level and caller propagation (#14074) 2026-09-01 13:37:30 +00:00
Vaibhav Mittal 4ff12d1194 fix: publish nodes only after gRPC listener binds (#14031)
Signed-off-by: Vaibhav mittal <vaibhavmittal929@gmail.com>
2026-09-01 11:55:33 +00:00
BhargavandHengfei Yang 6678b1908b fix(logs): scope Search History to the active telemetry type/stream (#14059)
Co-authored-by: Hengfei Yang <hengfei.yang@gmail.com>
2026-09-01 07:13:13 +00:00
Amey PawarandHengfei Yang 060e117d41 fix(alerts): keep every label set in a realtime metrics notification (#14010)
Co-authored-by: Hengfei Yang <hengfei.yang@gmail.com>
2026-09-01 06:52:59 +00:00
Huaijin 786d01a61d perf(promql): skip residual matchers when metrics-index selections are exact (#14056) 2026-09-01 06:40:26 +00:00
Yashodhan JoshiandYashodhan Joshi 131cf40c4b feat: support multi alert for sql (#13915)
Signed-off-by: Yashodhan Joshi <yjdoc2@gmail.com>
Co-authored-by: Yashodhan Joshi <yjdoc2@gmail.com>
2026-09-01 05:25:06 +00:00
Ashish Kolhe 043366ba6d fix(status-pages): pair the brand logo with the page name and link the OpenObserve mark (#14047) 2026-08-31 16:42:46 +00:00
Huaijin 18e1a323da refactor(promql): extract shared fused evaluation and selector helpers (#14035) 2026-08-31 13:34:29 +00:00
Ashish Kolheandgithub-actions[bot] 07807995e4 feat: public status pages built on synthetics checks (#13974)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-31 11:45:51 +00:00
0xfandomandHengfei Yang 906f3c46f4 fix: reject empty org_id in POST /api/{org_id}/users (#13806)
Co-authored-by: Hengfei Yang <hengfei.yang@gmail.com>
2026-08-31 11:34:50 +00:00
Mohd Quamar TyagiandHengfei Yang 8a3f9304fd fix(compactor): drop merged source files from the local disk cache after commit (#13961)
Co-authored-by: Hengfei Yang <hengfei.yang@gmail.com>
2026-08-31 09:35:30 +00:00
Prabhat Sharma 9a77c6b878 feat: Slack destination setup via OAuth and app manifest (#14020) 2026-08-30 14:29:57 +00:00
Ashish Kolhe 67d5ce0e14 fix: eliminate compactor/service-discovery Postgres write amplification (#14023) 2026-08-30 10:03:43 +00:00
Hengfei Yang 6ba0209557 fix(search): cap result cache entries per key and stop over-invalidating the cache index (#13981) 2026-08-29 05:55:51 +00:00
Hengfei Yang c23beef705 fix(metrics): bound the metrics cache index by memory budget and sync it with disk eviction (#13983) 2026-08-28 17:39:29 +00:00
Huaijin 732f721d04 fix: vortex Utf8-typed string statistics broke Utf8View interval analysis (#14011) 2026-08-28 08:48:15 +00:00
Huaijin ba1f00f7e1 perf(promql): avoid repeated parsing, lookup, and sorting in histogram_quantile (#13964) 2026-08-28 05:27:20 +00:00
9430c5f061 feat(synthetics): meter step-based billing usage, shipped dark (#13951)
Co-authored-by: Hengfei Yang <hengfei.yang@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-28 03:19:00 +00:00
Hengfei Yang eedc773cd7 chore: remove action scripts / action server (#14000) 2026-08-28 01:57:53 +00:00
udevandsai nikhil kethe 9e5488ed2b feat(llm-obs): add Playground and remote task workflows (#13991)
Co-authored-by: sai nikhil kethe <nikhil@openobserve.ai>
2026-08-27 16:30:49 +00:00
Hengfei Yang 78c18ad300 feat: org-scoped search inspector with dedicated permission (#14005) 2026-08-27 16:02:55 +00:00
HuaijinandHengfei Yang 826b2ef927 perf(promql): replace per-window counter reset scans with a per-series prefix (#13997)
Co-authored-by: Hengfei Yang <hengfei.yang@gmail.com>
2026-08-27 15:19:07 +00:00
Hengfei Yangandgithub-actions[bot] f0df035b49 feat(alerts): add is_null/is_not_null/is_empty/is_not_empty condition operators for alerts, pipelines and evaluator filters (#13978)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-27 14:32:47 +00:00
Hengfei Yang adf55fdba4 perf(traces): batched per-stream scan for multi-id time range lookup (#14002) 2026-08-27 13:19:28 +00:00
3e33b0e262 fix: workflow UX improvements (#13929)
Signed-off-by: Yashodhan Joshi <yjdoc2@gmail.com>
Co-authored-by: Yashodhan Joshi <yjdoc2@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-27 12:29:14 +00:00
Hengfei Yang bbf3754d3b feat(traces): multi-id and org-wide trace time range lookup (#13992) 2026-08-27 08:23:43 +00:00
HuaijinandHengfei Yang 9f5f9ab20e perf(promql): fold range-function output incrementally into aggregations (#13989)
Co-authored-by: Hengfei Yang <hengfei.yang@gmail.com>
2026-08-27 07:35:35 +00:00
Huaijin 775094165c perf(promql): preallocate hash-sorted series samples (#13971) 2026-08-26 14:35:33 +00:00
Hengfei Yang b40094e8eb fix(search): scan the exact requested range for histogram queries (#13969)
## Problem

Fixes #13946. The sum of histogram bucket counts exceeded `count(*)`
over the same time range (e.g. 2744 vs 2704).

#12313 made the leader snap the follower scan `start_time` **down** to
the histogram bucket boundary in `RemoteScanNodes::get_remote_node()`,
to keep the first chart bucket fully populated. But that widens the
actual scan window: records in `[bucket_start, requested_start)` — data
the user explicitly excluded — get counted into the first bucket, while
`count(*)` (a separate query without `histogram_interval`) scans the
exact requested range. The reporter confirmed the diagnosis: aligning
`count(*)`'s start to the bucket grid makes the two numbers match.

#13896 was the same root cause surfacing on sub-10s ranges (worked
around in #13916 by dropping to 1s buckets there, without touching the
snap).

## Fix

Revert the snap: the scan window sent to followers now equals the
requested range exactly.

- `distributed_plan/node.rs`: `start_time` passes through unchanged; a
comment pins the invariant and a regression test
(`test_get_remote_node_keeps_raw_start_time_for_histogram`) asserts the
window is not widened when `histogram_interval > 0`.
- `sql/histogram.rs`: delete the now-unused `histogram_bucket_start()`
(zero remaining references, including in the enterprise repo).
- `sql/visitor/histogram_interval.rs`: reword two comments that were
written in terms of the snap. The 1s interval floor for sub-10s ranges
from #13916 is kept — a bucket wider than the whole range is bad
charting regardless of the snap.

Histogram semantics are now the same as Elasticsearch/Kibana
`date_histogram`: buckets only count in-range records, so `sum(buckets)
== count(*)` always holds, and the first bucket may be partial.

Safety notes:

- The tantivy `SimpleHistogram` fast path is unaffected: it aligns
bucket *labels* itself (`start_time - start_time % rounding_by`) and
only counts docs matched by the query's own time filter, consistent with
the `date_bin` path.
- The results cache is unaffected: `write_results` already aligns the
cache-accepted window *inward* and drops edge buckets, so partial (and
previously over-counted) first buckets never entered the cache.

## Trade-off / follow-up for the UI

The left-edge display artifact that motivated #12313 (first bar looks
lower because its bucket is cut by the range start) returns by design.
Suggested frontend handling, either:

- align the picker's `start_time` down to the returned
`histogram_interval` and issue **all** page queries (histogram + logs +
count) over that aligned range — Grafana's approach; totals stay
consistent and no partial bucket exists, or
- detect `buckets[0].key < start_time` and dim/annotate the first bar as
partial.

## Test plan

- `search` crate: 88 histogram tests + 9 node tests pass (includes the
new regression test); workspace clippy with CI flags passes.
- Single-node E2E: ingested 240 events at 500ms spacing over 120s;
queried with mid-bucket start/end (+17s/+107s), interval 30s. `count(*)`
= 180; buckets 26+60+60+34 = 180 (match). First bucket correctly holds
26 in-range records instead of the full 60 (old behavior summed to 214).
2026-08-26 10:51:36 +00:00
Hengfei Yang ebdc2f0a4e fix(search): release the admission slot and follower task status on cancellation (#13954)
Paired with o2-enterprise `hf/issue-2491-admission-slot-leak` (same
branch name).

**Stacked on #13953** — base is that branch, so the diff here is just
this change.
Retargets to `main` automatically once #13953 merges.

## Problem

#13953 fixed the wal file locks. `do_get` touches two more trace-id
keyed
resources, and both had the identical defect: release depended on
control flow
reaching a call, inside the window where gRPC cancellation drops the
future.

**The node's admission slot.** Released from three hand-written sites —
the two
error arms in `do_get` and `FlightEncoderStream::drop`. All three are
skipped
when the request is cancelled during plan generation, because the
encoder
stream does not exist yet.

**The follower task status.** In the super-cluster path the entry was
inserted
before plan generation and removed after it:

```rust
if is_super_cluster && !SEARCH_SERVER.contain_key(&trace_id).await {
    SEARCH_SERVER.insert(trace_id.clone(), TaskStatus::new_follower(vec![], false)).await;
}
let result = get_ctx_and_physical_plan(&trace_id, &req).await;   // <-- cancellable
if is_super_cluster && !SEARCH_SERVER.is_leader(&trace_id).await {
    SEARCH_SERVER.remove(&trace_id, false).await;
}
```

Cancel in the middle and the entry stays in the map for the life of the
process. Nothing else removes it: an explicit `cancel_query` would, but
a
timeout or a client disconnect would not.

## Approach

Both move onto the same ownership chain #13953 established.

`SessionGuard` — already armed on the first line of `do_get` and already
carried into `FlightEncoderStream` — now releases the admission slot
too, so
the three call sites collapse into its drop.

The follower task status is registered through an `AsyncDefer` that
lives in
the same scope as plan generation, so it is removed on every exit
including a
cancelled one. It is now removed only when this node registered it,
rather than
whenever the entry is not a leader's: a request can no longer clear an
entry it
did not create. In practice these coincide, since the composite
`{trace_id}-{job_id}`
is unique per request.

### Drive-by

The `physical plan generate error` and `physical plan execution error`
logs sat
*inside* `if get_o2_config().work_group.max_nodes_per_query > 0`, so a
plan
failure was silent unless work groups happened to be configured — and
silent in
every OSS build. They are unconditional now.

## Enterprise side

The paired PR gives `Running` reservations a TTL backstop. They had no
expiry
edge at all, so a slot whose release was lost — this cancellation path,
or a
leader that dies before sending `Release` — was held until the process
restarted. This PR closes the local path; the backstop covers the remote
one,
which no local guard can.

## Verification

- `cargo fmt --all` and the CI clippy command: clean
- Enterprise verified via the `Cargo.toml.openobserve` swap against the
paired
  branch: `cargo check --all-targets` clean, and `cargo clippy --no-deps
-p openobserve-api-grpc --all-targets` clean. The workspace-wide
enterprise
clippy still reports the two pre-existing `cognitive_complexity` errors
in
`core/src/alerts/scheduler/handlers.rs` and `core/src/ofga/mod.rs`,
untouched
  by this change
- `api-grpc` flight tests 21/21

## Also here

Three zh-CN strings whose English is *events* said 活动 (activity).
Unrelated to
the rest of the PR, carried along at review request.

| key | en-US | was | now |
|---|---|---|---|
| `home.docsCountLbl` | Events | 活动 | 事件 |
| `rum.searchEvents` | Search Events | 搜索活动 | 搜索事件 |
| `logs.noEvents.title` | No events found | 未找到任何活动 | 未找到任何事件 |

zh-TW already had 事件 for all three, and every other locale translates
them as
events. `logs.noEvents` also contradicted itself — `descNoFilters`
directly
below the title already said 未找到任何事件.

## Not in this PR

The `QueryManager` map has a `created_at` on every entry but no reaper.
With
this change its entries are removed on cancellation, so it is no longer
known
to grow — but it has no backstop the way the ledger now does.
2026-08-26 09:09:35 +00:00
b09b56e25a feat(llm): add Phase 3.0 experiment workflows (#13778)
## Summary

- allow dataset rows without an expected output across management APIs
and UI flows
- add immutable experiment preview, creation, listing, persistence,
RBAC, and super-cluster synchronization
- add Experiment as an evaluation target with slot hydration, scoring,
and result indexing
- harden idempotent creation, scorer-version pinning, score-index setup,
and upgrade migrations

## Tickets

- Related to
[openobserve/o2-enterprise#2398](https://github.com/openobserve/o2-enterprise/issues/2398)
- Related to
[openobserve/o2-enterprise#2399](https://github.com/openobserve/o2-enterprise/issues/2399)
- Related to
[openobserve/o2-enterprise#2400](https://github.com/openobserve/o2-enterprise/issues/2400)

## Paired Enterprise PR

-
[openobserve/o2-enterprise#2413](https://github.com/openobserve/o2-enterprise/pull/2413)

## Validation

- focused Rust and frontend tests for the affected dataset and
experiment seams
- experiment migration test passes
- upgraded local environment applies schema version 68 and returns 200
from GET /api/team3/experiments

---------

Co-authored-by: sai nikhil kethe <nikhil@openobserve.ai>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-26 05:47:37 +00:00
Hengfei Yang 429e5148fe fix(search): tie wal search file locks to guards so cancelled queries release them (#13953)
Fixes o2-enterprise#2491. OSS-only — see "Enterprise side" below.

## Problem

Local wal parquet files that had already been merged and uploaded to
object
storage stayed on disk forever, because their `SEARCHING_FILES` refcount
never
returned to zero. The post-merge cleanup in
`jobs/src/job/files/parquet.rs`
checks `wal::lock_files_exists(file_key)`, defers the deletion when a
search
holds the file, and re-checks every `ZO_FILE_PUSH_INTERVAL` — forever,
if the
lock is never released. Queries stayed correct (`pending_delete::filter`
excludes those files from the search set); the symptom was disk.

The proximate cause is a missing decrement. The root cause is that
**release
depended on control flow reaching a line, while the only thing that can
cancel
the request guarantees no further line runs**.

### The unowned window

`SEARCHING_FILES` and `SEARCHING_REQUESTS` are global maps. Nothing in
the type
system points at an entry, so nothing releases one automatically:

```
lock_files()                        search_service/src/grpc/wal.rs, in get_file_list
  |   match_source, parquet footer reads, calculate_files_size, table build
lock_request()                      search_service/src/grpc/wal.rs
  |   pack segments, union table, plan rewrite, execute_stream_partitioned
FlightEncoderStreamBuilder::build() api/grpc .../flight/mod.rs  <-- first owner with a Drop
```

`lock_request()` is not a handoff to an owner — `SEARCHING_REQUESTS` is
removed
from only by `clear_session_data`, itself reached by control flow. So
the whole
of plan generation was unprotected, not just the gap between the two
calls.

### Why that window is exactly the window cancellation lives in

Both gRPC cancellation paths can only fire *before* `do_get` returns a
response:

- `hyper-1.9.0/src/proto/h2/server.rs` polls `poll_reset` only on the
`Poll::Pending` branch — "check if the client has sent a RST_STREAM
frame
which would cancel the current request" — and drops the service future.
- `tonic-0.14.6/src/transport/service/grpc_timeout.rs` reads the
client's
`grpc-timeout` header and returns `Poll::Ready(Err(TimeoutExpired))`,
dropping
  the inner future. Every flight request sets that header
  (`make_flight_client` → `req.set_timeout(grpc_timeout)`).

Once the response stream exists, `FlightEncoderStream::drop` handles
teardown
and neither path applies. So the set of cancellable moments and the set
of
unowned moments coincided.

Routine triggers, all of which drop in-flight `do_get` calls to
ingesters:
`query_task.abort()` on leader timeout or user cancel
(`search_service/src/cluster/flight.rs`), `SearchStreamGuard::drop`
cancelling
on client disconnect, and the ingester `grpc-timeout` itself. None of
them are
visible: `remote_scan_exec.rs` treats `Cancelled` / `DeadlineExceeded`
from a
node as an empty partial result, so the query still succeeds.

## Approach

Give the locks an owner for every instant they exist.

- **`wal::SearchingFiles`** owns the raw locks from acquisition until
`into_request` hands them to `SEARCHING_REQUESTS`; `Drop` releases
whatever is
left. `get_file_list` returns it alongside the file list, so the type
forces
  every early return to account for it.
- **`flight::SessionGuard`** is armed on the first line of `do_get` —
before
anything can be locked — owns `clear_session_data` for that trace id,
and
moves into `FlightEncoderStream`, whose `Drop` already performed it. It
is a
required argument of `FlightEncoderStreamBuilder::new`, not an optional
  setter, so a future call site cannot forget it.

`lock_files`, `release_files` and `lock_request` are module-private now:
the
guard is the only way to take a wal lock. The eight hand-written
`release_files` call sites are gone, and `create_tables_from_files`
loses the
`on_error: F` generic that existed solely to run one of them —
`storage.rs` was
already passing `|| {}`.

Cancellation at any `.await` in the chain now releases:

| cancelled during | released by |
|---|---|
| `get_file_list`, metadata reads, table build | `SearchingFiles::drop`
|
| after `into_request`, before the stream exists | `SessionGuard::drop`
|
| streaming | `FlightEncoderStream::drop` → `SessionGuard::drop` |

### Others on the same chain

- `release_request` no longer early-returns on non-ingesters — the last
step of
an ownership chain should not be conditional — and logs only a release
that
actually happened, so `lock_request` / `release_request` now pair
one-to-one
  per trace id and can be reconciled from logs.
- `lock_request` merges instead of overwriting; a second handoff for one
trace
  id used to silently drop the first file list.
- `FlightEncoderStream::drop` releases session data unconditionally.
Letting
"am I a super-cluster follower leader?" decide whether wal locks are
released
was the same bug in miniature; the deferred lock only covers the
work-group
  slot.

### Observability

Two gauges, both O(1) reads, published by the existing 60s ingester
collector
`update_parquet_metrics` (no stream label):

- `ingest_wal_searching_files` — size of the lock table
- `ingest_wal_pending_delete_files` — uploaded files still awaiting
deletion

A pending-delete backlog that rises and never drains is the leak,
plotted
directly. Previously the only signal was an *absent* `debug!` line.

## Enterprise side

**No paired PR — the fix is entirely in this repo.** The issue suspected
the
super-cluster follower-leader path, but `super_cluster/follower.rs` only
builds
a `RemoteScanExec` and never touches wal locks; `wal::lock_files` has
exactly
one call site, in `grpc/wal.rs`; and o2-enterprise contains no reference
to
`lock_files`, `release_files`, `lock_request` or `release_request`.
Verified by
a full enterprise build after making those three private.

## Verification

- `cargo fmt --all`, and the CI clippy command
  (`--workspace --all-targets -W too_many_lines -W cognitive_complexity
  -W excessive_nesting -D warnings`): clean
- Enterprise verified via the `Cargo.toml.openobserve` swap,
  `cargo check --all-targets`: clean. Clippy on that build reports two
pre-existing `cognitive_complexity` errors in
`core/src/alerts/scheduler/handlers.rs`
  and `core/src/ofga/mod.rs`, untouched by this change
- Unit tests: `common` wal 10/10 (4 new, covering drop-release,
duplicate
collapse, `release_one`, and the `into_request` handoff), `api-grpc`
flight
  21/21, `search` file_list 4/4

## Not in this PR

- **A timestamped reaper for stale locks.** Redundant once the chain is
  unbroken, but it is the lowest-risk first step when backporting to
  `branch-v0.40.0` and `branch-v0.80.0`, which carry the same defect.
- **`admission::ledger` and the `SEARCH_SERVER` task status.** Both are
trace-id keyed and both release only by reaching a call, so a
cancellation in
  the same window leaks a work-group slot the same way. Same bug class,
  different resource, enterprise-gated — worth its own issue.
- **`clean_lock_files()`** (`/node/flush`, `/node/enable`) wipes the
whole
refcount table, including locks held by running queries. It was
survivable
while locks leaked; now it turns a leak into deleting a file a query is
  reading. Retiring it is a product call.
2026-08-25 14:03:09 +00:00
Hengfei Yang 663e2f75da fix(api): stop unprintable error messages collapsing responses into a bare 500 (#13952)
## Problem

`api_fuzz_tests` (schemathesis) intermittently reported a 500 on
`POST /api/{org_id}/streams/{stream_name}` with the body
`failed to parse header value`.

That string is not ours — it is the `http` crate's `InvalidHeaderValue`
display. The chain:

- ~20 sites return `(StatusCode, [(ERROR_HEADER, format!(...))],
Json(...))`,
where the message interpolates user-supplied names (org, stream, **field
  type**).
- The failing payload carried `"type": "v^0K…"`. `HeaderValue` accepts
only `b >= 32 && b != 127 || b == b'\t'`, so the control byte makes
header
  construction fail.
- axum's `TryIntoHeaderError` then **discards the whole response** and
  substitutes `(500, inner.to_string())`.

So the handler's real 400 and its JSON body never reached the client.
The
status code was not chosen by the handler at all — axum overwrote it.

The org name was never the trigger; it only had to exist so the request
got
past the auth check. The field type was the poison.

A second, quieter bug sits in the same place: non-ascii bytes *are*
accepted
into a header, but `HeaderValue::to_str()` only reads back `32..127`,
and the
audit middleware reads this header with exactly `to_str().ok()`. Any
error
mentioning a non-ascii stream name was being silently dropped from the
audit
log.

## Approach

Route every `X-Error-Message` write through one chokepoint in
`common::meta::http`:

- `error_header_value()` escapes anything outside visible ascii via
`char::escape_default`, which fixes both failure modes at once — the
header
  always builds, and it always reads back.
- `HttpResponse::error_with_header()` sets the header and the JSON body
from a
  single message, replacing the hand-built tuples.

Most sites were formatting the identical message twice (once for the
header,
once for the body), so this removes that duplication as well.

`enrichment_table` additionally had four
`HeaderValue::from_str(..).unwrap()`
calls — outright panics, not 500s, on the same class of input. Those are
gone.
Other `HeaderValue` construction sites (router `user_email`,
`x-api-node`)
already guard with `unwrap_or_else` and are untouched.

## Organization identifier validation

Investigating how such an org existed at all: `create_org` validates the
display name and assigns a uuid identifier, but
`core::organization::check_and_create_org` takes the identifier
**verbatim from
the URL path** with no validation. It is reachable from any
root-authenticated
ingestion write (`ZO_CREATE_ORG_THROUGH_INGESTION`, default true), which
is how
the fuzzer provisioned it.

`validate_auto_created_org_id` (ascii alphanumeric plus `-` and `_`) now
guards
both auto-create entry points. It runs **after** the existing-org
lookup, so it
only gates new creation: no deployment loses an org it already has, and
SSO
auto-provisioning from IdP group names keeps working for normal
identifiers.

## Verification

Against a local server with the CI payload:

- stream create → `400 {"code":400,"message":"invalid data type:
v^0K"}`
  instead of the 500
- ingestion to the exotic org → 404; `autoorg-1` still auto-creates and
ingests
- valid create, duplicate create, settings update, settings on a missing
stream,
  delete, enrichment upload → correct codes, no panics

Two edits sit under `#[cfg(feature = "cloud")]` and cannot be compiled
here:
swapping in `Cargo.toml.openobserve` fails inside o2-enterprise itself,
whose
`main` has not yet picked up the `get_orm_client_rw` split from #13927.
They
were verified by temporarily un-gating them and stubbing the cloud-only
predicate.
2026-08-25 11:14:03 +00:00
91e524ce2d feat(dashboards): interactive table cells with in-place Logs Explorer drawer (#13675)
Design At: designs/dashboards/interactive-table-cell-drilldown

What changed
New component: DashboardLogDrawer.vue

Clicking a cell in a dashboard table panel now opens a side drawer
scoped to that field/value filter. The drawer shows filtered log events
with a SQL editor (Ctrl+Enter to run, Reset to restore default),
server-side pagination with configurable page size (50/100/200/500), and
an event detail sub-drawer on row click.

Event detail has three tabs:

Insights — Field Anomaly Profile (rarity scoring vs current page), Event
Timeline (SVG histogram of event frequency across the selected time
range), Log Patterns (grouped by structure via /patterns/extract, each
with "Open in Logs" that prefills the log explorer), and Surrounding
Events (events from the same stream in a configurable ±1–10 min window,
paginated 20/page with per-row expand)
Details — key/value field list with copy buttons
JSON — raw event JSON
Modified: PanelSchemaRenderer.vue

Fixed type mismatch: route.query.cell_panel returns string; panel schema
IDs may be number — comparison now uses String() on both sides
Added cell_event_ts to the URL param cleanup list so it's cleared when
the outer log drawer closes
Modified: en-US.json — added complete panel.logExplorer key tree
including the new insights subsection

Modified: design-debt-baseline.json — updated baseline after audit (new
file's rawVarInComponent uses are all in sanctioned color-mix()
positions)

URL persistence
All drawer state is encoded in URL params (cell_panel, cell_field,
cell_value, cell_stream, cell_stype, cell_t0, cell_t1, cell_event_ts).
Sharing the URL reopens both drawers and restores the exact event — on
mount it does a WHERE _timestamp = {ts} targeted fetch (size 1) rather
than searching through paginated results.

Zero backend changes
Uses only existing endpoints: search (SQL), search (histogram via
histogram() function), and patterns/extract.

---------

Co-authored-by: Shrinath Rao <shnath@openobserve.ai>
Co-authored-by: ktx-abhay <abhay.padamani@kiara.tech>
Co-authored-by: Abhay Padamani <141122205+ktx-abhay@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-25 10:31:13 +00:00
Hengfei Yang ccd254ad00 refactor(infra): split ORM read/write clients, retire the sqlite write lock (#13927)
Paired with o2-enterprise `refactor/orm-rw-ro-client` (same branch
name).

## Problem

Every ORM caller went through a single client backed by the sqlite
**write** pool, so writes were kept apart only by `table::get_lock()` —
a convention 365 call sites had to remember. Forgetting it surfaced as
`(code: 517) database is locked`: a deferred transaction that reads and
then upgrades to a write finds the snapshot moved under it, and sqlite
reports that **without ever consulting `busy_timeout`**, which is why
the failures were immediate rather than retried.

## Approach

Serialization now comes from the pool, not from a lock. The sqlite write
pool holds **one connection** — all sqlite allows anyway — so writers
queue on acquire and there is nothing to forget.

That is only affordable because reads move to the read-only pool.
Measured, same load, fresh data dir each run:

| | reads on the write connection | reads on the RO pool |
|---|---|---|
| 200 concurrent reads | p50 **135 ms** | p50 **1 ms** |
| writes during mixed load | p50 **189 ms** | p50 **14 ms** |

Both were 100% success with zero lock errors, so this is purely
throughput — but without the split, reads queue behind the single writer
and drag writes down with them.

## API

`infra::db` now exposes three functions and nothing else:

```rust
pub async fn get_orm_client_ro()  -> &'static DatabaseConnection
pub async fn get_orm_client_rw()  -> &'static DatabaseConnection
pub async fn get_orm_client_ddl() -> &'static DatabaseConnection
```

The statics behind them, every `connect_to_orm_*`, and the raw
sqlite/postgres pools are now private or `pub(crate)`. `ORM_CLIENT`,
`connect_to_orm` and `table::get_lock()` are gone, as is the raw
`CLIENT_RW` mutex (56 lock acquisitions, plus the 52 guard derefs that
existed only to reach through it).

Final split: **RO 247 / RW 360 / DDL 9**; `get_lock` 0, `ORM_CLIENT` 0,
`CLIENT_RW.lock()` 0.

## How read/write was decided

Per call site, then verified by resolving each call through **its own
file's imports** (bare-name matching is useless here — `get_by_id`,
`list` and `update` collide across modules) and propagating write-ness
along the edges that hand a connection to a callee.

A wrong write client only costs throughput; a wrong read client fails in
production. So the audit is zero-miss in the read direction, and a site
is only downgraded when *every* connection-passing callee resolves.

## Behaviour changes on sqlite

All three make sqlite match what postgres already does today — postgres
has never had this lock:

- check-then-write pairs like `folders::put` are no longer atomic; they
rely on their unique index (`folders_org_folder_id_idx`)
- `db::put` runs two sequential transactions, so concurrent puts to one
key are last-write-wins
- `db::delete` runs several statements outside a transaction

`write_pool_is_single_connection_on_sqlite` is now the **only** guard
against reintroducing 517 — please don't drop it.

## Review guide

The mutex removal is separable if you want to look at it on its own:
`src/infra/src/{db,file_list,scheduler,pipeline,schema/history}/sqlite.rs`.
Everything else is the client split.

## Verification

- `cargo check --workspace --all-targets` and the CI clippy command:
clean
- Tests match a HEAD baseline built with the same `.env`, exactly: infra
1410/2, core 1629/1, db 431/0, api-management 179/0. The three failures
are pre-existing and environmental (two need a postgres that isn't
running locally)
- Static scans, all zero: queries on the pool inside an open
transaction; transactions spanning a second connection acquisition;
ro/rw misclassification
- Runtime smoke on a fresh data dir: 200 concurrent reads, then 100
reads + 100 writes simultaneously — 400/400 HTTP 200, rows persisted,
**0** lock errors, 0 ERROR/WARN lines
- Enterprise verified via the `Cargo.toml.openobserve` swap (`cargo
check --all-targets`): clean

Also fixes `users::tests::set_up`, which truncated shared tables but
released its lock before the test body ran, letting a concurrent test
clear rows mid-test.

https://claude.ai/code/session_016ouf1RxiMpiRw5W7GvCUDh
2026-08-25 08:37:50 +00:00