Commit Graph
924 Commits
Author SHA1 Message Date
Ashish Kolhe bafc2be4f0 fix(correlation): P0/P1 fixes from service-streams correlation review (#13668)
## Summary

OSS half of the correlation P0/P1 fix set from the line-by-line review
(`docs/ak_review/correlation-line-by-line-review-2026-08-06.md`).
Companion enterprise PR: same branch name in o2-enterprise (required for
CI manifest pairing).

### Fixes (by review finding)

| Finding | Fix |
|---|---|
| **F14** | `StreamInfo.dropped_dimensions` contract field
(`#[serde(default, skip_serializing_if)]`, backward-compatible wire
format) + TS type |
| **F8** | Shared coverage-based identity-set ranking
(`resolve_best_set_by_coverage`) in `config::meta::correlation` — full
coverage > ratio > count, declaration-order ties; ingest and read now
rank identically |
| **F10** | `save_identity_config` purges rows of removed/changed sets
(`obsolete_set_ids`) + cache clear + org reload event |
| **F6** | `_reset` routes through enterprise
`ServiceStorage::delete_all` (cache clear + super-cluster replication)
and emits an org-scope reload; super-cluster Delete consumer emits org
reload instead of per-service delete |
| **F19** | `infra::table::service_streams::put`/`delete_subset_orphans`
return deleted orphan disambiguations so callers can evict cache keys |
| **F2** | `config::utils::sql::validate_where_fragment` (sqlparser)
applied to the `filter` param at both `traces/latest*` interpolation
sites — statement smuggling and comment-truncation payloads → 400 |
| **F2/F38** | FE: shared
`quoteSqlIdentifier`/`quoteSqlLiteral`/`buildSqlCondition` adopted by
all six SQL builders (two trace paths were fully unescaped) |
| **F35** | `applyFilterOverlay`: filter edits resolve per-stream via
semantic groups instead of literal key identity; dead
`removeFilter`/`resetFilters` removed; `semanticGroups` threaded to
`CorrelatedLogsTable` parents |
| **F36** | `applyDimensionEditsToFilters`: dimension-bar edits work in
both key spaces (raw-field and semantic-ID), case-insensitive shared
field→group map |
| **F31** | `mergeSubjectOverrides`: subject overrides replace the
same-group key (never union two aliases into contradictory WHERE),
schema-less field guessing removed |
| F14 (FE) | Warning banner shipped, then removed by reviewer decision
(`dropped_dimensions` stays in the API) |

### Verification

- `cargo test -p config` correlation/service_streams/sql: 121 tests
green; enterprise-feature and OSS-only cfg builds both compile
- FE: 263 vitest tests green across the correlation suites; vue-tsc
clean
- **Live-verified** against a local build (fresh DB): 19/19 API checks
(raw-value emission, set filing, obsolete-set purge, reset cache
invalidation, orphan eviction, filter validation) plus browser checks
(escaped SQL on the wire: `"environment" = 'Prod-EU'`)

Deliberately out of scope (per review decision): F15, F17/F37, F9.
2026-08-06 11:49:39 +00:00
Ashish Kolhe 32d02d6781 chore(traces): make Service Graph and Services their own routes (#13659)
## What

Service Graph and Services Catalog were tabs on the Traces page,
switched via a `?tab=` query param on a single keep-alive route. They
are now real routes, reached from a hover flyout on the Traces rail
tile:

| Route | Notes |
|---|---|
| `/traces` | Traces search — keeps the **Spans / Traces** toggle |
| `/traces/service-graph` | enterprise-gated; redirects to Traces on OSS
|
| `/traces/services` | |

**Spans stays an in-page toggle** on purpose. It's a view *granularity*
that shares the query, stream and time range — you switch to it
mid-investigation and expect context to carry over. Datadog's Trace
Explorer does the same. Service Graph and Services are destinations;
Datadog, New Relic and Grafana all treat them that way.

## Why this wasn't just cosmetic

As tabs, all three views shared one `meta.title`, one history entry, and
the traces page's `searchObj`. That last part is why Service Graph and
Services Catalog inherited the traces page's selected stream — the
coupling this PR removes structurally.

## UI

Both pages now use the Agent Graph header pattern (`OPageLayout` +
title/subtitle/icon, date picker and last-run refresh in `#actions`),
with a subnav row carrying stream selector + search on the left and
view-type + layout on the right.

## Handoff

`view-traces` no longer mutates the shared store from a sibling tab.
Both views build the same filter string — extracted verbatim into
`viewTracesHandoff.ts` — and navigate to Traces with it as query params,
so the URL survives a reload. (Traces then canonicalises `filter=` to
its own base64 `query=` via existing code in `useTraces.ts:290`.)

## Two bugs this surfaced

Both were things the tab views inherited for free and now have to set
themselves — and both would have hit real users deep-linking these
pages:

- **`organizationIdentifier`** was only assigned inside the Traces
page's `loadPageData()`. Without it the catalog issued requests to
`/api//_search_stream` (empty org segment) and rendered nothing.
- **`?period=` / `?from=`+`?to=`** were only applied by the Traces
page's `restoreUrlQueryParams()`. Without it a shared link carrying a
time range silently opened on the 15-minute default.

## Tests

| Suite | Result |
|---|---|
| `Traces/service-graph.spec.js` | 14 / 14 |
| `Traces/service-catalog.spec.js` | 10 / 10 |
| `RegressionSet/Traces/traces-ent.spec.js` | 1 / 1 |
| Unit (`Navbar` + `plugins/traces`) | 2514 pass |

Also clean: `type-check`, `eslint` (0 errors), `lint:design:strict`.

Test-side changes: navigation POMs go through the rail flyout instead of
the removed toggles; the graph POM pins an explicit window, since on its
own route the graph no longer inherits the traces page's range. The
shared auth fixture now verifies against `navbar-main-nav` — it keyed
off `menu-link-/-item`, which the current rail no longer renders, so
suites failed setup even after a successful login.

Filter-building coverage moved with the code into
`viewTracesHandoff.spec.ts` (21 tests) rather than being deleted
alongside the component tests that asserted the old in-page handlers.

## Known / follow-up

- `RegressionSet/Traces/traces-regression.spec.js` has 2 failures (trace
column sorting, pagination). **Pre-existing** — they fail identically
against production on `main`, and the Service Catalog test in that file
passes.
- Both views still default their stream picker to the traces page's
selected stream. Defaulting Service Graph and Services to **all
streams** is a deliberate follow-up: on our own instance the `default`
stream shows 22 topology nodes vs 71 across all streams, and services
like `o2-ai` span 4 streams. Graph is a one-line change (the API already
supports it); the catalog needs a backend decision, so it's out of scope
here.
2026-08-06 10:30:03 +00:00
Shrinath Rao 13acdd61da fix(web): hide LLM Model Pricing settings on OSS builds (#13651)
## What

Hide **LLM Model Pricing** on OSS builds and make it an
enterprise/cloud-only feature, end to end (UI + e2e coverage). Paired
with o2-enterprise PR on branch `fix/hide-llm-model-pricing-oss`.

## Why

Model Pricing was surfaced on OSS (settings rail + navbar flyout), gated
only on the `model_pricing_enabled` backend flag with **no**
enterprise/cloud check — unlike its sibling AI feature **LLM Providers**
(`(isEnterprise || isCloud) && online_evals_enabled`). Product decision:
it should not appear on OSS. This brings it in line with that sibling.

## Changes

**Web (hide on OSS)**
- `web/src/components/settings/index.vue` — settings rail item now
`visible: (isEnt || isCloud) && !!z.model_pricing_enabled`.
- `web/src/lib/core/Navbar/navGroups.ts` — `modelPricing` gate predicate
now requires enterprise/cloud (mirrors `llmProviders`), so the
collapsed-navbar flyout matches the rail.
- `web/src/composables/shared/useManagementRoutes.ts` — `model_pricing`
+ `model_pricing/edit` routes registered only inside the `isEnterprise
|| isCloud` block (defense-in-depth; matches `llm_providers`), so the
route isn't reachable by direct URL on OSS.

**E2E (move coverage to ENT)**
- `tests/ui-testing/ci-matrix/ci_matrix.json` — drop
`model-pricing.spec.js` from the OSS `GeneralTests` shard. It's added
back on ENT via the enterprise overlay (`ci_matrix.ent.json`, in the
paired ENT PR), so the merged ENT matrix runs it exactly once (verified
with `build-ci-matrix.js`).
- `.github/workflows/playwright.yml` — remove `ZO_MODEL_PRICING_ENABLED:
true` (OSS no longer exercises the feature; ENT `playwright.yml` keeps
it).
-
`tests/ui-testing/playwright-tests/GeneralTests/settings-form-validation.spec.js`
— gate the *ModelPricingEditor* describe block to a clean SKIP on OSS,
mirroring the existing Domain Management / Org Storage / Correlation
blocks.
- `tests/ui-testing/pages/generalPages/settingsFormValidationPage.js` —
`navigateToModelPricing()` now returns a boolean so the suite can detect
the OSS build and skip (mirrors `navigateToDomainManagement()`).

## Not changed (deliberately)

The backend `ZO_MODEL_PRICING_ENABLED` default (`true`,
[config.rs](src/config/src/config.rs)) is **left as-is**. It lives in
the shared config crate used by both editions, so flipping it to `false`
would also regress real ENT/Cloud deployments (feature off by default).
The UI gate already ensures the page never shows on OSS regardless of
the flag. Making the backend itself enterprise-only would be a separate,
backend-owned change.

## Behavior

| Build | Model Pricing UI | e2e |
|-------|------------------|-----|
| OSS | Hidden | `model-pricing.spec.js` not run;
settings-form-validation block SKIPPED |
| Enterprise / Cloud | Shown (if flag set) — unchanged |
`model-pricing.spec.js` runs (via ENT overlay) |
2026-08-06 01:54:37 +00:00
Bhargav bbf2aa37af feat: Issue ent 2289 mobile rum sdk error tracking v2 (#13645)
Design at: designs/tree/main/mobile-rum-sdk-evaluation/design-docs
2026-08-05 12:18:13 +00:00
ktx-vaidehiandKirtan Thakkar 3285a31e6f fix: i18n lint enforcement (#13439)
Co-authored-by: Kirtan Thakkar <kirtan@kiara.tech>
2026-08-05 10:16:00 +00:00
ktx-akshay 95e1ee83c3 fix(search): improve loading behavior on stream change (#13636) 2026-08-05 09:33:35 +00:00
Sai Nikhil 232b38351d fix: search history routes change (#13512) 2026-08-04 09:37:20 +00:00
Sai Nikhil 76f1df06fa fix: alert timezone fix on main (#13519) 2026-08-04 09:36:56 +00:00
Ashish Kolhe b31ff6c7fe chore: incident destinations, source rename, and Alert Sources UX redesign (#13612)
## Summary
- Adds a typed `destinations` field to incident_integrations (migration
+ entity + table ops), threaded through create/list/event-ingestion.
- Adds a PATCH endpoint to rename an alert source, including the org's
`default` source (rename allowed; delete stays blocked for it).
- Moves Alert Sources under the Reliability nav menu.
- Redesigns the Alert Sources list and Add/Edit drawer:
- Table-only landing page; Add Source opens a drawer instead of a route.
- Single row-level Edit action (name + destinations together), matching
the Destinations/Templates list pattern instead of per-field popovers.
- Fixed-layout, horizontally scrollable table with a pinned Actions
column; wrapped/truncated cells confined to their own columns.
  - Separate "copy token" action distinct from "copy URL".
- "Never resolves" warning moved from an unbounded banner list into a
tooltip on the status tag/icon.
- Removed the confusing "Shared token" badge and reworded the
no-senders-yet caption.
- All 3 setup steps shown together in the drawer (checklist-style)
instead of hiding steps 2-3 behind a post-save view.
- Destination creation now routes to the real Destinations page instead
of a fake local-only option.
- Fixed the test curl snippet: no stray `#` comment in the copyable
text, and a payload the backend's generic-format detector actually
accepts.

## Test plan
- `cargo build`/`clippy -D warnings` clean on `infra`,
`openobserve-api-management`, `openobserve-api-http` (enterprise
feature) and OSS-only workspace check.
- 151 frontend vitest tests passing (stable across repeated runs).
- Manually verified against a local enterprise build + Vite dev server.
2026-08-04 05:16:32 +00:00
Prabhat Sharma 624425a08a feat(editor): rebuild query autocomplete, and fix the value path behind it (#13586)
Design at autocomplete/autocomplete.md

Rewrites the query editor's autocomplete across every surface that has
one, and
follows the field-value path far enough to fix the bugs it was hiding.
53
commits, in six phases; each phase was written test-first, reviewed
adversarially, and checked in a browser before the next one started.

## What was wrong

The completion providers were wired inconsistently. Six surfaces bound a
resolver that was never defined, so their value completion could not
resolve at
all — SLO, dashboards and anomaly detection among them. Traces never
bound
`:suggestions`. Alerts never called `updateStreamKeywords`. PromQL ran a
second
editor runtime purely for its grammar, and asked
`/prometheus/api/v1/series`
— the heaviest endpoint available — for data the stream schema already
has.

## Phases

**1 — icons, staleness, quoting.** Correct `CompletionItemKind` glyphs,
stream
keywords refreshed when the stream changes, `spath` quoting.

**2 — the function catalog.** Custom functions get one tab stop per
argument,
org VRL functions stop appearing twice, column types read from
`dataType` as
well as `type`.

**3 — signature help, hover, async completion.** Providers return real
`CompletionList`s with `incomplete: true`, so a list that arrives late
is
re-queried rather than pinned.

**4 — numeric ranking.** Numeric columns sort first inside a numeric
aggregate.

**5 — values for streams nobody has searched.** The value cache only
ever held
what a search had already returned, so a stream you had not queried
offered
nothing. Values are now fetched on demand, bounded (10s timeout, 60s
cooldown,
one in-flight request per key), and shared between the editor and the
builder.
PromQL joins the same path: labels from the stream schema, values from
the
shared cache. `/series` now has zero production callers.

**6 — one way to fetch a value.** Item 22 only, in this PR.

## The dashboard filter 400

Clicking the ✕ beside "Select Field" in a filter's dropdown sets the
column to
`{}`, and the watcher on `condition.column` then asks for that column's
values.
`loadFilterItem` built `fields: [row.field]` out of `undefined`,
JSON.stringify
turned that into `null`, and the server answered

```
400 {"code":400,"message":"SerdeJsonError# invalid type: null, expected a string"}
```

`_values_stream` answers 400, not an empty result, for each way a piece
of the
payload can go missing — and every one is reachable: an unresolved
stream alias
(`.find(…)?.stream`), a stream not yet chosen (`""`), a range not yet
set. The
range fails differently: `meta.dateTime` starts as `{start_time: ""}`
and
`""?.toISOString()` does not short-circuit, so it throws and both
callers
surface "Something went wrong!" for a lookup the user never asked to
fail.

Guarded at the two callers, where the payload is built and where the
throw
happens. Not at `fetchFieldValues` — a guard there is too late to stop
the
throw and would duplicate validation the callers need anyway. The
trigger is
left alone: re-picking a field must still load its values.

## Notable removals

CodeMirror is gone — it was still bundled for the PromQL grammar alone.
Bundle 26.3 MB → 25.43 MB; the PromQL chunks 442.7 KB → 20.6 KB. The
PromQL
term catalog went from 7 entries to 113, generated from Prometheus's own
tables
and vendored with its MIT notice.

## Verification

- 76 files, +10.5k/−1.4k, ~9,100 tests passing across the touched areas.
- `vue-tsc` clean, `npm run build` exit 0.
- Every phase checked in a running browser against a live instance,
including
the 400 above: the ✕ now produces no request and no toast, adding a
filter and
re-picking a field each still fetch once, and the value list still
populates.

## Known, deliberately out of scope

- A filter still fetches twice (once on add, once on open) — that is
item 23,
  the cache read, not this PR.
- `useMetricsExplorer.loadFilterItem` has the same unguarded
`toISOString()`
  pattern on a different surface.
- `services/metrics.ts` and `services/search.ts` still carry dead
  `get_promql_series` wrappers now that nothing calls `/series`.
2026-08-03 22:29:04 +00:00
Omkar Kesarkhane aedcc83a24 fix(synthetics): stop a browser replay without stranding a step in progress (#13592)
## Problem

Recording a browser test and replaying it, then pressing **Stop** in the
journey editor:

1. The step the replay was on stayed stuck in the in-progress (spinner)
state forever.
2. The extension did not stop promptly — it kept going for another step
or two.

## Root cause

One defect produced both symptoms, and it lives in the recorder
extension's player
(`CrxPlayer.stop()` in `openobserve/playwright-crx`): it did not abort
the in-flight
Playwright call. It only raised a flag that `_checkStopped()` reads at
the **start** of an
action — never while a `click` / `fill` / `expect` is pending, and those
carry a 60s timeout.

So after Stop: the current step ran to completion, then the loop
advanced and emitted
`stepStarted` for the *next* step before unwinding. That step could
never report a result.
`useSyntheticsRecorder` set `activeStepId` from it and `stepDotState`
rendered it as
`"active"` — a permanent spinner.

The player fix is a companion PR in `openobserve/playwright-crx`. **This
PR is the O2 half**,
which is also what makes the UI honest while the stop is in flight.

## Changes

- **`ReplayPhase` gains `stopping`** — the interval between the click
and the extension
confirming. Stop becomes a disabled, loading button in the same slot (no
layout shift) and
the banner reads "Stopping replay…", so the UI no longer claims the run
is over while it is
  still winding down.
- **`useSyntheticsRecorder.stopReplay()` owns the `running → stopping →
stopped` transition**,
the way `replay()` already owns `running → passed/failed`. It clears
`activeStepId`, since
the interrupted step never reports a result and nothing else would ever
clear it. Bounded by
the existing `COMMAND_TIMEOUT_MS`, so `stopping` cannot strand the
journey.
- **A replay generation counter** orphans the in-flight replay promise
on stop. Otherwise the
abandoned run answers later and knocks a newer `running` back to
`stopped`, or resurrects a
banner the user just dismissed. Latent before — making stop fast is what
would start firing it.
- **`stepDotState` renders `active` only while `running`**, so an
unresolved step falls through
to `pending` — an empty circle, which is the truth: it never completed.
- **Stream gating** — a late `stepReplayStarted` is ignored outside
`running`; a
`stepReplayResult` already in flight still counts toward "completed X of
N", because that step
  genuinely ran.
- `CreateBrowserTest.onStopReplay` collapses to pure delegation; its
route guard treats
  `stopping` as live.

Also on this branch: a small `cursor-help` → `cursor-pointer` change in
`MonitorTable.vue`
(separate commit, unrelated to the above).

## Deliberately unchanged

The incognito replay window is **not** closed on Stop, so window reuse
across
record → replay → re-run is untouched.
(`crxApp.close({closeWindows:true})` was not an option
anyway — it removes *every* incognito window, including the user's own.)

`stopReplayAndForget` (route guards, `beforeunload`), recording
start/stop, and the
passed/failed replay paths are untouched.

## Testing

Regression tests in `useSyntheticsRecorder.spec.ts`,
`BrowserJourney.spec.ts` and
`CreateBrowserTest.spec.ts` — **each verified to fail with its guard
removed**, including the
headline one: `replayPhase: "stopped"` with a non-null `activeStepId`
must render `pending`,
not `active`.

The end-to-end proof lives in the companion PR: a browser test drives
the real extension in
real Chrome, blocks a step for 40s, presses Stop, and asserts prompt
acknowledgement,
`stopped: true`, and that no later step was announced. It passes in ~5s
with the player fix;
without it the extension never acknowledges the stop within 15s at all.

## Verification

- `npm run lint` — 0 errors in changed files (4 repo-wide errors are
pre-existing in
  `eslint.config.js` and `TraceDAG.spec.ts`)
- `npm run type-check` — clean
- `npx vitest run src/components/synthetics src/views/synthetics
src/composables/useSyntheticsRecorder.spec.ts`
  — **817/817 passing across 44 spec files**

## Note for reviewers

This needs the companion `playwright-crx` player fix to actually stop
promptly; without it the
UI is honest but the extension still winds down slowly. The extension
`dist/` must be rebuilt
and reloaded in `chrome://extensions`.
2026-08-03 11:04:22 +00:00
Loakesh Indiran f16e8e53ff perf(synthetics): config caching, run-budget validation, HA correctness, step-grain Steps tab (#13580)
Synthetics performance and correctness work. Four areas: caching the
config reads on the job path, a false-alarm bug in validation, four HA
correctness bugs found by auditing what happens with more than one alert
manager, and the Steps tab moving to a step-grain stream.

Pairs with o2-enterprise#2325 — **merge this first**, that one calls
functions introduced here.

---

## Caching
- [x] Probe-token auth cache — 10s TTL, negative results cached so a bad
token can't force a query per request
- [x] Locations whole-table cache — 30s, serves `get` / `find_by_pool` /
`list_visible` from one load
- [x] Check definition cache via `get_cached()` — was read twice per job
(resolve + ack)
- [x] Agent capabilities cache via `get_cached()` — was read on every
lease poll
- [x] Org ingest-token cache — `find_default_enabled()` replaces three
hand-rolled `list_by_org().find(enabled)` copies
- [x] `list_by_locations()` — one batched agents query, replacing one
per location

## Cross-node invalidation
- [x] `coordinator::synthetics` — event invalidation following the
alerts/pipelines pattern
- [x] Write paths call `invalidate_and_publish()`, the watcher calls
plain `invalidate_cache()` so events don't echo
- [x] Deletes emit a delete event, so coordinator keys don't accumulate
one per check ever created
- [x] Ingest-token invalidation reuses the existing
`/org_ingestion_tokens/` watch — no second event stream
- [x] SQLite write mutex released before emitting — `get_lock()` and the
coordinator's `put()` take the same lock, so emitting while holding it
deadlocked the process

## HA — correctness with more than one alert manager
Confirmed with @subhradeep that a region can run multiple alert
managers. The synthetics scheduler, dispatcher, reaper and staleness
watcher all run on **every** one of them.

- [x] **Duplicate runs** — `claim_due()` claims due checks with `SELECT
… FOR UPDATE SKIP LOCKED`, advancing the schedule inside the same
transaction
- [x] **Duplicate "location down" pages** — the one-shot flag moved from
an in-process `HashSet` to `synthetics_locations.down_notified_at`,
claimed with a CAS
- [x] **Duplicate run completion** — `increment_jobs_done` claims
completion via `completed_at IS NULL`; two acks of the same run could
both be told they completed it
- [x] **Inert dedup index** — `scheduled_ts` is now the schedule slot,
not each node's tick clock, so the existing `synthetics_jobs_dedup_uq` +
`ON CONFLICT DO NOTHING` actually fires
- [x] Migration `m20260803_000001` adds `down_notified_at`, defaulted so
no backfill is needed
- [x] `DB_SCHEMA_VERSION` 62 → 63 — without it the migration never runs
on an already-upgraded deployment

## The false-alarm fix
- [x] Validate against the run budget (840s), not the job lease (900s)
- [x] The three bounds are deployment-configurable:
`O2_SYNTHETICS_MAX_CHECK_BUDGET_SECS` / `_JOB_LEASE_SECS` /
`_MAX_NET_TIMEOUT_MS`
- [x] Rejected limits are logged, not fatal — nothing installed means
validation keeps the conservative defaults
- [x] Browser retries capped at 2; protocol types stay at 3; default
stays 0 for every type

## Steps tab — the step-grain stream (B10)
- [x] Three `GROUP BY`s over the step stream replace downloading 5000
execution rows and tallying their JSON in the browser
- [x] Falls back to the old tally when the stream is empty, so the tab
does not blank for checks whose agents have not been upgraded
- [x] Gated on the `step_id` column rather than stream existence — a
partially-created stream would otherwise cause four failed queries

## Overview KPIs
- [x] Count tiles summed from the cached histogram; `buildKpiSql`
deleted

## Rename
- [x] `synthetics_monitors` → `synthetics_checks`, `monitor_type` →
`check_type`
- [x] Three UI call sites that read the old field names, including a
delete guard that silently disabled itself

---

**Why validation moved off the lease.** Three bounds stack:

```
check worst case  <=  max_check_budget_secs  <  job_lease_secs
     840s                    840s                   900s
```

Validation used the lease, so the server accepted checks the execution
environment cannot finish. On the managed path the probe is a Lambda,
and a function that hits its timeout is killed mid-run — lease headroom
is irrelevant once the process is gone. The net probe's function timeout
had drifted to **60s** while validation accepted up to 900s, so a plain
`timeout_ms=30s, retries=2` (worst case 100s) was accepted and then
killed, reporting a failure the target never had.

**Why the limits are ENT-declared but OSS-consumed.** Synthetics is
enterprise-only, so the values live in `SyntheticsConfig`. The
validation they bound lives in `config`, which cannot depend on
`o2_enterprise`. `SyntheticsLimits` + `init_limits` is the seam, falling
back to built-in defaults in OSS builds and tests. A bad limit is logged
rather than fatal: refusing to start ingest, search and dashboards
because a probe ceiling is misconfigured would be the worse outage.

**The duplicate-run bug.** `fetch_due` was a plain `SELECT` and the
advance was an unconditional `UPDATE … WHERE id = ?`, so nothing stopped
two alert managers firing the same check. Each inserted its own
`synthetics_runs` row under a fresh KSUID, which made the resulting job
rows genuinely distinct — `lease_batch`'s `status = 0` guard cannot
collapse them, because they are not contending for one row. N alert
managers meant every check ran N times.

`SKIP LOCKED` rather than a compare-and-swap because it *distributes*: a
second scheduler skips the rows the first holds and picks up different
due checks, so N replicas split the backlog. A CAS gives the same
exactly-once guarantee but every node reads the same candidates, one
wins them all, and the losers each issue a doomed `UPDATE`. The lock and
the advance share a transaction because the locks only live until
`COMMIT`; the fan-out stays outside it so config rows are not locked
across N job inserts.

**Duplicate run completion needed no second alert manager.** The
increment was atomic, but the completion decision re-read the row in a
second statement and returned "complete" to whichever caller saw
`jobs_done >= job_count` — which both acks of a 2-job run do. Acks are
served on ingesters, of which every environment runs two, and the probes
of one run finish independently. Effect: `consecutive_failures` counted
one run twice on every check, plus a genuinely duplicated notification
on checks left at the default `cooldown_mins = 0`.

**The Steps tab was truncating silently.** It downloaded up to 5000
execution rows and tallied `last_attempt_steps` in the browser. On the
busiest check measured — 18,079 executions over 7 days — that cap
covered 4 of the 7 days and reported a **56.3% fail rate against a true
33.7%**. The win is correctness; the ~18MB payload reduction is
secondary.

## Verification
`cargo check --workspace`, clippy, fmt clean. Config 98, infra 33
synthetics tests pass. Frontend: **839 synthetics tests pass**. Three
frontend spec files fail to import on `useLocalTimezone` — verified
identical on clean `main`, unrelated to this branch.
2026-08-03 10:14:11 +00:00
4fa71dd15c feat(ui): calm-signal pass over IAM and Synthetics Monitors (#13568)
Second batch of the "Calm Signal" pass (after #13562: Streams,
Pipelines, Nodes).

## Users
- Summary strip that doubles as the role facet. Tiles are the roles
**actually present in the data**, so custom roles appear without being
enumerated in code. Counts are role *membership* — a user with Admin + a
custom role is counted under both, so tiles intentionally don't sum to
the total. Top 5 by size; the tail folds into one "+N more roles" tile
counting unique users.
- Pending users now show the invited chip on the enterprise path (it
only appeared on the open-source one).
- Custom roles get one stable neutral chip instead of an untyped tag
whose colour was derived from the role's spelling.
- Removed a `<style scoped>` block whose only selector is used nowhere.

## Roles
- Member count per role, muted when nobody holds it. Comes from the
batched user→roles map — **one request for the org, not one per role** —
and is gated to enterprise, where that endpoint exists. Rows render
immediately; counts patch in when they land.

## Service Accounts
- Created reads as relative age, with a dot on keys minted this week.

## Synthetics Monitors
- Status counts were hidden inside a dropdown's option labels ("Passed
(12)"). They're now a strip that acts as the facet; the dropdown and its
dead options plumbing are gone.
- Health row rail, plus the red / muted row wash for failing and paused
monitors that Alerts and Pipelines already use.
- Last check renders through `OTimeCell` instead of a local time-ago
copy.
- **Fixes styling that never applied:** the uptime sparkline and its
hover tooltip referenced `.spark` / `.stt-*` classes that are defined
nowhere in the repo, so the bars had no size and the tooltip no surface.
Rebuilt with tokens.
- `timezone` is a prop now — a leaf table shouldn't reach into the
store.

## No strip on Dashboards, Roles, Service Accounts or Groups
Their candidate tiles were structurally constant (System = 1),
near-always zero, derivable from a neighbour, or already in the table
footer. Dashboards was already compliant (favourites, owner cell,
relative dates) and is untouched; Groups' API returns names only, so
per-row counts would cost one request per group.

`calm-signal.md` records both rules this batch produced: the
three-question test for a tile, and rail-vs-wash (a full-row wash only
when the state means *act now*, is rare, and the row is the unit of
action).

## Testing
`lint`, `lint:design:strict`, `lint:styles`, `lint:tokens`,
`lint:token-purity`, `type-check` pass. 885 unit tests across IAM and
Synthetics pass.

Synthetics needs a monitor in each state to eyeball the rail/wash;
Roles' member column needs an enterprise build (403s and hides itself
otherwise).

---------

Co-authored-by: ktx-vaidehi <134508096+ktx-vaidehi@users.noreply.github.com>
Co-authored-by: ktx-vaidehi <vaidehi.akhani@kiara.tech>
2026-08-03 08:54:15 +00:00
Omkar Kesarkhane fc8e3c1657 fix: redesign the step editor and the run evidence surfaces (#13582) 2026-08-03 12:45:16 +08:00
Ashish Kolhe 23e816b1e4 feat: External Alert Sources incident ingestion (#13578)
## Summary
Adds a full "External Alert Sources" feature so OpenObserve Incidents
can serve as ingestion/correlation layer for third-party alert tools
(Grafana, Alertmanager, generic webhooks):

- Token-authenticated inbound webhook endpoint (`o2iat_` tokens) that
normalizes Grafana/Alertmanager/generic payloads and correlates them
into Incidents alongside internally-generated alerts.
- Incident integrations CRUD (create, list, enable/disable, rotate
token, delete — default integration protected from deletion) with
per-sender observed status.
- Settings UI: single always-open alert sources table (merged from an
earlier two-panel design), shared-token badge, add/rotate/delete flows.
- Incident detail UX: contributing-alert Source/Labels columns, "view
raw payload" action (surfaces the exact webhook JSON received), and an
external-source badge on the incident header so it's obvious at a glance
whether an incident has external contributors.
- **Auto-resolve**: incidents now close automatically once every
contributing external alert reports `status: resolved` — previously
required manual resolution even when the source alert cleared.

## Notable follow-up fixes bundled in
- Rebased onto latest `main`; repaired two build breaks surfaced by
upstream changes that landed after this branch diverged
(`CorrelationSubject` refactor's new `eval_level` param, and a new
`SeverityEscalated` correlation outcome) — both textually merged clean
but were semantically incomplete for the external-event path.
- Removed the `O2_INCIDENTS_EXTERNAL_SOURCES_ENABLED` enterprise config
flag (companion removal in o2-enterprise) — external sources are now
gated solely by `O2_INCIDENTS_ENABLED`, removing a redundant toggle.

## Test plan
- [x] `cargo build --no-default-features --features enterprise` — clean,
no warnings introduced
- [x] `cargo clippy` on touched crates (`infra`, `openobserve-core`,
`openobserve-api-management`, `openobserve-api-http`) — no new warnings
- [x] `cargo fmt --check` — clean on all touched files
- [x] `cargo test` — all tests in touched modules pass
(`alert_incidents`, `incident_integrations`, `external_alerts`,
`alerts::*` — 274/274); two unrelated flaky DB-pool-timeout tests
(`db::tests::test_delete`,
`table::ratelimit::tests::test_add_batch_missing_rule_id`) confirmed
pre-existing and pass in isolation
- [x] Frontend: `vitest run` on all touched components — 156/156
passing; `vue-tsc --noEmit` clean
- [x] Manually verified end-to-end against a running dev server:
fired/resolved external alerts, confirmed auto-resolve, delete, and
raw-payload retrieval all work against real HTTP calls
2026-08-02 16:16:50 +00:00
Prabhat Sharma d99ecff3e8 feat(nav): add Reliability menu, restore deferred SLO routes (#13577)
Groups the alerting surface under one left-rail tile, and un-defers the
SLO backend that shipped disabled.

> **Lands with** openobserve/o2-enterprise#2321, which adds
`ROUTE_PERMISSIONS` for the restored SLO endpoints — the route-coverage
test pairs the two, so this one alone fails it.

## Reliability menu

New `reliability` nav group absorbing **Alerts, SLOs, Incidents**, plus
**Notification Destinations** and **Templates**, which move out of
Settings — they are alerting configuration, not deployment
configuration. Settings keeps Pipeline Destinations (that one really is
pipeline config); its group is renamed "Destinations".

Their routes are **top-level and flat** (`/alert-destinations`,
`/alert-templates`) rather than nested under `/alerts`. They are
siblings of Alerts, not sub-pages of it — nesting them made the URL
claim otherwise and the rail believed it, highlighting Alerts alongside
them.

- Route **names** are unchanged, so the ~15 call sites that navigate by
name are untouched.
- Old `/settings/*` paths redirect, with the query preserved —
`?action=import` deep links keep working.

## SLO routes

`/api/{org}/slos` answered **404**. The routes were deliberately
unregistered before the last release (`6977b4020d`, `ac78a622e1`) while
the rest of the feature — `core/src/slo/*`, the infra tables, both
migrations, the `slo_maintenance` job, all seven handlers — stayed in
the tree. This restores the five registrations and the OpenAPI entries,
recovered from the pre-strip commit rather than rewritten.

`ZO_SLO_ENABLED` still **defaults to false**. It is now published as
`slo_enabled` on `/config`, so the menu entry follows the flag instead
of offering a page the API answers with 501.

## Nav active-state

Active state was decided per child, so any section whose path prefixed
the current route lit up alongside it. It is now resolved once per
flyout: exact route-name match wins, else the **deepest** path prefix —
so a genuine drill-down like `/alerts/detail/:id` is still attributed to
Alerts. `placeAfter` also accepts a group key, letting Data anchor on
the Reliability tile it follows rather than on an item that tile
absorbs.

## Tests

`tests/ui-testing` referenced `[data-test="alert-destinations-tab"]` /
`alert-templates-tab` in six files — those data-tests were on the
Settings rail items removed here, so the locators would have matched
nothing and hung until timeout. Those paths now go through the existing
`openNavFlyoutChild` helper, extended with the Reliability group. Two
URL regexes that would have silently mismatched on the hyphen are fixed
(one ended in `.catch(() => {})`, so it would have degraded to a silent
15s stall).

## Verification

- 3597 unit tests pass; `vue-tsc` and eslint clean; enterprise build
compiles.
- `playwright test --list`: all 141 tests across 23 files load, no
module errors.
- Against a live server: `GET /slos` → 200, `POST /slos/move` → 422 (so
the literal wins over the `{slo_id}` catch-all), `GET /slos/x` →
handler-level `{"code":404,"message":"SLO not found"}`.
- In the running app: `/alert-destinations`, `/alert-templates` and
`/alerts` each highlight exactly one row;
`/settings/alert_destinations?...&action=import` redirects with the
query intact; rail order unchanged.
2026-08-02 05:44:01 +00:00
Prabhat Sharma 8f60dbcc82 fix(metrics): widen rate windows when the scrape interval overstates cadence (#13576)
## Problem

Metrics explorer cards for histograms, counters, and summaries showed
**"Too few samples for a rate"** even though the streams had data. The
rate window is sized from the org's configured `scrape_interval` (e.g.
15s → `[1m]`), but when data actually arrives less often (the OTel demo
app exports every 60s), `rate()` almost never finds the two samples it
needs — so every rate-based card rendered the sparse message while
gauges charted fine.

Reproduced against a live instance:
`sum(rate(app_cart_add_item_latency_count[1m]))` → empty; the same query
with `[4m]` → `0.172/s`.

## Fix

**Widened-window retry** (`useMetricsExplorerGrid.ts`,
`computeWidenedRateWindows` in `metricDefaults.ts`): when a rate-based
card comes back empty but the presence probe confirms samples exist in
the window, retry with geometrically wider windows — 4×, then 16× the
standard window, capped at half the range — and chart the first rung
that answers. The sparse message now only appears when no honest window
could produce a chart.

**Editor / dashboard handoff** (`MetricsExplorer.vue`): the drill-in
normally hands the editor `$__rate_interval`, which resolves from the
same overstated scrape interval — opening a blank editor for a metric
the card visibly charts. The preview now remembers which rung worked
(`widenedRateWindow`, persisted through the card cache like
`nanGuardApplied`), and both the card→editor drill-in and the
favorites→dashboard conversion hand over that concrete window instead.
Cards that never needed widening keep the adaptive `$__rate_interval`
behavior.

Supporting changes:
- `previewKeysOf` enumerates the widened query keys so refresh
invalidates them instead of replaying stale retries from the queue
cache.
- Persisted-cache identity gains a shape version (`v: 2`) so pre-field
entries (widened data with no memory of the window) miss once and
re-learn live.
- A failing widened retry settles for the sparse answer already in hand
rather than repainting the card as an error; cancellations still abort.
- During a refresh the loading state carries `widenedRateWindow`
alongside the old results it keeps on screen, so a mid-refresh drill-in
stays correct.

## Verification

- Unit tests: retry ladder math (caps, dedup, no-room-to-widen), first-
and second-rung success, all-rungs-fail keeps the sparse card, window
carried on the preview, handoff passes the concrete window vs
`$__rate_interval` — 182 tests across the four spec files pass; ESLint
and vue-tsc clean.
- Live against a dev backend: previously-sparse `app_cart_*` cards all
render (network log shows `[1m]` → empty, presence probe, `[4m]` →
data); drill-in opens the editor seeded with `sum(rate(...[4m]))` and a
rendered chart, from both the live-query and cache-restored paths.
2026-07-31 19:45:26 +00:00
ktx-vaidehi 77d62ddee2 fix(logs): bound the last selected column to the viewport (#13573)
`body` — and any last selected field — sized itself to its longest value
instead of taking the leftover width, dragging the row into thousands of
pixels of horizontal scroll. Two separate causes:

- the absorber marked the column `meta.autoWidth`, which in OTable means
"no width at all", and a horizontalScroll table carries `min-w-max`,
which sizes every column to its max-content;
- sized columns in a scrolling table had no `max-width`, so as soon as
another field was added and `body` stopped being the absorber, its own
text stretched it to ~3000px.

Split the two behaviours the pre-migration table kept separate: plain
`autoWidth` stays content-sized (the default `source` column was `width:
auto` and scrolled), while the new `meta.fillRemaining` takes the
leftover width but stays inside the container and ellipsis-truncates
(pre-migration `flex: 1 1 auto; overflow: hidden`). A bounded filler
drops the table's `min-w-max` and pins its sized siblings at their own
size in both directions.

Opt-in, so the other horizontalScroll tables (traces, dashboard panels,
monitors, search history) keep their existing layout untouched.

Verified against the pre-migration build (044f561506) running side by
side on identical data: column widths and scroll offsets match exactly.
2026-07-31 14:12:12 +00:00
Omkar Kesarkhane bbed40d8f4 feat(synthetics): author-owned locators, and step-scoped run evidence (#13563)
## Summary

Three synthetics workstreams on one branch: author-owned locators (Phase
2b), retirement of version-1 monitors (Phase 2c), and making a failed
browser step diagnosable without leaving the Steps tab.

Everything else in the synthetics suite passes (422 tests across the 21
suites the evidence work touches).

## Author-owned locators (Phase 2b)

The recorder proposes a locator bundle; the author owns its order.
Schema, combined-locator build/read, provenance pinned across the
recorder bridge, and the payload contract tested from both sides.

`337911158d` `e63f8f429f` `50b811a865` `2a978b9408` `c8c081c3c5`
`35ef1c36cb`

## Version-1 retirement (Phase 2c)

Stop creating v1 monitors, delete the v1 migration path and its step
fields, drop `steps_version` from the schema.

`b58b03077d` `75984ad222` `68a33e885d` `a2bffae7b7`

## Run detail — evidence, scoped to the step

Implements §5.3 of `docs/synthetics/step-failure-evidence-design.md`
("render the bundle, scoped to the step"), which was specified and never
built: only the kind-grouped run-level Evidence tab shipped.

**Per-step page activity.** The expanded step now shows the
`evidence.ndjson` events attributed to it — severity-ranked, capped at
5, with "View all N →" deep-linking into the Evidence tab pre-filtered
to that step. One shared `OTable` wrapper renders rows for both surfaces
so they cannot drift, and one composable fetches the bundle once for
both.

`a483796d57` `a7cbe0a3e0` `f63973285e` `eb7ec4f3e6` `1ed226febe`
`e3d969bcb2`

**Settle signals were attributed to the wrong step.**
`failure_detail.settle_signals` accumulates across the whole journey. On
live run `3HFCZ3fm` it held 13 signals — one from step 2, six from step
8, two from step 14 — while the failed step 15 owned none of them, and
the panel rendered all 13 under step 15. The signal that mattered
(`stale POST **/_search_stream, 30274ms`) belongs to step 14, which the
run's own error string says, and step 14 displayed nothing.
`last_attempt_steps[i].settle_signals` had carried correct per-step
attribution all along and was read by nothing.

`6ba6f8d633`

**Artifact URLs carried no origin.** `artifactUrl()` returned a bare
path while every other call in that service goes through `http()` with
`baseURL: store.state.API_ENDPOINT`. Artifact URLs go to `<img src>` and
`fetch()` directly, so in dev they resolved against Vite on `:8081`,
which has no `/api` route and no proxy — every screenshot and evidence
bundle 404'd. Verified against a running backend with a real key:
`:8081` → 404, `:5080` → 200. Production, served from the API's own
origin, is unchanged.

`baa7407cb5`

**The locator ladder was invisible.** `candidates_tried` lists only the
rungs the probe stood on, so a step that matched on its primary looked
identical to a step that had no second candidate — both one row, both a
grey `used_as_primary` badge. On live run `3HGDMrlU` the failing assert
had one authored locator and every `assert`/`fill` step in that journey
is built the same way, while `click` steps average 2.4 candidates. The
section now leads with "N of M tried", names a one-rung ladder as having
no fallback, labels outcomes in words instead of enum values, and dims
unreached rungs the way the timeline dims a skipped step.

`c05e1ce133`

## Test plan

- `npx vitest run src/views/synthetics src/components/synthetics/results
src/components/synthetics/StepEvidence.spec.ts
src/composables/synthetics src/composables/useSyntheticEvidence.spec.ts
src/services/synthetics.spec.ts` → 422 passing
- `npm run type-check` → clean
- `npm run lint:design:strict` → 2 pre-existing regressions above (see
Known failing)
- Behaviour on the run-detail changes was verified against live records
from `OpenObserve Sys Query` and `OpenObserve Cloud Happy Path` rather
than fixtures alone

## Not covered

- The "Add a fallback locator" action from the locator-ladder work needs
a step-level deep link; `synthetics/edit/:id` has no step anchor, so the
warning states the finding without an action.
- Browser-level confirmation of the artifact-URL and locator-ladder
changes is outstanding — the local backend's search service was
returning `tcp connect error` during verification, so those two were
confirmed against records and by unit test rather than in a running
page.
2026-07-31 11:19:46 +00:00
83d12fff04 feat(web): inline-rename page titles with auto-generated default names (#13520)
## What changed

Naming an alert, panel, workflow, pipeline or function meant inventing a
name up front in a form field before anything else could be configured.
Two pieces replace that.

**Inline rename** — the page title *is* the input. `OInlineEdit` /
`OFormInlineEdit` (`web/src/lib/forms/InlineEdit`): click to rename,
Enter or blur commits, Escape restores the pre-edit value. A `#trail`
slot carries the "Auto" badge in display mode only.

**Auto-generated names** — `utils/autoName.ts` builds the name,
`composables/useAutoName.ts` decides when it's allowed to win. A name is
AUTO until the user types, then it's theirs for good; clearing the field
and blurring re-arms auto. The generators describe only what is actually
configured, so a half-built panel yields a shorter name rather than a
speculative one, and an empty string when there is nothing to say.

- Panel names read as prose — `Avg of duration by service`, `Record
count`, `k8s_logs overview`. Both live panel-field shapes are handled
(builder `functionName` + `args`, and legacy flat `column` +
`aggregationFunction`); the time column is read from
`zoConfig.timestamp_column` rather than hardcoded. Hand-written queries
fall back to the stream, since naming a panel after `x_axis_1` is worse
than saying nothing.
- Alert names are identifiers — `anomaly_k8s_logs`,
`k8s_logs_status_gte_500` — built to survive the backend's
`RE_OFGA_UNSUPPORTED_NAME` rule, with operators spelled out and a
`slugify` backstop. A leading underscore is preserved so internal
streams (`_rumdata`) keep their names.

### Supporting library changes

| Component | Change |
| --- | --- |
| `OPageHeader` / `OPageLayout` | New `titleOverflow="visible"` — the
`<h1>`'s `truncate` clips an interactive title's focus ring and the
error message it floats below itself |
| `OSelect` | New `appearance="inline"` — borderless, no fixed height,
sized to content, for a value that reads as a word inside running text
("Add Alert **in KTX**") |

All new strings go through i18n (`common.inlineEdit.*`,
`dashboard.autoName.*`, `alerts.autoName.*`, per-module `renameHint`).
Interpolated identifiers — column names, SQL functions, stream names —
stay verbatim.

## Wired into

AddPanel, AddAlert (+ `useAlertForm`), WorkflowEditor, PipelineEditor,
Functions / FunctionsToolbar, IncidentDetailDrawer,
InlineSelectFolderDropdown.

## Tested

244 unit tests across the 5 affected suites, all passing — including new
specs for `OInlineEdit`, `useAutoName`, and `autoName`.

One pre-existing expectation in `autoName.spec.ts` was updated: it
asserted `Avg of duration, max of duration +1 more`, capitalising only
the first word of a multi-measure name. `measureLabel` capitalises per
measure by design, so the expectation now reads `Avg of duration, Max of
duration +1 more`.

---------

Co-authored-by: ktx-vaidehi <134508096+ktx-vaidehi@users.noreply.github.com>
Co-authored-by: ktx-vaidehi <vaidehi.akhani@kiara.tech>
2026-07-31 08:50:19 +00:00
4772df1723 feat: per-group and per-series alerting, and SLO measurement (#13547)
Design at: alerts_2.md

Per-group and per-series alerting, SLO measurement, and the UI for both.

## Alerts

**Multi-alerts (Feature 3).** A grouped alert can opt in to evaluating
and paging **per group** rather than collapsing to one verdict. Opt-in
only (`multi_alert`), so no existing alert changes behaviour: the flag
cannot be present in JSON written before it existed. Per-group state,
transitions, silence fingerprints, disappearance/reaping and a
cardinality cap, with the count-gate rules that make "most severe group"
and the legacy collapsed level provably the same verdict.

**Per-series alerting for PromQL.** The same feature for the metrics
family, where a group is one returned **series** keyed by its full label
set — that is a series' identity in Prometheus, and the expression's own
`by (…)` clause is already where the label set is chosen, so a second
picker could only disagree with it. One additive nullable column; NULL
means off, so stored alerts are untouched.

**Alert detail page**, replacing the row-click side panel: stat strip,
evaluation chart with threshold marklines, per-group table and history.
Simple alerts get a real history too — evaluations (one record per run)
by default, level changes alongside.

## SLOs

Count / time-slice / alert SLIs, error-budget and burn-rate arithmetic,
rolling windows, per-group slices with an exact rollup, coverage gating,
backfill, and SLO-based alerts.

Two capabilities the model could not express, both added tests-first:

- **Metrics-native counting** (`CountSource::PromQl`) — counter metrics
have no rows for a predicate to classify, so "good" exists only as
arithmetic between series, and correct counter arithmetic is
`increase()`.
- **Freshness semantics** (`absent_is_bad`) — for a pipeline SLO,
silence IS the failure, which the default gap rule can never report. A
*failed* query still freezes rather than pages; only a query that PROVED
emptiness counts as bad.

**SLO form**: live good/bad preview so a wrong predicate is caught
before saving rather than days later, real query editors with field
autocomplete, folders shared with alerts, and a move endpoint.

## Notes for review

- The SLO and composite features are **deferred (TODO)**:
`ZO_SLO_ENABLED` stays default **false** and the SLOs menu entry is
hidden until they ship; the SLO backend and UI remain in the tree behind
the flag, and composites stay pure logic + tests.
- One migration (schema 61): a nullable column for the PromQL opt-in. No
backfill.
- Two fixes outside the feature that it surfaced: query-editor
completions were duplicated per mounted editor (Monaco registers
providers per *language*), and the alert detail chart had no PromQL
branch, so it charted row counts against the series-count gate.
- Lands together with the matching o2-enterprise PR — `QueryType::Slo`
is new here and the enterprise dedup matches are exhaustive over it.
- `ratelimit::test_add_batch_missing_rule_id` fails intermittently on
`main` as well; unrelated to this branch.

---------

Co-authored-by: Shrinath Rao <shnath@openobserve.ai>
Co-authored-by: hengfeiyang <hengfei.yang@gmail.com>
Co-authored-by: sai nikhil kethe <nikhil@openobserve.ai>
2026-07-31 07:23:22 +00:00
Prabhat Sharma 16bab4c700 fix(metrics): heatmap step unit, explorer back nav, gauge percentiles (#13557)
Four fixes to the metrics explorer and the panel editor it drills into.
Each was reproduced from a real symptom; the root causes turned out to
be independent.

## 1. Histogram heatmap rendered as a single column in the panel editor

The same panel that renders correctly as a card collapsed to one
full-width bar per bucket once opened in the editor.

`usePanelPromQLExecutor` computed the heatmap step with `range / 1000`,
but `startISOTimestamp`/`endISOTimestamp` are **microseconds**:

- `plugins/metrics/Index.vue:294` documents `getConsumableDateTime()` as
microseconds, and line 424 builds `meta.dateTime` from it with no
conversion
- `usePanelDataLoader.ts:412` preserves that magnitude via `.getTime()`
- `usePanelVariableSubstitution.ts:410-412` reads the *same pair* and
correctly divides by `1_000_000`

So every range looked 1000x longer. A 15m window asked for a **7500s
step over a 900s range** → Prometheus returned one sample per series →
one bar per `le`. Now `max(15, ceil(900/120))` = 15s / 60 columns,
matching the card.

Only heatmaps were affected — every other chart type sends `step: "0"`
and lets the backend choose.

**Why this shipped:** the spec asserted the same wrong unit, feeding
`1_700_000_000_000` ms — a value the loader never produces — so it
passed while the product was broken. Rewritten to microseconds, with an
assertion that no range collapses below 10 columns.

## 2. Browser Back from Visualize landed on Logs

Explore → Visualize is entirely in-page (`mode` is a query param on the
`metrics` route; `MetricsVisualize.vue` is a child component, not a
route). The only URL write was `router.replace`, so the Explore entry
was **overwritten** rather than stacked, and Back popped straight past
the page to whatever preceded `/metrics` — reliably `/logs`, since
`logsUtils.ts:519-528` pushes an entry per search.

Mode transitions now `push`; filter, time-range and refresh edits still
`replace` so they don't stack history. `syncVisualizeUrl` stays
`replace` — it fires repeatedly while editing. The existing mode-only
fast path (`MetricsExplorer.vue:1388-1392`) restores Explore without
re-querying the ~40 cards.

## 3. Gauge percentiles all drew the same line

`quantile(φ, v)` aggregates **across the series** present at each
timestamp, so on a single-series gauge p50, p90 and p99 all return that
series' own value — three identical lines under three legends.

Switched to `quantile_over_time` — percentiles over **time**, which is
the question a gauge actually poses and which stays meaningful at any
series count. (Histogram percentiles still use `histogram_quantile`,
correctly untouched.)

Two constraints shaped this:

- **A range vector may only be taken over a selector.** The NaN-guard
retry rewrites the gauge selector into `(x and x > -Inf)`, and
`(...)[5m]` doesn't parse — the hazard already documented at
`metricDefaults.ts:586`. The subquery escape is closed too:
`src/promql/src/engine.rs:281-299` rejects a non-matrix inner
expression. So these queries are built from an unguarded `rawSel`. The
guard still applies to every variant that can carry it (asserted in
tests).
- **The rate window is the wrong size for a quantile.** Sized for
`rate()`'s two-sample need, it gave a 15m view a 1m window — four
samples, where p90 (index 2.7) and p99 (2.97) interpolate between the
*same* top pair and draw as one line. Percentiles now get
`computePercentileWindow`: floored at the rate window (no gaps between
evaluation points), targeting `MIN_PERCENTILE_SAMPLES (20) × scrape`,
capped at `range / 4` (a wider window flattens the chart).

| Range | Rate window | Percentile window | Samples |
|---|---|---|---|
| 15m | 1m | **3m45s** | 4 → **15** |
| 1h | 1m | **5m** | 4 → **20** |
| 6h | 3m15s | **5m** | 13 → **20** |
| 24h | 12m15s | 12m15s | 49 (unchanged) |
| 7d | 1h24m15s | 1h24m15s | 337 (unchanged) |

At long ranges the floor dominates and nothing changes, correctly.

A new `$__percentile_interval` panel variable mirrors all three bounds
so the card and the panel it drills into resolve to the **same** window
— asserted directly against `computePercentileWindow`. Without it the
editor would have kept the narrow window and disagreed with the card,
the exact failure `computeRateWindow`'s own doc warns about.

## 4. Multi-series charts were monochrome

`fixed` resolves to `fixedColor[0]` for *every* series name
(`colorPalette.ts:279`), and these charts carry no legend — so
Percentiles and Min/Max drew every line in one colour: indistinguishable
when they differ, and invisible when they coincide.

Now `palette-classic-by-series` above one series; a single series keeps
the card's accent colour. Hashing the series *name* keeps `p50` the same
colour across re-renders and between the preview tile and the card.
Verified `p50`/`p90`/`p99` and `min`/`max` land on distinct palette
slots in both light and dark themes.

Heatmaps are unaffected — `convertPromQLHeatmapChart.ts` uses a
hardcoded `HEATMAP_VISUAL_MAP_COLORS` and never reads `config.color`.

## Testing

- **15,743 passed, 0 failed** across `src/utils`, `src/composables`,
`src/plugins/metrics`, `src/components/promql`,
`src/components/dashboards`
- `vue-tsc` and `eslint` clean on every changed file
- New coverage: heatmap step at five ranges, mode-transition
push/replace, percentiles-over-time shape, the NaN-guard/range-vector
interaction, window bounds (including the invariant that the percentile
window is never narrower than the rate window, across ranges and scrape
intervals), card↔panel window agreement, and four colour-mode cases

Two existing assertions were corrected: one pinned the old `quantile()`
string, and the router mock's `push: vi.fn()` returned `undefined` where
the real router returns a Promise.

## Not addressed

A separate issue found while reviewing: `cache_hit_ratio` displays as
~8500%. `metricDefaults.ts:236` infers any `*_ratio` name as `percent-1`
(0.0–1.0), whose formatter multiplies by 100, but that producer emits
0–100. The inference follows Prometheus/Grafana convention, so the fix
is arguably in the emitter — and `FnOverride` currently has no unit
field, so there's no UI escape hatch. Left out deliberately pending a
decision.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-31 02:49:18 +00:00
db2f0fb62a feat(synthetics): attempts view, evidence panel, alerting, concurrency (#13543)
> Note: this branch carries ~20 earlier synthetics commits. The alerting
piece is ~280 lines and is cleanly separable if a smaller review is
wanted.

## Backend — alert state

- **Four columns on `synthetics`** (`consecutive_failures`,
`last_alert_at`, `alerting`, `degraded_notified_at`), all defaulted so
no backfill is needed
- **`DB_SCHEMA_VERSION` 55 → 56** — without the bump the migrator
short-circuits and the columns never appear
- Guarded with `has_column` on **both** `up` and `down`: SQLite ignores
`IF NOT EXISTS` on `ADD COLUMN`, and sea-query has no
`drop_column_if_exists`
- **Compare-and-swap** accessor (`update_alert_state_if`), because the
caller does a read-modify-write with no transaction
- `alerting` is a separate column because without it "recovered" cannot
be told from "was never alerting"
- **`synthetics_jobs::failing_locations`** — the notification previously
had no location field, so a six-location check with one broken region
could only say "the check is failing"
- Distinct subject, headline, icon and colour for FLAKY vs DEGRADED,
from one shared `locations_line` helper so Slack/plain-text/email cannot
drift

> The table is `synthetics`, not `synthetics_monitors` — the entity
module is named after the concept but declares `table_name =
"synthetics"`. Recorded at the `DeriveIden` so the next column addition
doesn't repeat it.

## Performance — the step-stats panel fetched ~35 MB per load

- **`recorded_steps` out of the 5000-row tally** — ~60% of payload; ~4
KB/row and near-identical within a config version, so an *aggregation*
re-read the same definitions 5000 times. Still written to every record,
because steps are editable and history must not be rewritten
- **`retry_history` out too**, replaced by `retry_step_ids` scanned only
over rows that actually retried
- **Key step stats by `step_id`, not name** — a rename *split* one step
into two rows; two steps sharing a name *merged* into one
- **Single-pass sparklines** — was O(steps²) per row: ~2M iterations for
a 20-step journey, ~50M for a 100-step one
- **Flaky Rate could exceed 100%** — the fallback incremented the
numerator without the denominator
- **Label the real window** — a 1-min check × 2 locations × 4 device
combos is 11,520 executions/day, so a 5000-row cap is ~10 hours of a
"last 7 days" selection

## UI

- **Attempts selector** in the run detail — costs no request,
`retry_history` is already on the row. Labels are 1-based while
`attempt` is 0-based
- Each attempt resolves **its own** screenshots, trace and evidence
bundle; a superseded attempt's live under an `attempt-N-` key
- **Evidence panel** — parses `evidence.ndjson` per line, grouped by
kind in severity order, chips with counts visible at zero, third-party
dimmed not dropped, truncation stated, four distinct empty states
- **Flaky / Degraded / Unstable tiles.** Unstable is partitioned by
`(location, device, engine)` — aggregated, a check solidly broken in one
region and healthy in five is indistinguishable from one intermittently
broken everywhere. It measures *oscillation*, not badness
- All flakiness tiles read **"—" at `retries = 0`**, never `0.0%`: a
zero is a measurement, that is the absence of one
- **`init_ms` and queue delay shown separately** — init was observed at
113,131 ms on a cold Lambda against a 243 ms check; unsubtracted, Lambda
locations look permanently slower at every percentile
- Steps and Evidence are sibling `OTabPanel`s — stacking them broke the
drawer's scroll

## Bugs fixed during review

- **`mapRetryHistory` guarded on `Array.isArray`** but the search API
returns blob columns as JSON *strings*, so it returned `[]` for every
real record — the attempts strip could never render, whatever the query
selected. Same bug in `mapEvidence` and the `attempts` fallback
- **`mapRunDetail` read `rawHit.attempt`**; the field is `attempts`. The
count was 0 on every run
- **`mapRetryHistory` hard-coded `status: "failed"`** — reported a flaky
run's *passing* final attempt as a failure
- **A superseded attempt's failing step rendered as a pass** — the
compact timeline's `passed`/`failed` was never normalised to the
`ok`/`fail` vocabulary every consumer tests for
- **`failure_detail` is not a column** — OpenObserve flattens nested
objects; naming the object is rejected by the search API
- **The retry-attribution query named `status_reason` unconditionally.**
That field only exists once something has been a `warning`, and the
three step queries share a `Promise.all` — so one missing column emptied
the *entire* Steps tab
- **`fetchSchemaFields` returned an empty Set on failure**, which the
gate reads as "the schema has no fields", silently degrading every
optional column to a literal

## Lease correctness (added after review of the concurrency docs)

Two corruption-class bugs, both of which double-count the alert failure
streak this PR introduces. Included here rather than in the concurrency
branch because they undermine *this* PR's own feature.

- **The browser lease validation was per-device while the work is
per-job.** `validate_browser_config` computed `(retries+1) x
journey_budget + retries x wait` against the 900s lease, but the probe
runs `browser_devices` **sequentially inside** the leased job and
nothing multiplied by the combo count (`MAX_BROWSER_DEVICE_COMBOS =
12`).

At defaults a plain **desktop + mobile** config is 1210s of work;
validation computed 605s and allowed it. It then blew its 900s lease on
**every run, forever**, with nothing failing at save. At the cap: 7260s
vs 900s.

The consequence is verbatim what the `LEASE_SECS` comment in
`dispatcher/mod.rs` was written to prevent — *"the reaper would requeue
mid-run and the journey would EXECUTE AGAIN — duplicate result records,
multiplied browser cost, and false alerts caused by the reliability fix
itself."* All three happen at two devices today, on the **managed**
path, since browser is Lambda-only.

> ⚠️ **Breaking validation change.** An existing 2+ device check will
fail to save until its budget, retries or combo count comes down. The
error now names the combo count as a lever. **Query dev and prod for
`json_array_length(config->'browser_devices') > 1` before rolling out.**

- **`ack_complete` had no status guard** — `WHERE id = $4` alone. A
duplicate ack succeeded and the caller called `increment_jobs_done`
again for one job: `jobs_done` overshot `job_count`, the run was
declared complete on a **partial set**, and since this PR it also
advanced `consecutive_failures` twice, so `alert_if_fails: 3` fires
after two failures.

Reachable today with no concurrency work: `aws-sdk-lambda` retries a
timed-out `RequestResponse` invoke, re-executing a check already in
flight.

`AND status = 1` makes it idempotent, and `ack_complete` now returns
`None` when it did not apply so the caller skips run accounting. Reaper
reassignment is covered too, as of the concurrency work merged below:
`AND claimed_by = $5`.

## Verification

`vue-tsc` + `type-check:app` clean · Prettier + design ratchet clean ·
**711 synthetics tests** · `cargo check` clean · infra unit tests pass.

Migration applied against a real SQLite metastore: `55 → 56`, all
columns correct. Every drawer field verified against live ingested data.

> `MonitorResults.spec.ts` fails on a pre-existing `storage.ts` import
error — identical on a stashed tree.

## Requires

o2-enterprise#2305 — the `AlertDecision` this consumes. Merge together;
each is inert alone.

---

# Also merged: probe concurrency (#13552)

The concurrency branch merged into this one, so this PR now carries
both. Everything below is a correctness fix — none of it is throughput
work.

### The reaper never completed a run, on any path

A run completes when `jobs_done` reaches `job_count`, and only three
callers move that counter: the probe ack, the dispatcher's failed
invoke, and the reaper. The reaper never did — including in
`dead_letter_expired`, whose whole purpose is to report "we gave up
after N attempts". It wrote a customer-visible error record into a run
that then stayed open forever: no `completed_at`, and since this PR's
alerting work, no `resolve_alert` either, so the failure streak never
moved.

The mechanism was `requeue_expired` ignoring `valid_until`. It reset a
job to Pending, and `prune_stale` — later in the **same tick** — deleted
it again, because `valid_until` is one interval (60s for a 1-minute
check) while a lease is 300s+. `dispatch_attempts` never got past 1, so
`MAX_DISPATCH_ATTEMPTS` was unreachable and the retry budget was dead
code on the agent path.

- requeue only while the job is still inside its window
- dead-letter everything that can no longer report, tagged with why
(attempts exhausted / window closed / never dispatched), so a dead agent
does not read as an unresponsive probe
- per-row compare-and-swap instead of SELECT-then-bulk-UPDATE; the
reaper runs on every alert_manager node
- increment **before** the ingest-token lookup, which returns early when
an org has no token

### Every check's retry sequence is bounded by the lease

Retries run *inside* the leased job. The browser path has checked this
since `journey_budget_ms`; the protocol path never did, and its inputs
were unbounded — the `5_000..=300_000` bound on `timeout_ms` is
**browser-only**, so a TCP check with `timeout_ms: 3_600_000` saved
fine.

With the reaper now completing runs, the symptom is a wrong answer
rather than a stuck one: the lease expires mid-flight, the reaper
completes the run as Error, and the probe's real ack is rejected as
stale. A passing check reports Error permanently. `timeout_ms=100s,
retries=2, wait=30` needs 360s and hits it.

`lease_batch` treats the client's `lease_secs` as a floor request, not
the decision — both probes guess 300s, and a probe cannot know how long
its own job may take.

### Guards on the ack, and on dispatch failure

`AND claimed_by = $5` closes reaper reassignment. `claimed_by` is
`Option` during rollout: an older probe acks without it and falls back
to the status guard, so nothing is discarded.

`fail_dispatch` had no status guard, so a dispatch judged failed *after*
the probe acked reset a **completed** job to Pending — leased again, run
again, duplicate result, `increment_jobs_done` twice. Third outcome
`AlreadySettled` added; the dispatcher matches all three with no
wildcard, so handling it is compile-time enforced.

`limit` is now clamped before the query. It is cast to `u64`, where a
negative wraps to ~1.8e19 and the LIMIT goes unbounded — one agent
leasing the entire pending queue for its pool. Reachable from one
mistyped env var.

### Queue backlog is observable

`synthetics_pending_jobs` and `synthetics_oldest_pending_age_seconds`,
per location and pool. `started_ts - scheduled_ts` already covered work
that *ran*; a check nobody leased produces no record, so its backlog was
invisible by construction.

## Before merging this to main

The `timeout_ms` validation is a **breaking change for existing protocol
configs**. It runs at create/update only, so nothing stops on deploy —
but an over-budget check is rejected the next time someone edits it.
Expected result is zero rows: `retries` of 0 or 1 cannot trip the budget
rule at all, and the defaults sit at 10,000ms, 1.1% of the lease.

```sql
WITH c AS (
  SELECT id, org_id, name, synthetics_type,
         COALESCE((config->>'timeout_ms')::bigint, 10000)           AS t,
         COALESCE((settings->>'retries')::bigint, 0)                AS r,
         COALESCE((settings->>'wait_before_retry_secs')::bigint, 5) AS w
  FROM synthetics WHERE synthetics_type <> 'browser'
)
SELECT id, org_id, name, t AS timeout_ms, r AS retries, w AS wait_s,
       (r + 1) * t + r * w * 1000 AS worst_case_ms
FROM c
WHERE t NOT BETWEEN 1000 AND 300000
   OR (r + 1) * t + r * w * 1000 > 900000
ORDER BY worst_case_ms DESC;
```

Anything returned is a check that **cannot work today either** — fix the
config rather than loosening the rule.

## Not done

Wiring the reaper's now-completing runs to a **notification**. The send
lives in a crate that depends on `o2_enterprise`, so the reaper cannot
call up into it; it needs the send moved into
`openobserve_core::synthetics` or a queue. No regression — before this,
the run never completed, so there was no notification either.

---------

Signed-off-by: Yashodhan Joshi <yjdoc2@gmail.com>
Co-authored-by: omkark06 <omkar@zinclabs.io>
Co-authored-by: Harsh Mahajan <115500013+007harshmahajan@users.noreply.github.com>
Co-authored-by: Yashodhan Joshi <yashodhan@openobserve.ai>
Co-authored-by: Yashodhan Joshi <yjdoc2@gmail.com>
Co-authored-by: sai nikhil kethe <nikhil@openobserve.ai>
Co-authored-by: Shrinath Rao <shnath@openobserve.ai>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 01:00:19 +00:00
6fb5eecdc5 fix: table migration to same components (#13451)
## What changed

Every table in the app now renders through the shared `OTable`
(`web/src/lib/core/Table`). Both legacy implementations are deleted —
`components/TenstackTable.vue` and `plugins/logs/TenstackTable.vue` —
for ~7,000 lines removed and one table behaviour contract instead of
three.

To get there, `OTable` gained: per-cell hover actions, per-column value
filter, opt-in column close "x", sticky pivot total row + right-pinned
total columns, pivot row-field cell merge, caller-owned pagination bar,
delegated scroll (shared scroller with the logs/traces histogram), and
variable-height virtual rows.

## Tables migrated

| Area | Tables |
| --- | --- |
| Logs | Search results grid, correlated logs |
| Traces | Search results grid, trace details sidebar (attributes /
events / links), service graph node panel |
| Dashboards | Table panel + pivot (incl. PromQL), panel pagination
controls, import error panel |
| RUM | Session player traces tab |
| Pipelines | Scheduled pipeline SQL preview |

## Tested

775 unit tests across the 12 affected suites, all passing.

Manually verified per table:

- **Columns** — resize, reorder, close "x", persistence across reload;
horizontal scroll on wide grids; wrap toggle with virtual scroll; client
+ 3-state server sort
- **Pagination** — custom records-per-page, enabling pagination after
mount, scroll-to-top on page change, no duplicate pager
- **Logs** — FTS highlighting, row expand → JSON preview and its
actions, hover actions + AI button, cell font / row density / status
spine, histogram pinned while scrolling sideways
- **Dashboards** — pivot multi-level headers, cell merge, sticky totals
staying aligned under horizontal scroll, cell colouring (auto palette /
value mapping / conditional rules / overrides), CSV + JSON export
- **Traces, RUM, Pipelines** — events table expansion, error-row border,
row click → trace navigation, SQL preview copy + send-to-AI

---------

Co-authored-by: ktx-vaidehi <134508096+ktx-vaidehi@users.noreply.github.com>
Co-authored-by: ktx-vaidehi <vaidehi.akhani@kiara.tech>
Co-authored-by: Shrinath Rao <shnath@openobserve.ai>
2026-07-30 12:18:23 +00:00
IluckySiandHengfei Yang c278682cca feat(web): add categraf option in custom metric ingestion (#13466)
This PR adds categraf as a explicit custom metric ingestion.

The configuration as follows:

> [[writers]]
> url = "http://localhost:5080/api/default/prometheus/api/v1/write"
> basic_auth_user = "root@example.com"
> basic_auth_pass = "o2oi_dMJ0RsB50MzVt2CVPCripths0WaGE832"

<img width="3064" height="1162" alt="image"
src="https://github.com/user-attachments/assets/189c3c96-441c-4476-8c31-943acaa788b0"
/>
<img width="3840" height="1862" alt="image"
src="https://github.com/user-attachments/assets/0408704b-b1d1-4546-8d8b-86dd5895d8bc"
/>

Co-authored-by: Hengfei Yang <hengfei.yang@gmail.com>
2026-07-28 12:29:01 +00:00
ktx-vaidehi c9fc8d3b55 fix: dashboard builder redesign (#13419) 2026-07-28 10:59:27 +00:00
Bhargav e9596a10a3 feat: Mobile rum sdk v2 (#13503)
Design at: design/mobile-rum-sdk-evaluation/design-docs

Backend changes exist to make the OpenObserve RUM intake accept the
mobile SDK wire format alongside the existing browser format.

src/api/src/handler/http/request/rum/ingest.rs

200 → 202 Accepted on /rum, /logs, /replay | ---> OpenObserve-family
SDKs (which the mobile forks derive from) treat any non-202 as an
unexpected error and drop the batch.

#[serde(default)] on Event ---> Mobile replay omits creation_reason /
index_in_view that the browser format sends.

Multi-segment replay handling ---> Browser sends 1 segment + 1 event
object; mobile sends N segment parts + one event JSON array. Handler now
collects all segments, parses event as array-or-object, pairs them,
emits NDJSON → one _sessionreplay row per segment.

MAX_SEGMENTS=200, 16 MB decompressed cap via .take() --->
Decompression-bomb guard, newly needed now that N segments are accepted.
2026-07-28 07:58:21 +00:00
Loakesh Indiran cca95a9ec6 fix(synthetics): stop reporting unevaluated HTTP assertions as passed (#13498)
## Problem

A failing HTTP check showed **Failed** at the top of the run detail and
**Assertions: Passed** below it — a green tick against `status_code eq
200` on a run that never got a status code.

The probe is right to omit `assertions_passed` when the request never
returned a response (connection refused, EOF, timeout): it returns
before evaluating anything, because there is nothing to assert against.
The view had two branches for three states:

```vue
:variant="run.assertionsPassed === false ? 'error' : 'success'"
```

`null` is not `false`, so "never ran" landed in the success branch.

| `assertions_passed` | means | UI showed |
|---|---|---|
| `true` | ran, all passed | Passed ✅ |
| `false` | ran, one failed | Failed ✅ |
| *absent* | never ran | Passed ❌ |

## Changes

1. **Three states, not two.** Absent renders as a neutral **Not
evaluated** badge with neutral rows and a line explaining why.

2. **Render the probe's per-assertion results** when the record carries
them (see synthetic-o2-agent#4), so a run with several failing
assertions shows each one with its own comparison string instead of a
single inferred red row. Records written before the probe echoed that
array keep the old inference path.

3. **Fix `response_time_ms` on the legacy path.** The probe's detail
leads with `response_time`, so `startsWith` never matched the configured
field name and a failing latency assertion left every row green. The two
renamed fields now go through a lookup map rather than one special case.

4. Reported rows are self-describing, so the assertions card no longer
depends on the check-config fetch succeeding (403 on community builds,
deleted check, …).

## Testing

`ProtocolRunSummary.spec.ts` — 7 new tests covering the not-evaluated
state, the still-passes case, the reported-array path, config-fetch
failure, the legacy single-failure record, and `response_time_ms`. Full
synthetics sweep: 499 passed, 0 failed.

Verified end to end against a local deployment with five checks covering
each case:

| Check | Assertions | Record | UI |
|---|---|---|---|
| AT-A | `status_code eq 200` | `assertions_passed: true` | Passed, row
green |
| AT-B | `status_code eq 999` | `false` + array | Failed, row red |
| AT-C | `status_code eq 999` + `body contains NOPE` | `false` + array
of 2 | Failed, **both** rows red |
| AT-D | `response_time_ms lt 1` | `false` + array | Failed, row red |
| AT-E | unreachable target | `assertions_passed` absent | **Not
evaluated** |

Before: AT-C showed one red row and one green, AT-D showed no red row,
AT-E showed Passed.
2026-07-28 01:21:39 +00:00
Omkar Kesarkhane 237fbca3af fix: Synthetic check runs table sorting issue (#13477)
## What changed
This PR makes the Synthetics runs table sortable (with a default sort by
last-run, newest first) on all columns, using raw underlying values
(e.g. `durationMs`, `statusRank`, `locationName`) instead of the
formatted display strings the cells render. It also removes a
`tableFilterKey`-based force re-render hack and an inline error-badge
cell/`filterByError` helper that duplicated existing
`filterByErrorPattern` filtering. Separately, it fixes a loading-state
flash in `RunDetail.vue`/`ProtocolRunSummary.vue`: `MonitorResults.vue`
now passes a resolved `overrideMonitorType` down to `RunDetail`,
`RunDetail` seeds `monitorType` from that override in drawer mode to
skip a redundant fetch, and `loadRun()` now sets
`synthetics.loading.value = true` synchronously so the skeleton shows
instead of an empty/wrong-branch UI while monitor type resolves. A new
`ProtocolRunSummarySkeleton.vue` component replaces a generic
`OSkeleton` block to mirror the actual result-card layout. The runs
query limit in `useSyntheticResults.ts` was bumped from 500 to 1000, and
the runs page size default is kept at 10.

## Why
Reviewing `MonitorRuns.vue`'s pagination, default sort, and column
sorting surfaced that columns weren't sortable and had no default sort,
and that sorting on formatted values would produce incorrect ordering
(e.g. "900ms" sorting after "1.2s", or status ordering depending on
locale). Separately, the session's testing turned up a UI flash where
`RunDetail`/`ProtocolRunSummary` briefly rendered the wrong branch
(empty state or "browser" default) before `fetchCheck()`/monitor-type
resolution completed.
2026-07-27 16:04:39 +05:30
Loakesh Indiran 3f49343426 feat(synthetics): private-agent token lifecycle, browser private agents & P0 lease fixes (#13432)
Synthetics work spanning private-agent tokens, browser private agents,
and job-lease correctness. (Enterprise backend:
openobserve/o2-enterprise#2260.)

## Private-agent probe tokens
- Named, org-level, rotatable `o2syn_` probe tokens — **create / list /
rotate / disable**. Rotate = create-new + keep-old for zero-downtime
overlap; disable = revoke. Fixes the gap where a private-agent token was
an unrevocable standing credential.
- IAM **Synthetics Tokens** page (create / rotate / disable) with an
**Agents** column — how many agents currently use each token, so a token
is only revoked once it's safe.
- `POST /synthetics/agent-tokens` to mint a named token; agent
`token_id` tracking; per-agent **Restart config** action.

## Browser checks on private locations
- Allow browser checks on private, **browser-capable** locations
(previously public/Lambda only).
- Agent-setup drawer emits `--type=browser` so the install command pulls
the browser probe image.

## Private locations as agent pools
- A private location is treated as a **pool of interchangeable agents**
— list shows live/total agent count + C/min (deliberate naming instead
of auto-generated `private-location-XXXX`).

## Lease correctness (P0)
- **Atomic job lease** — no duplicate runs when a location has multiple
agents (guarded `status = 0` claim + winner-scoped return).
- **Capability-filter the lease by agent type** — a net agent and a
browser agent can now share one pool; the net agent no longer wins (and
fails with `unsupported check type: browser`) a browser job.

## Scheduling & RBAC
- Expose `next_run_at` on DueMonitor for drift-free scheduling.
- CREATE-folder query is authoritative (RBAC); require PUT permission to
move checks between folders; pass the folder query param on per-check
API calls.
2026-07-24 17:39:35 +00:00
Omkar KesarkhaneandClaude Opus 4.8 80592f9404 fix: fixed protocol check breakdown cards (#13443)
## What changed

- **MonitorRuns.vue**: Fixed protocol check breakdown cards showing
"Pass Rate by Device" instead of "Duration by Location (Avg)". Added
`OTooltip` with truncation and `cursor-help` on all breakdown card name
spans. Resolved private location labels in the timeline tooltip. Unified
duration formatting to 2 decimal places. Added back the device breakdown
card for browser checks. Fixed `locationDurationBreakdown` to resolve
location IDs to human-readable labels.
- **syntheticResultsSchema.ts**: Renamed device keys `laptop_large` →
`desktop` and `mobile_small` → `mobile` to match the incoming data
schema.

## Why

Protocol check monitors were showing an irrelevant "Pass Rate by Device"
card (device data doesn't exist for protocol checks) instead of the
correct "Duration by Location" card. Browser names and location names in
breakdown cards were untruncated, causing layout issues with long names.
Private location labels in the timeline tooltip displayed raw KSUIDs
instead of human-readable names. Duration values had inconsistent
decimal precision (some clipped, some raw).

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 10:18:44 +00:00
Abhay PadamaniandClaude Opus 4.8 5ec9b89ff8 build: FE code format check for improved maintainability (#13351)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 07:11:39 +00:00
Shrinath RaoandClaude Opus 4.8 c122fbf9dc fix(logs): stop lazy-editor's transient empty emission wiping restored share-link query (#13426)
## The bug
Opening a **shared link** to an SQL search intermittently lands on an
**empty query editor** ("SQL query is missing or invalid"). Recurring
Logs-Features alpha flake: `monaco-query-prefill` P0 / `shareLink` P1 →
*"query editor never contained SELECT."*

Follow-up to #13414 — that PR guarded a plausible path (the
`fullSQLMode` watcher's "switch ON"), but the trace after it deployed
showed the query **still** dropped. This finds and fixes the actual
cause.

## Root cause (trace-confirmed)
The Monaco query editor is **lazy-loaded**. After the short-URL restore
sets the SQL query, the editor fires its change callback with an empty
`""` **while it mounts — before the restored value is applied.**
`SearchBar.updateQueryValue()` treated that transient empty as a real
edit:

```js
searchObj.data.editorValue = value;   // "" overwrites the restored query
searchObj.data.query = value;
if (value.trim() === "" && searchObj.meta.sqlMode === true) {
  searchObj.meta.sqlMode = false;     // → SQL mode auto-detects OFF → editor cleared for good
}
```

Under load the editor's empty emission lands *after* the restore, so the
query is lost. The trace shows the URL ending `sql_mode=false` with the
`query` param gone — exactly this.

## The fix — state-based, not a timer
The editor emits `""` *because it isn't ready yet*. So we gate on
**readiness (state)**, not a wall-clock wait (a fixed delay would just
change the odds, like "make the env faster"):

- `restoreUrlQueryParams()` sets `meta.pendingUrlQueryRestore = true`
when it restores a non-empty query.
- `updateQueryValue()` **ignores an empty emission while that flag is
set**, and clears the flag the instant the real (non-empty) value lands
— so genuine user clears *after* load still work. This mirrors the
existing `loadingStream` guard already in that function.

Because it keys off state, the race can't beat it: no matter *when* the
empty emission arrives, it's ignored until the real value is in.

## Verification
- ✅ **New real unit test** on `restoreUrlQueryParams`
(`useLogs.spec.ts`, actual composable) — asserts the base64 query
decodes into state, SQL mode restores, and `pendingUrlQueryRestore` is
set. **7 passed.**
- ✅ The guard is deterministic by construction (reads the same shared
`searchObj.meta`).
- ⚠️ The load-race is only observable against a **deployed** frontend,
so the final "flake gone on alpha under load" confirmation is a re-run
once this is deployed to alpha. (I was wrong once here — so I'm being
explicit that this last step is pending, not assumed.)

## Note
#13414's guard is left in place — it's a valid defensive symmetry fix,
just not the operative cause. This PR is the operative fix.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 04:26:46 +00:00
ktx-vaidehiandClaude Opus 4.8 8886a03b99 feat(dashboards): redesign panel config sections (#13371)
- Collapse all config sections by default (was expanded)
- Add open-state accent to OCollapsible sidebar variant: left accent
bar, tinted header, and accent-colored icon/chevron when expanded
- Move sidebar chevron to the right of the label (rotates on open)
- Give every config section a leading icon via SECTION_ICONS
- Add theme-aware collapsible open-state tokens (light + dark)

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 11:14:16 +00:00
Omkar KesarkhaneandClaude Opus 4.8 100a82ab00 fix: added warning start filter in synthetics (#13395)
## What changed

- **Warning status consistency**: The backend returns 4 run statuses
(`passed`, `warning`, `failed`, `error`) but the frontend only handled
`passed`/`failed` consistently. Across `MonitorRuns.vue`,
`SyntheticMonitoring.vue`, `syntheticResultsSchema.ts`, and
`useSyntheticResults.ts`, all 4 statuses are now properly mapped to
their own badge variants, filter options, KPI cards, timeline segments,
and breakdowns.
- **Cooldown period label**: Removed the `*` required indicator from the
cooldown period label in `CheckAlerts.vue` since the field is optional.

## Why

The TLS certificate-expiring-soon check returns `warning` status, which
appeared inconsistently: green "passed" in some views, red "failed" in
others, or entirely missing from filter dropdowns. The fixes ensure SREs
can distinguish "slightly flaky check" from "service is crashing" at a
glance.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 10:02:52 +00:00
Omkar KesarkhaneandClaude Opus 4.8 53ae25923c fix: added "Synthetics Locations" settings panel under for managing public probe locations (AWS, GCP, Azure, custom) (#13385)
## What changed

Adds a "Synthetics Locations" settings panel for managing public probe
locations (AWS, GCP, Azure, custom). Includes four new components —
`SyntheticsLocationsList` (OTable-based list with search, bulk actions,
import/export), `SyntheticsLocationForm` (ODrawer + OForm + Zod for
create/edit), and `ImportSyntheticsLocations` (JSON upload/textarea with
live validation). Extended the frontend service with `createLocation`,
`updateLocation`, `deleteLocation`, and `bulkDeleteLocations` methods.
Registered a new `synthetics_locations` route under the enterprise
feature flag and added a "Synthetics" group to the AppSettings sidebar.
Shortened import/export action labels from "Import Locations"/"Export
Locations" to "Import"/"Export".

## Why

The backend already exposed full CRUD endpoints for synthetics locations
(`POST/PUT/DELETE /api/{org}/synthetics/locations`), but there was no
administrative UI for managing them. This panel gives meta-org (root)
users the ability to create, edit, delete, enable/disable, import, and
export public probe locations without manual API calls.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 05:28:16 +00:00
Omkar KesarkhaneandClaude Opus 4.8 287f17f41c fix: In synthetics replaced chrome runtime with window post message (#13346)
## What changed

Replaces `chrome.runtime.*` direct messaging (which requires a static
`externally_connectable` manifest entry) with a `window.postMessage`
bridge transport for the synthetics recorder extension. This removes
`externally_connectable` from the extension manifest so the recorder
works with self-hosted OpenObserve instances on any domain. Also
includes fixes for recording tab lifecycle handling, replay proxy
deserialization, and tab removal edge cases.

## Why

The `externally_connectable.matches` field in the manifest is static —
it cannot include arbitrary self-hosted domains, so users hosting
OpenObserve on their own URLs couldn't use the extension recorder. The
bridge transport (content script + `window.postMessage`) decouples the
web app from the extension's manifest, letting any origin communicate
with the recorder.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 20:38:19 +00:00
Omkar KesarkhaneandClaude Opus 4.8 b290f3c8b2 fix: added header-level tab navigation (Checks / Private Locations) to the Synthetic Monitoring page (#13386)
## What changed

- Added header-level tab navigation (Checks / Private Locations) to the
Synthetic Monitoring page using `OPageLayout`'s `#header-tabs` slot with
`tabs-below`, rendering tabs on a dedicated second row.
- Made the header action button conditional: "New Check" on the Checks
tab, "Setup an agent" on the Private Locations tab.
- Removed the `@setup` emit and "Setup an agent" button from the
`PrivateLocations` table toolbar — that action now lives in the page
header.
- Removed the `#tabs` slot (type toggle group) and `onEmptyAction`
handler from `PrivateLocations`.
- Added 14 new tests for `SyntheticMonitoring` (25 total) and created a
new `PrivateLocations.spec.ts` with 35 tests.

## Why

The user reported that `header-tabs` was not rendering on a second row —
investigation revealed the `tabsBelow` prop was missing. Separately, the
"Setup an agent" action needed to appear contextually in the page header
when viewing Private Locations rather than as a persistent button in the
table toolbar.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 20:17:57 +00:00
Prabhat Sharma d2f279ec04 fix(ingestion): add macOS host agent and uninstall path to Data Sources (#13342)
## What

Two gaps in **Data Sources → Recommended**, both cases where the agent
scripts already existed but nothing in the product reached them:

1. **macOS was missing entirely.** `openobserve/agents` ships
`mac/install.sh`, but Linux and Windows were the only host agents in the
UI, so Mac users had no path from the product.
2. **No uninstall anywhere.** `linux/uninstall.sh`,
`windows/uninstall.ps1` and `mac/uninstall.sh` all exist and were
reachable from nowhere.

## macOS card

Sits between Linux and AWS in the sidebar, reusing the existing
`osAgent` scaffolding (install code block, host-metrics detection,
shared troubleshooting). Two deliberate differences from its siblings:

- **No generic/EC2 toggle.** There is no `mac/ec2` script, so it is a
single command — and the EC2-only troubleshooting row ("hosts show up
under an instance id") is dropped, since it cannot apply.
`sharedAgentTroubleshooting` gained an `includeEc2` flag; Linux/Windows
behaviour is unchanged.
- **The unified log needs a bridge.** It is not a file any collector can
tail, so the installer registers a second launchd daemon piping `log
stream --style ndjson` into the agent over loopback TCP. Troubleshooting
covers that daemon specifically, plus the volume knobs and the fact that
an *empty* `collector.err` is expected rather than a symptom.

## Uninstall

New `extras.uninstall`, rendered as the **last** collapsed accordion —
below troubleshooting — so a destructive action stays clear of the setup
flow while living on the page that installed the thing.

I deliberately did not reuse `extras.advanced`, whose documented
contract is "an alternative manual path to the *same* result"; uninstall
is the opposite, and Kubernetes already uses that slot for its manual
Helm sequence.

One script per OS covers both the generic and EC2 installs (there are no
EC2-specific uninstall scripts). All three raw URLs verified to return
200.

## Icon

`images/common/macos.png` is a neutral-grey Apple mark derived from the
existing `rum/mac.png`. That asset is solid black, and the sidebar tab
has no dark-mode inversion mechanism (unlike variant icons, which have
`iconInvertDark`), so a black mark would have been invisible in dark
mode. One asset that reads on both themes.

## Testing

- **2,410 tests pass across 749 suites**, including 6 new macOS content
tests and 4 new renderer tests.
- One renderer test mounts a card whose *only* extra is `uninstall`,
which catches forgetting the `.c-more` visibility guard.
- Extended the existing guard that every accordion icon is registered in
`OIcon` (an unregistered name silently degrades to raw text beside the
label) to cover the new `delete-outline`.
- `npm run type-check`: 0 errors. `eslint`: clean.
- `routeTabMaps.spec.ts` had a hard-coded `recommended has 9 entries` →
updated to 10. That is the only pre-existing assertion touched.

**Not verified:** I have not seen this render in a browser — it needs a
running backend and login. Worth a reviewer eyeballing the tab, the icon
contrast in dark mode, and the accordion order.

## Found while here (not fixed in this PR)

Two pre-existing bugs in the adjacent card content, both predating this
change:

1. **Linux and Windows cards name a service that does not exist.** They
tell users to run `systemctl status openobserve-agent` / `Get-Service
-Name openobserve-agent`, but the installers register
**`otel-collector`** (`linux/install.sh:114,132`,
`windows/install.ps1:31`) — `openobserve-agent` is the Linux *user
account*, not the service. So the primary "most likely fix" snippet on
both cards returns "Unit openobserve-agent.service could not be found".
The new macOS card uses the real launchd labels.
2. **`windows/uninstall.ps1` never deregisters the service** (agents
repo). It stops the service and deletes the install dir but has no
`sc.exe delete`, so the registration survives pointing at a deleted
binary — and since `windows/install.ps1` calls `New-Service` without
removing an existing one, uninstall → reinstall fails. The accordion
copy describes what the script actually does, not what it ought to.

Happy to take both in follow-ups.
2026-07-22 16:55:53 +00:00
Omkar KesarkhaneandClaude Opus 4.8 80be88a396 fix: synthetics UI issues (#13352)
## What changed

Fixes UI issues in OpenObserve's Synthetics monitoring section: label
corrections ("Synthetic" → "Synthetics"), validation feedback (toast +
OForm thread-through), step editing data-loss bugs (wire sync,
buildWireFromStep), replay banner cleanup, retries extraction into its
own component, and URL display in RunDetail step expansion.

## Why

Each fix addresses a specific user-reported or observed defect — steps
that silently dropped edits on save, validation errors that gave no
visual feedback, a banner that persisted after deleting all steps, and
incorrect label/URL display in sidebar, headers, and run detail views.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 16:15:53 +00:00
Abhay Padamani db1da2f096 fix: MetricCardChart to use PanelSchemaRenderer (#13380)
- Replaced ChartRenderer with PanelSchemaRenderer in MetricCardChart.vue
for consistent rendering.
- Updated MetricCardChart.spec.ts to assert the panel schema and
injected data correctly.
- Introduced applyMeasuredYAxisLeftInset utility to adjust y-axis label
spacing based on content width.
- Enhanced convertPromQLData to call applyMeasuredYAxisLeftInset before
returning options.
- Added tests for applyMeasuredYAxisLeftInset to ensure correct behavior
with various configurations.
- Implemented compact preview adjustments in heatmap charts to optimize
rendering in metrics explorer cards.
2026-07-22 14:24:46 +00:00
Loakesh Indiran 9d70e0e905 feat(synthetics): private locations — check-form picker, agent identity, install flow (#13308)
Design at: https://github.com/openobserve/designs/tree/main/synthetics
2026-07-22 12:30:31 +00:00
ktx-vaidehi f1ffc974d7 fix: stream page refresh, loading when delete and org dropdown update when new org added (#13369) 2026-07-22 11:12:00 +00:00
IluckySi d015f1c748 feat(web): add loongcollector option in custom log ingestion (#13258)
This PR adds LoongCollector, **Alibaba’s self-developed log collector**,
as a explicit custom log ingestion choice in the user interface.

The LoongCollector log collection configuration as follows:
ilucky@ubuntu:/.../loongcollector-3.3.5$ cat
conf/continuous_pipeline_config/local/collector.yaml

> enable: true
inputs:
  &nbsp;&nbsp;\- Type: input_file
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;FilePaths:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;- /tmp/*.log
processors:
  &nbsp;&nbsp;\- Type: processor_json
  &nbsp;&nbsp;&nbsp;&nbsp; SourceKey: content
flushers:
  &nbsp;&nbsp; \- Type: flusher_http
&nbsp;&nbsp; &nbsp;&nbsp; RemoteURL:
http://192.168.139.3:5080/api/default/default/_json
    &nbsp;&nbsp; &nbsp;&nbsp; Method: POST
    &nbsp;&nbsp; &nbsp;&nbsp; Headers:
&nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; Authorization: Basic
cm9vdEBleGFtcGxlLmNvbTpvMm9pX1hVcHBBeUdVd0J4VHZHbmpYY2c1eWJGblZFNFduak9D
    Compress: gzip

<img width="2926" height="1194" alt="image"
src="https://github.com/user-attachments/assets/ae2a5af6-ea03-4a41-833a-c53e7f692f9d"
/>
<img width="2932" height="990" alt="image"
src="https://github.com/user-attachments/assets/1bed13bb-ec40-4200-a71a-1229a6884af6"
/>
<img width="2932" height="1196" alt="image"
src="https://github.com/user-attachments/assets/79e54f4c-1c67-46c5-8fac-97506907da5e"
/>
2026-07-22 03:43:12 +00:00
Prabhat Sharma 21d0907342 fix(logs): redesign log patterns UI (#13337)
## What

Redesign of the log patterns UI.

- **`PatternCard`** reworked: template tokens, a badges row (trend /
rarity / anomaly), and inline frequency + percentage replace the old
fixed count and action columns.
- **`PatternVolumeCell`** (new) + **`usePatternVolume`** (new) render
per-pattern volume sparklines.
- **`PatternDetailsDialog`** now owns the include / exclude /
create-alert actions that previously lived on each card.
- **`patternUtils`** / **`usePatternActions`** extended to back the
above.

## Test plan

- `web` unit tests for the touched areas: 356 passing, 0 failing
(`plugins/logs/patterns`, `plugins/logs/data-table`, `SearchResult`,
`FieldValuesPanel`).
- New spec coverage for `PatternVolumeCell`, `usePatternVolume`, and
`patternUtils`.
- `npm run type-check` clean.
- `lint:design`, `lint:tokens`, `lint:token-purity` all clean.

## Notes

Rebased onto latest `main`. Five conflicts came from main's repo-wide
type-check pass (#13174), which made `OIcon`'s `name` prop required.
Main's fix (`name=""` + slot) was kept in `CodeQueryEditor.vue`,
`FieldValuesPanel.vue`, and `CellActions.vue`; in `PatternCard.vue` the
surrounding block was removed by this redesign, so the deletion stands.
2026-07-21 15:46:12 +00:00
Abhay Padamani 4d58a1efe4 build: add UI code quality checks and enhance linting processe (#13174)
chore: update package.json scripts for improved type-checking
fix: adjust frontmatter regex to handle BOM characters
refactor: streamline error handling in dashboard data conversion
style: clean up comments and formatting in various files
2026-07-21 12:47:06 +00:00
2abb10e54a fix(dashboards): repair delete from the Favorites view (#13327)
Deleting a favorited dashboard failed or left ghost rows behind:

1. Bulk delete sent activeFolderId as the `?folder=` param. In the
Favorites view that is the `__favorites__` pseudo-folder id, which the
backend 404s on — so bulk delete never worked there for any dashboard.
Favorites can also span folders, which one folder-scoped call could not
cover. Group the selection by each row's real folder_id and issue one
call per folder; a normal folder view collapses to the single request it
always was.

2. Nothing pruned the favorites setting when a dashboard was deleted, so
the Favorites view kept rendering the row from its stored label. Add
removeFavorites() and call it after single and bulk delete, plus a
self-heal on entering Favorites for deletions made elsewhere (another
tab, the dashboard view). It deliberately does not revert on a failed
write — the dashboard is already gone, so restoring the row would
recreate the ghost the cleanup exists to remove.

3. Folder navigation is cache-first, and bulk delete never touched the
store, so the deleted rows reappeared in their source folder until a
manual refresh. Add evictDashboardsFromCache(), mirroring what
deleteDashboardById already does for the single-delete path. Only ids
the backend did not report as unsuccessful are evicted, so a partial
failure keeps the surviving rows visible.

Also snapshot the selection before reading the `dashboards` computed in
bulk delete — that computed resets selectedIds as a side effect, so the
new folder-grouping could otherwise have deleted nothing.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Abhay Padamani <141122205+ktx-abhay@users.noreply.github.com>
2026-07-21 11:50:43 +00:00
Omkar KesarkhaneandClaude Opus 4.8 c7871126c5 fix: In traces 'status' field was getting added as 'Span Status' in spans mode (#13294)
## What changed

Removes the automatic migration of the `status` field to `span_status`
in spans search mode, and passes the current `searchMode` to column
definition generation so `status` is treated as a generic data column in
spans mode instead of getting traces-mode special column rendering.
Alongside this, the TenstackTable header actions area gets a
hover-visibility fix and the old `.logs-table` CSS block for column
actions is deleted in favour of the generic `group-hover:visible`
approach.

## Why

The previous code was conflating the `status` data field (a legitimate
user-configurable column in spans mode) with the "Span Status" column
definition that is meaningful only in traces mode. This meant any user
who had `status` saved in their spans column preferences would have it
silently renamed to `span_status`, which either broke the column or
showed incorrect data.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 11:49:11 +00:00
Sai NikhilandYashodhan Joshi 36518e49c1 fix: workflows add on (#13323)
Signed-off-by: Yashodhan Joshi <yjdoc2@gmail.com>
Co-authored-by: Yashodhan Joshi <yjdoc2@gmail.com>
2026-07-21 10:31:52 +00:00
IluckySiandHengfei Yang 2be4fc627c feat(web): add nightingale option in custom metric ingestion (#13264)
This PR adds nightingale as a explicit custom metric ingestion.
github: https://github.com/ccfos/nightingale

The configuration as follows:

> [[Pushgw.Writers]]
> Url = "http://localhost:5080/api/default/prometheus/api/v1/write"
> BasicAuthUser = "root@example.com"
> BasicAuthPass = "o2oi_XUppAyGUwBxTvGnjXcg5ybFnVE4WnjOC"

<img width="2940" height="1004" alt="image"
src="https://github.com/user-attachments/assets/1683255f-1591-4a40-bfe0-ea52c9484746"
/>
<img width="2926" height="1614" alt="image"
src="https://github.com/user-attachments/assets/c3c3b181-5b6a-4104-b7e4-8857597dfd8e"
/>

Co-authored-by: Hengfei Yang <hengfei.yang@gmail.com>
2026-07-21 08:39:52 +00:00