feat(explore): Trace inspect + Log inspect — cross-layer trace/log query tools (#72) ## Why The per-layer Traces / Logs / Browser-errors / Pod-logs tabs answer "what's happening in *this* service?" — but operators often need to query **across layers**, by trace-id / keyword / category / pod, with no layer in hand. This adds two cross-layer **inspect** power-tools modeled on Metrics inspect: name a service (pick it, type it, or leave it blank), set conditions, get one result. As an inspect surface it deliberately exposes every layer / service / flag rather than pinning defaults. ## What ### Trace inspect (`/operate/trace-inspect`) Cross-layer trace query with a **Native ↔ Zipkin** source toggle. Optional entity — **Pick** (layer→service→instance→endpoint), **Type** (service name + real flag, which the BFF base64-encodes into the OAP id), or **blank** (all services). Native conditions (status / order / duration / tags / trace-id / time) plus the duration-distribution scatter — drag to brush-filter the list, click a dot to open the trace (a brush drag no longer also opens a detail). Zipkin gets its own service / remote-service / span pickers + annotation query. Result and detail reuse the **same shared widgets** as the per-layer Traces tab. ### Log inspect (`/operate/log-inspect`) Cross-layer log query; row-click opens a shared full-payload popout. Three sources: - **Raw** — OAP `queryLogs` (Tags / Trace ID / Time / Limit); Tags is a custom on-theme key/value autocomplete. - **Browser** — the BROWSER layer's JS errors, filtered by **Category, Version and Page** (`serviceVersionId` / `pagePathId`) plus time; **upload and manage source maps** inline, and the row popout resolves the minified stack against them. Entity is Pick / Type / blank with no pinned layer. - **Kubernetes Pod logs** — on-demand container tail. **Pick or Type** a service (Type is layer-less — the typed name encodes to a service id, and a Real flag is exposed), then the pod + container **auto-select**, and it **live auto-tails** (Interval + Pause/Start + Refresh) with **Include / Exclude** raw-regex filters. Live and never persisted → no cold-stage; surfaces OAP's `errorReason` (e.g. a terminated pod). A tip notes that each pod is the service instance, surfaced as its live Kubernetes pod, so it must be currently running. ### Shared + cross-cutting - New shared widgets (`render/widgets/`): `TraceListPanel`, `TraceDetailCard`, `TraceDistribution`, `ZipkinTraceDetailCard`, `LogStreamPanel`, `LogDetailPopout`, `BrowserErrorPopout` — extracted from the per-layer views and reused by **both** the inspect tools and those tabs (no behavior/look change to the per-layer Traces / Logs / Browser tabs). - New `TagInput` primitive — a custom dark, anchored autocomplete dropdown (tag key suggestions before `=`, per-key values after) replacing the browser-native `<datalist>`, in the inspect Tags fields and the per-layer Traces / Logs tag inputs. - BFF: one `POST /api/explore/query` dispatches by kind + source; byte-exact entity-id encoding in `util/entityId.ts` (verified against OAP `IDManager`); `fetchNativeList` / `fetchLogs` / `fetchBrowserErrors` extracted from the per-layer routes; k8s reuses the existing pod-log routes. Cold-stage is honored for native trace / raw logs / browser errors and correctly a no-op for k8s pod logs and Zipkin. - **RBAC**: both tools gate on **`inspect:read`** — the same verb as Metrics inspect, so one grant covers all three inspectors (the prior dedicated `explore:read` verb was removed). Note: this also grants the **viewer** role Metrics inspect. - MAL/DSL rule cards now show the full rule name (wrapping to a second line) with the status pills moved to the bottom-right corner. - i18n: every new string in `en.json` first + all 7 non-English locales.
Horizon UI is the next-generation web UI for Apache SkyWalking — a config-driven, dark-dense, multi-layer observability front end built for feature parity with the legacy skywalking-booster-ui on the same OAP GraphQL query-protocol and MQE. It renders services, instances, endpoints, topology, traces, logs, alarms, and profiling across 44 instrumentation layers, and ships an in-browser admin suite for runtime rules, RBAC, template management, and cluster status. Dashboards are JSON templates published to OAP — new screens are configuration, not code.
visibleWhen) — render a widget only when an MQE metric has a value or an entity attribute matches (e.g. language = JAVA, container_name = lifecycle).queryTraces on BanyanDB with inline spans, or queryBasicTraces on any backend), plus Zipkin traces in a separate tab when a layer enables both, with state/order/duration/tag filters, a duration distribution chart, and second-precision time windows..map files, resolved to original file/line/column/symbol with a source snippet.Horizon UI is a pnpm-workspaces monorepo:
apps/ui — the Vue 3 + TypeScript (strict) single-page app, built with Vite. State via Pinia, data via @tanstack/vue-query, charts via Apache ECharts (wrapped — never instantiated directly in a view), topology and flame graphs via D3, 3D via Three.js + TresJS, code editing via Monaco.apps/bff — a Fastify (Node) backend-for-frontend. It is the only tier that talks to OAP, shaping every reply for the SPA, owning timezone conversion, template sync, auth/RBAC, and the audit log.packages/api-client, packages/design-tokens, packages/templates — the shared GraphQL client, the canonical design tokens, and the bundled dashboard JSON.The BFF speaks three OAP contracts, all owned upstream and treated as fixed:
POST /graphql) — metrics, topology, traces, logs, alarms, browser errors.17128) — runtime rules/DSL, cluster status, metrics inspect, live debugging.The flow inside the BFF is one-directional — http → logic → client → OAP — and bundled templates are synced to OAP on first boot (bundled → local draft → push), after which the running UI renders only what OAP serves.
Prerequisites: Node (see package.json engines — >=22) and pnpm via Corepack (the repo pins the version through packageManager).
corepack enable
pnpm install
Run the dev servers — Vite serves the UI on :9091 and proxies /api to the BFF on :8081:
pnpm dev # run UI + BFF together pnpm dev:ui # UI only (Vite on :9091) pnpm dev:bff # BFF only (:8081, NODE_ENV=development, tsx watch)
Quality gates and build:
pnpm type-check # TypeScript strict, all packages pnpm lint pnpm test:unit # vitest + jsdom pnpm build # build all packages pnpm package # produce the distributable dist/ (BFF dist/server.js + static UI) pnpm start # run the packaged server (HORIZON_CONFIG=./horizon.yaml)
License headers and dependency licenses are enforced in CI via skywalking-eyes — run pnpm license:check (or pnpm license:fix) before pushing.
A multi-stage, multi-arch image (linux/amd64, linux/arm64) ships the BFF plus the static UI on node Alpine, runs as a non-root horizon user, exposes port 8081, and mounts /data for state files. The Dockerfile builds dist/ from source inside the image (no host pre-step); images are published to GHCR and Docker Hub per release.
docker build -t horizon-ui:local . docker run --rm -p 8081:8081 -v "$PWD/horizon.yaml:/app/horizon.yaml:ro" horizon-ui:local
See docs/setup/container-image.md for image tags, env vars, mounting horizon.yaml, and a Kubernetes example.
Horizon UI is configured by a single horizon.yaml (hot-reloaded, with ${VAR} environment-variable interpolation) — see horizon.example.yaml. Key sections:
server — host / port.oap — queryUrl, adminUrl, zipkinUrl, timeoutMs, and optional outbound basic-auth.auth — backend local or ldap (with LDAP bind / user-filter / group-mapping and an optional audited break-glass local admin).rbac — four built-in roles (viewer / maintainer / operator / admin) over fine-grained, verb-namespaced permissions (e.g. dashboard:write, rule:write:structural, source-map:write).session, debugLog, sourceMaps (browser-error source-map cache), layers.excluded, query.landingServiceCap, and the state-file paths (audit log, setup state, alarms state, wire debug log) — defaulting under /data/* in the container.Local user passwords are argon2-hashed; generate a hash with the BFF CLI. Set session.cookieSecure: true when running behind HTTPS.
Operator-focused documentation (setup, OAP compatibility, access control, customization, components, and operate) lives in this repo under docs/ and is rendered on the SkyWalking website. Start with the Quick Start, then OAP Connection and Auth/RBAC.
Contributions are welcome. Horizon UI is a greenfield rewrite that tracks the OAP GraphQL query-protocol and MQE — backend contracts are fixed and owned by apache/skywalking. Read CLAUDE.md for the project's working principles (correctness first, validate against a live OAP, TypeScript strict, charts wrapped, density beats whitespace), keep CHANGELOG.md current, and run the type-check / lint / unit-test / license-header gates before opening a PR.
Licensed under the Apache License 2.0 — see LICENSE and NOTICE.
Apache SkyWalking, SkyWalking, and the Apache feather logo are trademarks of The Apache Software Foundation.