signoz-open-dashboard

Documentation

Embed a live SigNoz dashboard in one afternoon

This service ships as a single Docker image that serves the embed app and proxies the read-only SigNoz API. Bring a SigNoz 0.97.0 backend and an API key with dashboard view permission.

Getting started

Prerequisites: Node >=20 and pnpm@9 for source builds, or Docker for the image. You also need a running SigNoz v0.97.0 backend and an API key with dashboard view permission.

Docker (recommended)

bash
docker run -p 8080:8080 \
  -e SIGNOZ_BASE_URL=http://<signoz-host>:30303 \
  -e SIGNOZ_API_KEY=<read-only-key> \
  taosherio/signoz-open-dashboard:latest

# 1. open a dashboard: http://localhost:8080/embed/<dashboardId>
# 2. embed it:
#    <iframe src="http://localhost:8080/embed/<dashboardId>?relativeTime=30m"></iframe>

From source

bash
pnpm install

# dev: web on :5173 proxies /api/signoz to the API on :8080
SIGNOZ_BASE_URL=http://<signoz-host>:30303 SIGNOZ_API_KEY=<key> pnpm dev:api
pnpm dev:web

# production
pnpm build
SIGNOZ_BASE_URL=http://<signoz-host>:30303 SIGNOZ_API_KEY=<key> node apps/api/dist/main.js

Verify connectivity against SigNoz directly before debugging the proxy:

bash
curl -H "SIGNOZ-API-KEY: <key>" \
  http://<signoz-host>:30303/api/v1/dashboards/<dashboardId>

Configuration

The backend is a single upstream fixed by SIGNOZ_BASE_URL; a backend address is never accepted from URL parameters. A non-http(s) base URL crashes at boot, and an empty default key only warns.

VariableRequiredDefaultNotes
SIGNOZ_BASE_URLYes-The only upstream. http(s) only, never taken from the URL (SSRF guard).
SIGNOZ_API_KEYNoemptyDefault key, overridable by the URL apiKey. Empty only warns.
PORTNo8080Listen port.
UPSTREAM_TIMEOUT_MSNo30000query_range timeout; dashboard metadata uses 10000.
MAX_REFRESH_SECONDS_FLOORNo10Refresh intervals below this are clamped.
LOG_LEVELNoinfopino JSON logs.
CORS_ORIGINNo*Public by design, do not tighten blindly.

Embed URL

Base: {EMBED_ORIGIN}/embed/:dashboardId. An invalid dashboard id short-circuits to a 404 empty state without calling upstream. The view is fully URL-driven and syncs back via history.replaceState, so every view is a shareable link.

html
<iframe
  src="https://embed.example.com/embed/<dashboardId>?apiKey=<key>&relativeTime=30m&theme=shadcn&mode=light&refresh=30s&var-env=prod"
  style="width:100%;border:0"
  allowfullscreen>
</iframe>
ParamExampleDefaultNotes
apiKey?apiKey=<key>env.SIGNOZ_API_KEYURL wins over env. Missing key renders a 401 empty state. Never written back unless the caller put it there.
relativeTime / startTime+endTime30m, or epoch seconds30mNative time params. Legacy from=now-30m&to=now is translated once at mount.
themeshadcn / legacyshadcnUnknown values fall back to shadcn.
modelight / darklightColor mode, orthogonal to theme.
localezh / enzhRendered by the active theme.
refreshoff / 30s / 1mdashboard valueClamped to >= 10s.
annotationstrue / falsetrueRead-only annotation markers.
var-<name>?var-env=proddashboard defaultHighest priority, overrides dashboard defaults. One dashboard can serve every customer, region or tier.
title / toolbar?title=falsetrueChrome toggles for kiosk and big-screen use.
timeControl / refreshControl / modeControl / fullscreenControl / localeControlshow / hidden / disabledshowPer-control visibility. toolbar=false hides the whole bar.
fullscreen?fullscreen=truefalseEnter fullscreen right after load.

Auto-resize: the child ships @iframe-resizer/child. Parents may optionally load @iframe-resizer/parent@5; without it the embed falls back to internal scroll.

Themes and variables

Two themes implement the same ThemeModule contract (Tokens, Toolbar, WidgetCard, ErrorState): shadcn (Tailwind + Recharts, default) and legacy (antd + ECharts). Color mode is orthogonal: ?theme=legacy&mode=dark.

Variable handling covers QUERY-type candidates via POST /api/v2/variables/query, DYNAMIC-type candidates via GET /api/v1/fields/values, and expression substitution via POST /api/v5/substitute_vars. A var-<name> parameter wins over the dashboard default.

A custom theme adds themes/<name>/ plus one registry line. The core/ layer stays theme-agnostic and is never touched by a theme.

Proxy matrix

All SigNoz traffic goes through ALL /api/signoz/*, which strips the prefix, forwards to SIGNOZ_BASE_URL, injects SIGNOZ-API-KEY, strips inbound authorization and cookie headers, and returns the response with an x-embed-request-id. The proxy denies by default: only the routes below are allowed.

UpstreamMethodPolicy
/api/v1/dashboards/:idGETPass through. PUT/POST/DELETE and /lock return 403 EMBED_READONLY.
/api/v3/query_range, /query_range/formatPOSTPass through (legacy panels).
/api/v4/query_rangePOSTPass through.
/api/v5/query_rangePOSTPass through (primary path).
/api/v5/substitute_varsPOSTPass through.
/api/v2/variables/queryPOSTPass through (QUERY-type variable candidates).
/api/v1/fields/valuesGETPass through (DYNAMIC-type variable candidates).
/api/v1/version, /api/v1/featuresGETPass through or local stub.
/api/v1/rules, /alerts, /channels, /user/*, /org/*, /invite/*ANY403 EMBED_BLOCKED, never proxied.
All other /api/*ANYDefault-deny 403.

Timeouts: 30s for query_range and 10s for dashboard metadata; the frontend cancels in-flight requests via AbortSignal. Global throttle is about 120 requests/minute/IP.

Error codes

Every failure maps to a typed code and an empty state with Retry where it makes sense. Codes live in packages/shared/errors.ts.

CodeHTTPMeaning
EMBED_MISSING_API_KEY401No key in the URL and no env default.
EMBED_INVALID_API_KEY401Upstream rejected the effective key.
EMBED_DASHBOARD_NOT_FOUND404Invalid id format (short-circuited) or unknown id upstream.
EMBED_UPSTREAM_UNAVAILABLE502Connection failure or timeout to SigNoz.
EMBED_READONLY403A write was attempted against a read-only route.
EMBED_BLOCKED403Route outside the allowlist.

Operations

GET /healthz reports liveness and GET /metrics exposes Prometheus metrics (prom-client). JSON logs record the effective key source and an 8-char hash of the key, never the plaintext value; API key fields are stripped from query strings before serialization.

bash
curl http://localhost:8080/healthz
curl http://localhost:8080/metrics | head

The single-image deployment is stateless: no query results are cached and no keys are stored. Scale by running more replicas behind any load balancer.

Security notes

  • URL keys end up in browser history, proxy logs and referers. Use short-lived, read-only keys and rotate immediately on leak.
  • The embed is public by design: CSP frame-ancestors * and CORS * are intentional.
  • The frontend keeps keys in memory only. There is no localStorage, cookie or URL echo of the env default key.
  • Write interfaces are not merely hidden: they are blocked at the proxy with 403.