The client is React independent and uses the Rust handlers in this repository. createClient({mode: 'live'}) uses same-origin /api/v1 requests with credentials: 'include'. The Vite development proxy must preserve the frontend origin in Host, or the backend must explicitly allow that origin, because the login handler validates the absolute redirect URL.
| Method | HTTP endpoint | Response |
|---|---|---|
listDatasets(signal?) |
GET /api/v1/logstream |
[{name}]; normalized to {name,type:'logs'} |
schema(dataset, signal?) |
GET /api/v1/logstream/{encoded name}/schema |
Arrow schema {fields:[{name,...}],metadata:...}; returns top-level field names |
query({sql,startTime,endTime}, signal?) |
POST /api/v1/query |
JSON array of records |
login(username,password,returnPath?) |
GET /api/v1/o/login?redirect=<absolute same-origin path> |
Basic authorization is exchanged for backend session cookies, then a separate cookie-authenticated stream list request verifies the session (403 is retained as an authenticated permission limitation) |
logout() |
GET /api/v1/o/logout?redirect=<origin>/login |
Top-level browser navigation; backend invalidates the local session and may redirect OAuth users through the provider logout endpoint |
identity(signal?) requests GET /api/v1/users/{encoded user_id} after reading the display cookie as an identity lookup key. The server response (id, username, optional method and email) takes precedence. An unavailable profile falls back to the display-only username/user_id cookies, except that 401 and cancellation propagate. The fallback never claims to be a verified server identity. Missing cookies produce no identity request. Neither cookie content nor the identity label grants permissions; protected backend requests remain authoritative. The endpoint is declared by get_users_webscope and handled by get_prism_user in src/handlers/http/rbac.rs.
Rows and histogram issue separate POST /api/v1/query requests with one captured UTC start/end pair and identical search/group predicates. Rows default to LIMIT 100, with selectable limits 1,000, 5,000 and 10,000; histogram SQL uses DATE_BIN, COUNT(*), grouping and ordering without a result-row limit. An explicit half-open timestamp predicate enforces the same window for both reads. Dataset information selects the timestamp field. The chart fills 48 fixed elapsed-time buckets and distinguishes matching totals from loaded rows, with independent loading/error/retry states. /counts manifest statistics are not used for exact totals. Refresh captures new bounds; changing filters preserves the current window. Two-level AND/OR expressions allow 12 groups and 50 rules, escape identifiers/literals, ignore empty groups, and support =/!= with SQL NULL semantics (NULL is excluded).
Saved log views use existing user/tenant-authorized GET/POST /api/v1/filters and PUT/DELETE /api/v1/filters/{encoded id} through listLogViews, createLogView, updateLogView and deleteLogView. Documents keep stream_name, filter_name, SQL query {filter_type:"sql",filter_query,filter_builder:null}, optional classic time_filter {from,to}, and flattened explorer_state containing validated version-1 JSON. State includes dataset, range, search, grouped filters, columns, timezone, result limit, wrapping and histogram visibility. Saved SQL uses the dataset timestamp partition rather than assuming p_timestamp. New views retain dataset-scoped stream_name for gradual adoption. Compatible owned views offer Convert to SQL view, using a bodyless POST /api/v1/filters/{encoded id}/convert-to-sql through convertLogViewToSql. Conversion retains the ID, owner, query, explorer state and other metadata, and is idempotent. Canonical stream_name:"sql" views resolve their dataset from validated explorer state; malformed or unrelated canonical states do not appear in this dataset’s list. Updates preserve canonical SQL storage and unrelated server metadata. Only verified owners can update/delete/convert a view in this UI; authorized views owned by others are labeled read only and can still be applied or copied with Save new view. Backend authorization remains authoritative and also permits administrators to convert views. Opening the dialog refetches the list so views created in another tab appear. Classic/incompatible views are labeled and cannot be applied or overwritten here. Views storing SQL offer Open in SQL editor without updating the server record; unreadable saved time bounds show a warning before using the default range. The SQL sidebar also lists these server-saved SQL records read only, with explicit copy to the independent browser library. Builder/search records without SQL remain unsupported. Failed CRUD remains visible, without browser-storage fallback.
Copy links put raw version-1 JSON in the URLSearchParams logView parameter with fixed absolute bounds. Validation bounds state at 64KiB, checks timestamp/timezone/limit/filter constraints, rejects duplicate identifiers/NUL characters and discards unrecognized keys. Clipboard feedback follows successful copying, with a selectable fallback for blocked clipboard access. The native logView state is consumed after applying it, preserving later edits on reload; classic view retains its saved-view-ID meaning. Classic Logs parameters remain supported; linked datasets remain subject to server authorization.
logContext(request, signal?) calls POST /api/v1/query/context with dataset, valid pTimestamp, one string anchor field (message, body or log), contextWindow (1m, 5m or 1h) and pageSize:101. Context excludes active search/filter predicates. The typed response carries scope:"contextWindow", UTC bounds, limit, anchor index, duplicate count, anchoredDuplicate:"first", records and optional previous/next query payloads. The UI shows a bounded window and highlights the first anchor, with a notice for duplicates; it does not offer unbounded traversal. Missing supported anchors disable the action; errors/cancellation are explicit.
Logs opt into row/column virtualization for limits above 100 or more than 20 visible columns. Shared table defaults remain available to SQL/dashboard consumers. Sorting/export cover loaded records only; wrapped virtual rows use a fixed 96px preview, with full values in details. UTC/local/IANA selection applies to absolute editing and histogram/table/context display. Local selection resolves to a concrete IANA zone for stable saved/shared behavior. Display changes preserve UTC instants. Offset-only timezone identifiers are rejected. Invalid dates/DST gaps are rejected; repeated times require an explicit earlier/later choice. Histogram buckets remain fixed elapsed intervals across DST.
Team requests share the cookie session and credentials: 'include'. The administrative APIs use /api/v1, while API keys use the separate Prism prefix confirmed by modal/server.rs.
| Client method | HTTP endpoint | Success response |
|---|---|---|
about(signal?) |
GET /api/v1/about |
Normalizes oidcActive, capabilities.oidcRoleMapping, capabilities.oidcRoleSync; missing fields become false |
listUsers(signal?) |
GET /api/v1/users |
Users with direct privilege arrays and group-inherited roles; accepts groupRoles/userGroups and snake_case equivalents; nullable email/picture become absent |
createUser(username, roles) |
POST /api/v1/user/{encoded username} |
Bare JSON role-name array; returns a text/plain one-time password |
deleteUser(id) |
DELETE /api/v1/user/{encoded id} |
Checked success; response body is not needed |
addUserRoles(id, roles) |
PATCH /api/v1/user/{encoded id}/role/add |
Bare JSON role-name array; checked success |
removeUserRoles(id, roles) |
PATCH /api/v1/user/{encoded id}/role/remove |
Bare JSON role-name array; only manual OAuth grants are removable |
resetPassword(id) |
POST /api/v1/user/{encoded id}/generate-new-password |
text/plain one-time password; native users only |
userRoleSources(id, signal?) |
GET /api/v1/user/{encoded id}/role |
Roles contain {actions, roleType}; group roles and optional OAuth oidc provenance; issuer and grant lists may be null |
listRoles(signal?) |
GET /api/v1/roles |
Object of privilege arrays; internal roles excluded |
putRole(name, privileges) |
PUT /api/v1/role/{encoded name} |
Bare JSON privilege array replaces the full role; an empty array is valid |
deleteRole(name) |
DELETE /api/v1/role/{encoded name} |
Empty success; default roles and roles in use cannot be deleted |
defaultRole(signal?) |
GET /api/v1/role/default |
JSON string or null |
setDefaultRole(name) |
PUT /api/v1/role/default |
JSON string body; empty success |
clearDefaultRole() |
DELETE /api/v1/role/default |
Empty success |
listApiKeys(signal?) |
GET /api/prism/v1/apikeys |
Array of {keyId, apiKey, keyName, roles, createdBy, createdAt, modifiedAt, expiresAt}; apiKey is masked as **** plus the last four characters |
createApiKey(name, roles, ttlSeconds?) |
POST /api/prism/v1/apikeys |
Body {keyName, roles, ttlSeconds?}; returns the same shape with the full generated key |
getApiKey(id, signal?) |
GET /api/prism/v1/apikeys/{encoded id} |
Full key, retrieved only on an explicit copy action |
deleteApiKey(id) |
DELETE /api/prism/v1/apikeys/{encoded id} |
Checked success; backend returns {keyId, keyName, message} |
API key listing accepts ?privilege=reader|ingestor|admin|writer|editor|superadmin (alias priviledge), matching direct or group-inherited role privileges by kind (an admin key does not match reader, and a super-admin key does not match admin). Each entry's roles lists direct roles only. Unknown or differently cased values, or a repeated parameter, return 400; unknown parameter names are ignored and leave the list unfiltered.
API key expiry is optional: ttlSeconds must be an integer from 1800 (30 minutes) through 7776000 (90 days), inclusive; invalid values return 400. Omitted or null TTL means no expiration. Responses include expiresAt, a UTC timestamp or null; the frontend also accepts an omitted field from older servers for legacy records. Finite lifetimes are elapsed durations from creation, independent of time zones or daylight saving changes. The creation form defaults to 90 days and offers presets, custom minutes/hours/days, and No expiration. TTL cannot be edited after creation. Expired keys remain listed and deletable; their names remain reserved until deletion.
Both X-API-KEY and Bearer authentication return 401 once server time reaches expiresAt, and key validation reports false. Temporary API-key sessions, including those created for intra-cluster requests, cannot extend the deadline or refresh as OAuth sessions. Intra-cluster requests on behalf of an expired key return 401. Requests authorized before expiry may finish on the node that authorized them. Expiration changes credential validity, not its assigned roles or an administrator's authority to create other credentials.
Upgrade every serving and metadata-writing node before creating finite API keys. Older nodes ignore expiration and can strip expiresAt when rewriting stored user metadata. Rolling back to those versions with finite keys in service is unsafe. Keep node clocks synchronized.
All reads validate successful response shapes. Password methods read text, reject empty responses and never JSON-parse successful passwords. Every Team method reports 401 through onUnauthorized; 403 remains a visible permission state. Text errors, including root-admin protections, OIDC-managed grants and roles in use, appear in the active dialog. JSON non_existent_roles and roles_not_assigned errors become readable role lists even when the response advertises text/plain.
Role/user names follow src/validator.rs: 3–64 ASCII characters, letters/numbers or _ - . @, no consecutive separators, an alphanumeric ending and case-insensitive reserved names. A leading separator is permitted by the backend. The frontend additionally reserves the role name default (case-insensitively) because /role/default shadows the name route. Existing privileges can include LLM resources or unrestricted null/all resources; creation offers dataset-scoped resources. Unrestricted privileges are labelled “of all resources”. The root user's internal superadmin privilege is accepted for display and is never offered in role forms. API key names have no backend role-name validator; the form requires a nonempty name and at least one existing role.
Passwords and newly created keys live only in an open result view, with copy feedback and a warning. They are cleared when the view closes and are never logged or written to browser storage. Copying an existing key uses the supported GET and passes its value directly to the clipboard; tables display exactly the list endpoint's masked value. Provider groups are a read-only exact-name mapping of existing roles, enabled only by explicit OIDC capabilities. Role inspection must succeed before OAuth removal; legacy grants and recorded manual grants are removable, and overlapping provider grants remain effective.
PromQL requests use the server-root /prometheus/api/v1 prefix and the same cookie session. Vite proxies /prometheus to the configured backend alongside /api. The dataset is the stream parameter; the client does not send X-P-Stream for queries.
| Client method | HTTP endpoint | Success response |
|---|---|---|
datasetInfo(name, signal?) |
GET /api/v1/logstream/{encoded name}/info |
Normalizes telemetryType, logSource[].log_source_format and optional latestEventAt |
promqlQuery({stream,query,time?}, signal?) |
POST /prometheus/api/v1/query |
Form body; success envelope with vector, matrix, scalar or string data |
promqlQueryRange({stream,query,start,end,step}, signal?) |
POST /prometheus/api/v1/query_range |
Form body; success envelope with matrix data |
promqlLabels({stream,start?,end?,match?,limit?}, signal?) |
GET /prometheus/api/v1/labels |
Sorted string array in the success envelope; normalized to {data,truncated} |
promqlLabelValues(label, request, signal?) |
GET /prometheus/api/v1/label/{encoded label}/values |
Same metadata envelope; __name__ discovers metric names |
Query bodies are application/x-www-form-urlencoded. Times are Unix seconds with at most millisecond precision. Metadata sends repeated match[] keys and omits undefined parameters. The explorer scopes labels and values with {__name__="<metric>"}, the current time bounds and limit=1000. It displays truncation warnings. Range queries are bounded to 31 days and 11,000 steps; the page validates before requesting. Explicit steps are sent verbatim. Automatic steps follow query windows, chart width and the 3,000-point cap.
The client checks envelopes, result shapes, finite numeric timestamps, string values and string-valued labels. Malformed success payloads become readable 502 contract errors. Values can be NaN, +Inf or -Inf; charts show gaps and tables show -. Dotted metric and label names retain their OTLP spelling and use quoted selectors or matchers.
Errors preserve the server error message and errorType. A PromQL JSON 401 with errorType:"forbidden" is mapped to 403 and does not call onUnauthorized. The explorer shows “Permission denied” for that query and keeps the session. Plain-text 401 still opens login; plain-text 403 remains a permission error. Queries run independently, so a failed query does not hide successful results.
about().capabilities.promql uses an explicit boolean when supplied. If absent, it is true only when license.plan is a string other than OSS. Unavailable servers show a mode explanation without a pricing link.
listMetricsDatasets reads the name-only stream list and inspects each dataset's /info with at most six requests in flight. A logSource format of otel-metrics selects a PromQL dataset; missing formats fall back to telemetryType:"metrics". Forbidden or missing info is skipped. Other errors fail discovery. The result is sorted by name.
The explorer snapshots query text, type, step and bounds on Run. Time changes and refresh use the last applied configuration; edits wait for the next Run. Re-runs and navigation abort in-flight requests. URL state uses repeated query, type, step, and range or absolute ISO start/end; valid URL queries run on load. Successful history keeps ten queries per dataset under parseable:promql-history:<stream>.
The opt-in e2e-live/metrics.spec.ts uses the live suite's server, credentials and mount-path variables. It ingests a fresh metrics_live_<suffix> dataset through /v1/metrics, checks gauge/counter queries and metadata, compares table values with API responses, checks an unsupported query, and deletes the dataset afterwards. It is excluded from npm run check.
All alert and target operations use the cookie-authenticated /api/v1 prefix. Dynamic IDs are URL encoded. Reads accept an abort signal; mutations do not. Success payloads are checked by alertsContract.ts, including flattened promqlConfig, runtime instances and deliveries. Malformed successes become 502 errors. Plain-text failures retain their message and status: missing alerts/targets are 400, and deleting an in-use target is 409. Expired-session 401s invoke session recovery. The Rust dataset-permission 401 bodies (User does not have access to stream- … and User does not have access to PromQL alert stream, including the ActixError: wrapper) become 403 permission errors without invalidating the session or losing a failed-save draft. The SQL helper’s Stream not found: … 401 becomes 404. Other 401s still recover; PromQL’s existing JSON errorType:"forbidden" mapping is unchanged. Outbound-policy JSON errors retain their message and detail.
| Client method | HTTP endpoint | Success response |
|---|---|---|
listAlerts(signal?) |
GET /api/v1/alerts?limit=1000&offset=N |
Summary array; fetches pages until complete and normalizes capitalized severity |
getAlert(id, signal?) |
GET /api/v1/alerts/{id} |
Full alert config with optional flattened runtime |
createAlert(body) |
POST /api/v1/alerts |
Full config, HTTP 200 |
updateAlert(id, body) |
PUT /api/v1/alerts/{id} |
Full config; sends promqlConfig again |
deleteAlert(id) |
DELETE /api/v1/alerts/{id} |
Plain-text success, ignored by the UI |
enableAlert(id) |
PATCH /api/v1/alerts/{id}/enable |
Full config |
disableAlert(id) |
PATCH /api/v1/alerts/{id}/disable |
Full config, state:"disabled" |
muteAlert(id, state) |
PATCH /api/v1/alerts/{id}/update_notification_state |
Full config; body {state} |
evaluateAlert(id) |
PUT /api/v1/alerts/{id}/evaluate_alert |
Config before asynchronous evaluation; sends no body |
listAlertTags(signal?) |
GET /api/v1/alerts/list_tags |
String array |
listAlertTargets(signal?) |
GET /api/v1/targets |
Array of {target,enabled,error?} |
getAlertTarget(id, signal?) |
GET /api/v1/targets/{id} |
One status wrapper |
createAlertTarget(body) |
POST /api/v1/targets |
Bare masked target |
updateAlertTarget(id, body) |
PUT /api/v1/targets/{id} |
Bare masked target; name is immutable |
deleteAlertTarget(id) |
DELETE /api/v1/targets/{id} |
Bare masked target |
Summaries include queryType (same camelCase values as detail) but omit query and evaluation config. Older servers omit queryType; for those threshold rows the list falls back to detail reads with at most six requests in flight, caches resolved types per client by alert id across reloads (current servers provide authoritative query types in summaries), retries failed lookups on the next reload and retains other rows when one lookup fails. Non-threshold rows are never read through detail. It does not guess from names or the optional PromQL config. Search, tag filtering, sorting and 25-row pagination are local. Summary dates use chrono display formatting; detail dates are RFC3339. Notification state is "notify", a summary timestamp (including the maximum-year indefinite sentinel), or detail {mute:"indefinite"|timestamp}. Expired mutes are displayed as notifications on. Custom mute dates are interpreted in UTC.
PromQL threshold creation requires about().capabilities.promqlAlerts === true; absence means unavailable even on a paid license. Discovery reuses Metrics' OTLP dataset checks and dataset-scoped editor metadata. Frequency is 1–1440 whole minutes, hold duration defaults to 0s and cannot exceed 30 days, threshold values must be finite, and at most 20 targets are allowed. SQL uses queryType:"code"; the server derives datasets from SQL and enforces one numeric aggregate and no subqueries. Both SQL and PromQL accept empty targets for state tracking. New rules send notificationConfig.interval:1; no recurring-reminder control is exposed. Local validation handles fields and PromQL syntax/type, while engine and policy validation stays with the server.
PUT replaces flattened extras. Payload helpers preserve user extras and resend the complete PromQL config while omitting runtime, execution identity and managed response fields. Editing a PromQL rule resets runtime and captures the editing user as owner. Dataset and type controls are locked on edit. Both query editors become read-only during a pending save and unlock on failure. Tags are individual fields backed by an array; edit and duplicate preserve commas, spaces, ordering and repeated tags exactly. Builder rules offer Convert to SQL rule to users with write permission. The form displays the stored SQL and settings; only Convert and save changes queryType to code. Cancelling or a rejected save leaves the stored builder rule unchanged. The server preserves enabled and mute state. Builder duplication remains unavailable. Duplicate opens a pre-filled form before saving. Dashboard handoff accepts dataset, queryBuilderType=promql|sql, alertQuery and title on /alerts/new. Classic /alerts/create redirects there with its query string intact. React Router's data router blocks dirty-form navigation, including browser Back; document navigation uses beforeunload.
Preview never calls evaluate_alert: PromQL uses the existing instant-query client at /prometheus/api/v1/query, compares each finite vector sample locally, and displays labels/value/breached or within. SQL uses the query client with startTime:evalStart and endTime:"now", matching the rolling window, and displays returned values. Draft query/dataset/threshold changes clear and abort the previous preview. Preview, instances and deliveries share compact key=value label chips. Detail uses a last-hour range query with readable series names and a horizontal threshold reference included in the y range. Displayed alert timestamps and mute deadlines use 24-hour UTC. Evaluate-now feedback refreshes the config again after two seconds. Alert and target rows use a shared keyboard menu; Escape/selection return focus to its trigger. Successful row deletion focuses the next or previous surviving action, falling back to search when the list is empty.
Targets support Slack, Webhook (custom header rows and optional skip TLS) and Alertmanager (top-level username/password and optional skip TLS). Endpoints are returned as scheme://********, and header values/passwords are masked. Alertmanager's masked response reports type:"webhook"; auth-key presence without headers identifies it. Edits require re-entering endpoints and secret values. Secrets remain only in an open sheet and are never logged or stored. Delivery error URLs are redacted before rendering. Disabled target policy reasons are shown; the outbound-policy editor is outside scope. Reader privileges make management controls read-only when role inspection is available; the server always authorizes operations.
The opt-in e2e-live/alerts.spec.ts ingests a unique OTLP metrics dataset, creates a webhook target and PromQL rule through the UI, checks preview/evaluation/mute/enable/edit behavior against the API, and deletes alert, target and dataset even after failures. It is excluded from npm run check; use the live suite's credentials, server URL and mount-path variables.
| Client method | HTTP endpoint | Contract |
|---|---|---|
listDashboards(signal?) |
GET /api/v1/dashboards?limit=0 |
All tenant summaries; no server offset. Search, tabs, tag filter, sort and 25-row pagination are client-side. Summary timestamps use Chrono Display; details use ISO timestamps. |
getDashboard(id, signal?) |
GET /api/v1/dashboards/{id} |
Full document, including unknown fields. |
createDashboard(body) |
POST /api/v1/dashboards |
Returns 200 with server metadata. Title uniqueness is tenant-wide, exact and case-sensitive. |
updateDashboard(id, body) |
PUT /api/v1/dashboards/{id} |
Replaces the full document. A fresh GET compares modified before each save; a mismatch offers Reload or explicit Overwrite. |
deleteDashboard(id) |
DELETE /api/v1/dashboards/{id} |
Owner or admin; UI uses exact-title confirmation. |
Dashboards can store an optional dataset string in both the full document and list summary. The create/edit form calls it Default dataset: new tiles start with it, while existing tile datasets and queries remain unchanged. PromQL tiles inherit it only when metrics discovery confirms compatibility. Missing or null means no default; clearing the selection sends null. SQL chart creation and legacy local migration retain their source dataset.
Dashboard failures are plain text, including missing dashboards at 400, unauthorized ownership at 400, and RBAC at 403. Only 401 triggers session recovery. Reads forward abort signals; responses pass shape guards. Bodies are never logged. The frontend never calls add_tile or query-parameter PUT. Favourites also use a complete GET/PUT with conflict detection.
author is lowercase SHA-256 hex of the server user ID. crypto.subtle is preferred, with a tested dependency-free UTF-8 SHA-256 fallback for insecure non-localhost HTTP origins. Ownership controls editing; admins can delete another owner's dashboard but cannot update it. The latter is verified against Rust's strict body-update owner check and the disposable server: admin PUT of another user's dashboard returns 400 Cannot perform this operation: Dashboard does not exist or you do not have permission to access it. Reader roles have dashboard CRUD privileges, subject to ownership.
Saving patches a loaded object, never reconstructs a narrower model. Unknown top-level, tile, variable, section, time-range and config fields survive. This includes tenantId, version, dashboardType: "Report", sections and sectionId. The server still owns metadata and can overwrite it. The pre-save check is best effort: the API offers no conditional PUT or atomic revision token.
Every new tile uses a dependency-free ULID as tile_id, including dashboard duplicates and imports. Duplicate and nil IDs on load are repaired in the working copy without making it dirty. The original server document remains the conflict-check source; Discard restores the clean repaired baseline and keeps its IDs stable. Only owners see the next-save repair notice and can save the repair itself. Non-owners can leave or duplicate without an unsaved-changes prompt; Duplicate always allocates fresh IDs. Classic shapes are:
{
"tileType": "code",
"chartQuery": "SELECT * FROM \"logs\" LIMIT 100",
"dbName": ["logs"]
}{
"tileType": "promql",
"chartQuery": ["{__name__=\"system.cpu.load_average.1m\",host=~\"$host\"}"],
"promqlQueryType": ["range"],
"dbName": "$metrics_dataset"
}PromQL reads also accept legacy promqlQuery: {query, step, type}, promqlStep, and object chartQuery. Missing tileType means builder, matching classic and resources/ingest_demo_data.sh. Builder tiles run stored SQL or supported legacy objects; confirmed Edit as SQL always writes a string query. Unsupported legacy filters are blocked with an explanation rather than dropped. Unknown tile types and unknown chart types are preserved as named placeholders. Rendering dispatches on chartType and reads config.type only where classic does (the timeseries series style, and bar-versus-line for the ungrouped mapper); neither is rewritten. Timeseries/line/area/bar plot config.axes.x.field against config.axes.y.field (string or array), pivoting by config.axes.groupBy.field with duplicate values summed; timestamp x keeps a time axis in ascending order and other x values use a category axis instead of a table. Area fills each series at 10% opacity without stacking; bars are grouped unless config.layout.stacked, and horizontal with config.layout.barOrientation: "horizontal". Stacked bar is stored as chartType: "timeseries", config.type: "bar", config.layout.stacked: true. Timeseries tiles with a time axis can be dragged to set the dashboard's absolute range. PromQL instant vectors on these types become one categorical value series. Pie and donut take the category from the first config.axes.y.field and the value from the first other column, summing duplicate categories; PromQL gives one slice per series from its last finite sample. config.layout.label shows outside category labels. Gauge reads the first row of the first y field (PromQL: the first series' last finite sample) against config.layout.rangeFrom/rangeTo (default 0–100) and colours the arc green, yellow or red from config.layout.segments {value, isPercent}. Bubble is a native extension (classic stores it but draws nothing): x, y, config.axes.size.field and the group field, with PromQL bubble tiles showing a not-available message. Series and slice colours follow config.colourScheme (classic's thirteen palettes; unknown ids read as classic). Values use classic number formatting from config.layout.unit, precision (-1 means up to two decimals) and humanize; units keep a leading space. Query value reads a configured y-axis field, otherwise the first numeric column of the last row; PromQL uses the instant value. Its number is drawn in config.layout.queryValueTheme (default blue) and fitted to the tile; config.layout.sparkline adds a trend from SQL timestamp rows or, for PromQL, a range request alongside the instant one. A stat whose unit is % or percent shades its tile to the value's share of 0–100, clamped, as set by config.layout.percentFill: horizontal (the default, also used for unknown values), vertical, diagonal from the top-left corner with the covered area equal to the percent, or none. The editor preserves an existing unsupported chart type until explicitly changed. Config edits patch unit, precision, humanize, colour scheme, legend position, labels, orientation, series style, gauge range and segments, query-value theme and sparkline, percent fill, axis titles and the x, y, group-by and size fields without replacing other keys. Tile charts place the x-axis title before the legend, omit the visible series-count row while retaining its accessible summary and Show all control, and reserve space for a scrollable legend. Variable controls stay aligned when inline errors appear. Changing a query language or variable type removes only recognized fields belonging to the old type and preserves unknown fields.
capabilities.promqlDashboard === true enables PromQL tiles, and explicit promqlMetadata enables editor metadata. Instant/Range/Both reuse existing client methods; step follows span/max-data-points with a 60s fallback. Each tile displays server query errors. The server evaluates a subset documented in the repository's docs/promql.md. Tile requests run concurrently through a four-request limiter, forwarding cancellation and ignoring stale results.
The dashboard owns its URL time range (range, or absolute start/end), using 10m, 30m, 1h, 5h, 1d, 3d and custom. Save writes classic fixed form {startTime:"1h",endTime:"now",type:"fixed",label:"Last 1 hour",interval:3600000,shiftInterval:1} or type:"custom" with ISO bounds. Relative ranges remain anchored until a range change or Refresh, matching classic. Merely changing a variable selection never changes the document: var-<name> URL parameters hold selected values. Text controls commit local drafts after 400ms, Enter or blur. Tile queries depend only on effective bounds, their referenced query/dataset variables and refresh; layout, unrelated edits and Save do not query. Stored variable definitions support promql, promql_query, sql, list, text and dataset; blank fields are omitted like classic. Definitions retain default values, while interpolation handles both $name and ${name}, SQL literals/identifiers/dataset quoting and PromQL escaping/regex arrays. All uses .* for PromQL and * for list/SQL, comes first, and is selected when no default is stored. Only defined dashboard variable names count as references. Unknown tokens such as $1, $__interval and SQL/regex dollar literals pass through unchanged, matching classic. Dependencies wait for validated upstream URL values; cycle validation reads only consumed option-definition fields (queries, datasets and label filters), ignoring labels, defaults and extras. Unknown tokens in definitions are also allowed. Create alert requires concrete values and a single PromQL query; links interpolate dataset/query before handing off to Alerts. Classic builder alert links open SQL mode. Explore uses the existing SQL/Metrics URL contract.
Portable export contains {tags, variables, sections, tiles} plus dataset when a default is set and removes each tile_id, matching classic (which omits title and timeRange). Import from file or paste accepts empty tiles and promql_query, preserves an input timeRange and unknown fields, strips tenantId, allocates fresh tile IDs and resolves append layouts to finite coordinates. Files over 5 MB are rejected before reading. Duplicate also strips tenantId; both preserve dashboardType, so a Report copy remains a Report. Each stored section has its own 12-column grid, after unsectioned and orphaned tiles, in section order. Vertical compaction resolves collisions and deletion gaps; moves choose neighbours from the displayed compacted (y, x) reading order, swap coordinates, then compact within that section. Display bounds never rewrite stored widths/heights. Null, missing or non-finite y appends within the section; sections itself stays unchanged.
Live mode uses only the server. Legacy browser dashboards are read solely for a per-identity migration notice. A pure planner resolves exact title collisions, three imports run at a time, each local dataset becomes a classic SQL histogram tile, and failures remain retryable. Local copies remain until the separate confirmed Remove local copies action. Demo mode owns independent in-memory SQL, PromQL/variables and non-owner fixtures per client, with matching validation messages and no localStorage persistence.
Native login sends UTF-8 username/password Basic credentials only to the login endpoint, then uses cookies for data requests. Passwords are cleared from the form on success or dismissal and are never persisted or reused for queries. The redirect origin must satisfy is_valid_redirect_url in the current Rust handler. Frontend return paths accept local application paths and reject external/protocol-relative destinations, backslashes, control characters and authentication/API routes.
Sign in with SSO performs top-level navigation to /api/v1/o/login?redirect=<absolute same-origin intended path>. No credential fetch follows the identity provider. The backend owns authorization, provider discovery, callback verification and cookie creation through /api/v1/o/code. With no configured provider, it redirects to /oidc-not-configured; the frontend explains this state and offers native sign-in. The client stores the intended local path, selects live mode in session storage, and clears stale browser session/display cookies before starting SSO. Selecting live mode prevents an existing demo tab from reopening sample data after a successful provider callback.
When no server identity is available, a stream-list request also probes the session on pages that contain only local content. A live data or identity request returning 401 clears the displayed identity, removes stale local cookie hints and opens login with the intended route retained, except for the PromQL JSON-forbidden rule above. A 403 stays a permission-denied state; neither response offers sample results. Network/server errors remain visible request failures. A 403 from the post-login stream-list check does not reject otherwise successful native login, because a restricted account can lack list permission.
Logout uses browser navigation to /api/v1/o/logout?redirect=<origin>/login. This checkout removes the backend session and, for OAuth users when configured, redirects to the identity provider logout endpoint before the requested return. Native logout returns directly. The handler currently leaves browser cookies in place; subsequent 401 handling and a new SSO attempt clear stale cookies. The frontend does not claim that provider logout completed merely because the local navigation started. This differs from the previously deployed overlay described in the supplied wiki; that deployment's behavior is not this checkout's contract.
Query bodies use {query: sql, startTime, endTime, sendNull: true}. We deliberately omit the fields and streaming query flags, so the response is an unwrapped JSON array, including for aggregate SQL. ApiError preserves HTTP status, the backend error message and response detail. Network and cancellation errors remain native errors. Malformed successful payloads raise an explicit contract error.
References: src/handlers/http/modal/server.rs (routes), src/handlers/http/logstream.rs (list/schema), src/handlers/http/query.rs (query serialization), src/handlers/http/oidc.rs (native login/logout and redirect validation).
- The stream list returns names without telemetry type. The live client labels these entries as logs; Metrics discovers queryable datasets through
/inforather than guessing from names. - Search targets a
messagefield using a literal case-insensitive substring. Other schemas should use field filters or SQL. Sort order defaults top_timestamp; datasets with a different time partition field need explicit SQL until metadata support is added. - Filters quote fields and string literals, escape search wildcards, and only allow
=/!=. SQL editor text is sent to the backend SQL engine and its authorization checks; there is no browser-side SQL security boundary. - Native and OAuth sign-in use the existing backend authorization behavior. Team exposes the current checkout's OIDC group mapping, default role and provenance contracts; it does not provision identity-provider groups or enterprise native groups. Clerk, Stripe, billing and payment collection are absent.
- Alerts supports threshold rules only. Builder editing, anomaly/forecast, AI, outbound-policy editing and recurring reminders are outside scope. There is no alert-history endpoint.
- Mock HTTP contract tests verify frontend request shape and failure handling. The opt-in
e2e-livesuite exercises a disposable real backend and local mock identity provider; see validation-review.md for actual completed runs. A mock provider does not establish interoperability with a real deployment or identity provider.
Each demo client owns an independent, in-memory Team fixture: the root admin, a native analyst with a labelled group-inherited role, an OAuth user with one provider and one manual role, a second OAuth user with only fallback access, roles covering all five editable privilege kinds (including a writer of all LLM keys), an OIDC default, and two masked API-key entries. About advertises OIDC mapping and sync so every Team tab is visible. Mutations update that fixture, validate names and role references, protect the root admin and roles in use, and retain provider grants when a manual overlap is removed. User creation/reset and API-key creation return transient demo secrets; reload creates a fresh fixture and never loads those secrets from storage.
demoAlerts.ts owns independent alert/target state per client, returns clones, and advertises PromQL alerts. Fixtures include a firing PromQL rule with firing/resolved instances and a delivery error, SQL and builder rules, a muted PromQL rule, and two masked targets (one disabled by policy). Mutations enforce field limits, target references, immutable target names, state transitions and in-use deletion conflicts. The demo does not run a scheduler or make deliveries, and network/DNS policy checks remain server-only.
Demo mode is enabled only during Vite development or when built with VITE_ENABLE_DEMO=true; ordinary production builds ignore a previously stored demo choice. Demo mode makes no network calls and does not authenticate users. It provides three log datasets with the same schema and the same deterministic 100-row fixture sequence, plus demo_metrics with gauges and a cumulative counter, and demo_traces with related spans and events. APM demo aggregation derives from those same trace rows. Times are anchored when the client is created so the initial time ranges contain useful data; values and relative ordering are deterministic. Refresh does not simulate ingestion.
Demo PromQL supports selectors and matchers, range windows, rate/increase/irate/delta, selected over-time functions, one outer sum/avg/min/max/count aggregation, and scalar literals. Other expressions return an explicit 422 explanation. Metadata honours selectors and limits. Full PromQL needs a server.
The demo SQL evaluator accepts SELECT * FROM <sample dataset> with optional AND filters (=/!=/<>), the query builder's CAST(field AS VARCHAR) ILIKE ... ESCAPE ... search expression, one ORDER BY column, and LIMIT. Alert previews also support a single COUNT/SUM/AVG/MIN/MAX aggregate, with an optional alias and the same filters. It applies ISO or relative rolling-window bounds. It rejects unsupported SQL, grouped aggregates, joins, other projections, OR conditions and multiple statements with an explanation. Use a live backend for full SQL.
Traces uses POST /api/prism/v1/services/traces/list and /detail. List requests send the dataset, captured start/end bounds, explicit traces/spans/errorSpans option, exact service filter, sort and bounded pagination. Lists, counts and detail discovery filter the exact half-open interval [startTime,endTime); the native query transport expands to minute boundaries without changing those SQL predicates. Empty or reversed intervals return 400. Detail carries dataset, trace ID and bounds and retains its expansion around the discovered trace timestamps; optional recorded fields/events use a separate bounded SQL query. Discovery requires the otel-traces source. The decoder accepts an omitted/null optional service name on detail spans while retaining strict identity and numeric validation.
APM uses typed SQL builders through the existing /api/v1/query client, with schema inspection and quoted dataset/column/value inputs. Every aggregate deduplicates service+trace+span identities first. Counts and rates describe spans, including all span kinds. The selected dataset time partition supplies precise predicates and buckets, with a minute-expanded transport envelope. Dependency edges join observed parent identities after deduplication; missing or ambiguous parents are excluded. Demo queries dispatch to the APM adapter before the generic SQL fallback.
Add data creates datasets with X-P-Log-Source set to json, otel-logs, otel-metrics or otel-traces. Generated JSON commands use /api/v1/ingest; OTLP commands use /v1/{signal} with JSON encoding. Configuration contains environment-variable credential placeholders. Destinations with custom partitions are explicitly unsupported by guided setup: the required fields are explained, configuration/copying/verification remain unavailable, and selecting or creating an unpartitioned dataset restores setup. Verification reads dataset statistics, schema and info: the first check records a baseline, later lifetime-count increases report new stored events without claiming sender identity. Endpoint edits retain the baseline; destination changes reset it.
See observability validation for bounds, caps, error behavior, live evidence and reference screenshots.
Agents uses existing dataset list, schema, metadata and SQL requests. Discovery recognizes flattened gen_ai.agent.name, gen_ai.agent.id, gen_ai.operation.name, gen_ai.request.model, gen_ai.response.model and gen_ai.tool.name; direct dataset links defer the full schema scan until the selector is focused or rescanned. Discovery is limited to four concurrent schema requests and skips forbidden/missing datasets. Queries quote dataset/field identifiers and use the dataset time partition. Up to 100 recent trace IDs containing GenAI telemetry seed a query for related spans, including children without GenAI attributes; the outer response is bounded to 1,000 flattened records. Explicit half-open UTC predicates preserve exact time bounds inside the minute-expanded query envelope. Search, status and model filters apply to the loaded runs. Run normalization deduplicates span identities before counting usage, preserves event content and displays absent values distinctly from zero. A unique outer agent span can certify recovery within its descendants: explicit completed OK permits recovered child errors, which remain visible. Failed or incomplete ancestors and separate roots retain their outcome. Completed UNSET spans are accepted; missing status or completion remains incomplete. Missing identity, parents, clipped time coverage and the separate 100-trace / 1,000-record caps are disclosed. Evaluations and cost estimates remain deferred until telemetry and pricing inputs are reliable. Unknown demo dataset names cannot return synthetic records.
Cluster reads GET /api/v1/cluster/info and /api/v1/cluster/metrics through the shared authenticated client. The first returns node identity, storage/staging paths, reachability and nullable status/error text. Metrics are independent: a denied metrics request does not hide node health. Nonfinite Rust metric values serialized as null display as unavailable per field. Missing cluster routes (404/405) show a deployment-mode explanation; 403 and 401 retain shared permission/session handling. No node deletion is exposed.
Errors aggregates grouped issues and frequency through the existing POST /api/v1/query contract. Dataset metadata and schema determine the trace or log interpretation and the time partition. OTLP spans are deduplicated before aggregation; log occurrences count severity-matched records. Exact service, error type and message values identify a group, with SQL NULL kept distinct from literal display placeholders. Aggregates cover the full captured interval independently of paginated issue and occurrence lists. SQL enforces precise half-open bounds inside a minute-expanded transport envelope.
Trace type and message come from one deterministic representative row, preferring populated exception events, so different events cannot form an invented pair. Logs recognize textual severity aliases, exact OTLP severity codes 17–24 and Pino/Bunyan level codes 50/60; msg is a recognized message alias. The log SQL handoff returns full stored records, with internal query metadata retained only in sample mode. See the Errors runbook for alias behavior, counting rules and real-server tests.
The Error spans view retains the native trace-list contract with options: "errorSpans", server sorting and pagination for datasets advertising otel-traces. Failed metadata or schema requests remain explicit and retryable. Dataset/range changes cancel obsolete requests and reset pagination. Trace and SQL investigation links preserve captured query bounds. No classic grouped-issue endpoint or persisted issue lifecycle API is required.
Network access reads and replaces GET/PUT /api/v1/alert-target-policy. The complete object contains allowPrivate, allowInvalidTls, allowedDomains, allowedCidrs, deniedDomains, and deniedCidrs. All fields are required; the server is authoritative for authorization, CIDR validation and allow/deny overlaps. This policy governs outbound alert delivery, not ingress access to Parseable. The editor retains failed-save drafts and protects unsaved navigation. Demo writes are isolated within each client. The backend caches policy per node and loads it on startup; cross-node propagation is not guaranteed by a successful save.
Onboarding uses the existing telemetry-aware dataset inventory and Add data routes. It does not call the side-effecting demo-data endpoint or provision a hosted workspace. Keystone and LLM management remain unsupported by this checkout: native deep links explain the limitation and offer deployment-dependent classic handoff only when classic is available. No provider keys are read or stored.