mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-05 16:10:43 +00:00
ee71df7ce73cec00a9dba13dbe4b2e910e16ebfc
924
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
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. |
||
|
|
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) | |
||
|
|
bbf2aa37af |
feat: Issue ent 2289 mobile rum sdk error tracking v2 (#13645)
Design at: designs/tree/main/mobile-rum-sdk-evaluation/design-docs |
||
|
|
3285a31e6f |
fix: i18n lint enforcement (#13439)
Co-authored-by: Kirtan Thakkar <kirtan@kiara.tech> |
||
|
|
95e1ee83c3 | fix(search): improve loading behavior on stream change (#13636) | ||
|
|
232b38351d | fix: search history routes change (#13512) | ||
|
|
76f1df06fa | fix: alert timezone fix on main (#13519) | ||
|
|
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. |
||
|
|
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`.
|
||
|
|
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`.
|
||
|
|
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.
|
||
|
|
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> |
||
|
|
fc8e3c1657 | fix: redesign the step editor and the run evidence surfaces (#13582) | ||
|
|
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 |
||
|
|
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. |
||
|
|
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. |
||
|
|
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 (
|
||
|
|
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.
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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) |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
c9fc8d3b55 | fix: dashboard builder redesign (#13419) | ||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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> |
||
|
|
5ec9b89ff8 |
build: FE code format check for improved maintainability (#13351)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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.
|
||
|
|
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>
|
||
|
|
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. |
||
|
|
9d70e0e905 |
feat(synthetics): private locations — check-form picker, agent identity, install flow (#13308)
Design at: https://github.com/openobserve/designs/tree/main/synthetics |
||
|
|
f1ffc974d7 | fix: stream page refresh, loading when delete and org dropdown update when new org added (#13369) | ||
|
|
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: \- Type: input_file FilePaths: - /tmp/*.log processors: \- Type: processor_json SourceKey: content flushers: \- Type: flusher_http RemoteURL: http://192.168.139.3:5080/api/default/default/_json Method: POST Headers: 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" /> |
||
|
|
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. |
||
|
|
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 |
||
|
|
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> |
||
|
|
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> |
||
|
|
36518e49c1 |
fix: workflows add on (#13323)
Signed-off-by: Yashodhan Joshi <yjdoc2@gmail.com> Co-authored-by: Yashodhan Joshi <yjdoc2@gmail.com> |
||
|
|
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> |