OtlpDashboard

io.github.Adiakys由 GitHub 帳號 Adiakys 發布v26.11更新於 Nov 1, 2026

Query OpenTelemetry logs, traces and metrics from a self-hosted OTLP dashboard

已驗證STDIO僅桌面安全與監控資料與分析

概覽

AI 產生的概覽

自架 OpenTelemetry 儀表板的唯讀 MCP 端點,讓助理查詢已儲存的日誌、追蹤與指標。

可以做什麼
  • 依條件查詢日誌,並列出產生日誌的服務
  • 查詢追蹤、依 ID 取得單一追蹤,並列出追蹤服務
  • 列出可用指標、依時間範圍查詢指標資料點,並列出指標服務
  • 所有工具對應儀表板的唯讀 API,回傳相同的遙測資料
適用情境
適合已經執行這套 OTLP 儀表板、想讓助理不必打開網頁介面就能排查事故、檢視追蹤或核對指標的情況。它並非獨立的遙測來源,只能讀取儀表板已收集的資料。
執行需求
需要一個執行中的 OtlpDashboard 實例(Docker 映像或本機 .NET 建置),並將 DashboardMcpEnabled 設為 true,伺服器會以 Streamable HTTP 掛載於 4318 連接埠的 /mcp。若設定了 DASHBOARD__BROWSERTOKEN,用戶端須在 Authorization 標頭帶上該權杖。僅支援桌面用戶端。
安裝前請注意
MCP 端點與網頁應用程式、唯讀 API 共用 DASHBOARD__BROWSERTOKEN;若該權杖留空,此介面即為公開,任何能連上連接埠的人都能讀取遙測資料。工具本身唯讀,但該權杖同時可存取儀表板的唯讀 API。遙測資料可能含敏感請求內容,保留期限設定決定保存時間。

安裝

在 SourceWeft 中

  1. 開啟 儀表板中的 OtlpDashboard,將其新增到工作區。
  2. 為需要使用其工具的對話啟用該服務。

Desktop only,透過 STDIO。 STDIO 服務會啟動本機處理程序,因此需要 SourceWeft 桌面主機。

其他 MCP 客戶端

參照 儲存庫 中的啟動說明。

README

OpenTelemetry Dashboard

"Grafana was overwhelming, while Aspire was lacking."

This dashboard is the "just right" middle ground: a lightweight, self-hosted OTLP receiver and viewer designed for those who need professional observability without the setup fatigue.

[OpenTelemetry Dashboard — dashboard, traces, and logs views]

A self-hosted OTLP receiver and viewer for traces, logs, and metrics. .NET 10 + EF Core backend, Vue 3 / Nuxt 4 SPA. Stores telemetry in SQLite, PostgreSQL, or SQL Server. Speaks OTLP gRPC on :4317 and OTLP HTTP/Protobuf on :4318 (the same port also serves the SPA).

Website: adiakys.github.io/OtlpDashboard

Live demo: adiakys.github.io/OtlpDashboard/demo — static build with mock telemetry, no backend required. Login accepts any password (e.g. demo).

The demo runs against an in-browser mock backend (a tiny TS shim that intercepts every API call and returns generated DTOs) — not the real .NET host. It exists to let people click around without us hosting anything; behaviour can drift from the real server in subtle ways (some flows are stubbed, some validation paths return generated data instead of real errors). For a faithful preview run the docker compose stack below.


Quick start

Docker — full demo stack

The bundled docker-compose.yml starts the dashboard against Postgres plus a small demo workload (sample-server + sample-client + the OpenTelemetry Collector) that pushes traces, logs, and metrics so the dashboard isn't empty on first boot:

bash
docker compose up --build# OTLP gRPC : localhost:4317# OTLP HTTP : localhost:4318# SPA       : http://localhost:4318

Sources for the demo workload live under demo/ — sample app (Server / Client), the built-in widget-library pack (demo/packs/default/, distributed as a separate repo and consumed via git submodule), the demo-stack starter dashboard (demo/packs/default-dashboard/), and the Collector config. To run only the dashboard against an external Postgres / SQL Server, strip the demo services from your override file and point Dashboard__Storage__Provider + ConnectionStrings__* at your DB.

Local dev (no Docker)

bash
# Backenddotnet run --project src/OpenTelemetryDashboard.Host
# Frontend (separate terminal, hot-reload, proxies /api to :4318)cd web && pnpm install && pnpm dev

The backend runs migrations on every boot, so the SQLite file appears at ./src/OpenTelemetryDashboard.Host/data/telemetry.db on first run.


Configuration

All settings live under Dashboard:* in appsettings.json and can be overridden by environment variables using double underscores (e.g. Dashboard__BrowserToken).

Auth tokens

Two static bearer tokens gate the surface:

VariableProtects
DASHBOARD__BROWSERTOKENThe SPA + read API (/api/v1/...) + MCP (/mcp)
DASHBOARD__OTLP__APIKEYOTLP ingestion (gRPC + HTTP)

Auth is opt-in per surface, identically in Development and Production:

  • Set a token → that surface requires it.
  • Leave a token empty → that surface is public (allow-all). Leaving DASHBOARD__BROWSERTOKEN empty also makes the SPA skip the login screen (/api/v1/info reports requireAuth: false), so the dashboard opens straight away — convenient for a local Docker / Docker Compose run.

Either unset token surfaces as Degraded on /healthz (in any environment) and is logged at startup, so an open deployment is visible rather than silent.

The SPA's /login form exchanges the password for an HttpOnly + SameSite=Strict session cookie via POST /api/v1/auth/login; JS never sees the token after the exchange. POST /api/v1/auth/logout clears the cookie. OTLP clients send the API key as Authorization: Bearer <token> or as the x-otlp-api-key header.

Retention (max days)

Background job that drops rows older than the configured age. Defaults target a "debug + recent forensics" workload — logs hold a month so last-week incidents stay investigable; traces and metrics rotate weekly because high-cardinality spans and split-by metric attributes inflate fast.

jsonc
"Dashboard": {  "TelemetryLimits": {    "MaxLogDays": 30,    // default    "MaxTraceDays": 7,   // default    "MaxMetricDays": 7,  // default    "SweepIntervalMinutes": 60  }}

Or via env vars:

bash
Dashboard__TelemetryLimits__MaxLogDays=30Dashboard__TelemetryLimits__MaxTraceDays=7Dashboard__TelemetryLimits__MaxMetricDays=7Dashboard__TelemetryLimits__SweepIntervalMinutes=60

A value of 0 disables retention for that signal (records are kept indefinitely) — /healthz then reports Degraded so an unbounded window can't hide silently. The sweep runs each SweepIntervalMinutes per signal independently — a failure on one kind doesn't stop the others.

Storage provider

bash
Dashboard__Storage__Provider=Sqlite        # defaultConnectionStrings__Sqlite="Data Source=/app/data/telemetry.db"
Dashboard__Storage__Provider=PostgreSqlConnectionStrings__PostgreSql="Host=...;Database=telemetry;Username=...;Password=..."
Dashboard__Storage__Provider=SqlServerConnectionStrings__SqlServer="Server=...;Database=telemetry;User Id=...;Password=...;TrustServerCertificate=True"

MCP server (read-only)

An optional Model Context Protocol server exposes the same data as the read API (logs, traces, metrics) so an LLM client — Claude Code, Claude Desktop, MCP Inspector, etc. — can query the dashboard directly. Disabled by default; set the flag to mount it:

bash
Dashboard__Mcp__Enabled=true

Mounted at /mcp on the same port as the SPA (:4318) over Streamable HTTP, gated by the same browser bearer token (DASHBOARD__BROWSERTOKEN) that protects /api/v1/*. Tools exposed:

ToolMirrors
query_logsGET /api/v1/logs
list_log_servicesGET /api/v1/logs/services
query_tracesGET /api/v1/traces
get_traceGET /api/v1/traces/{traceId}
list_trace_servicesGET /api/v1/traces/services
list_metricsGET /api/v1/metrics
query_metric_pointsGET /api/v1/metrics/points
list_metric_servicesGET /api/v1/metrics/services

Wire it into Claude Code by dropping a .mcp.json at your project root:

json
{  "mcpServers": {    "otel-dashboard": {      "type": "http",      "url": "http://localhost:4318/mcp",      "headers": { "Authorization": "Bearer ${DASHBOARD_BROWSER_TOKEN}" }    }  }}

Built on the official ModelContextProtocol.AspNetCore SDK (jointly maintained by Anthropic and Microsoft).


Widgets

Dashboards are made of widgets dropped onto a 12-column grid. Three sources:

  • std — built into the bundle (Stat, Line, Sparkline, Gauge, Bar gauge, Pie, Heatmap, Recent traces, Top traces, Logs stream, Text).
  • custom — preset of a builtin saved by the user, persisted in the DB.
  • <library> — read-only widgets shipped by an installed library (filesystem or installed from a Git repo).

The picker dialog (+ Add widget while editing a dashboard) shows all three groups in the same modal. The search box filters by name, description, or source — type std, custom, or a library id to filter a whole bucket.

Custom legend labels

Line, Pie, Bar gauge and Heatmap widgets label their series, slices, bars or rows with the metric's attributes (Line adds the metric name when it shows several), which can get long. To shorten them, open the widget's config drawer and fill the Legend field (one per selected metric on Line). Use {attribute} to insert an attribute value, e.g. Requests {http.request.method} renders as Requests POST. Leave it empty to keep the default label. In pack JSON the same value is the legend field of the metric binding.

Creating a custom widget

  1. Open a dashboard, click Edit, add a builtin widget.
  2. Configure it (metric binding, thresholds, range, …).
  3. In the config drawer click Save as widget, give it a name, icon, and default size.

It now appears in the picker under My widgets, with inline edit and delete buttons that show on hover.

Notes:

  • The seed config is captured at save time — editing the template later changes only metadata (name, description, icon, default size). The seed config is immutable; clone + delete to change it.
  • Existing dashboard instances of a deleted custom widget render a placeholder, not a crash.

Packs (widget libraries + dashboards)

Everything that can ship with the dashboard — widget libraries, built-in dashboards, and any future asset type — is distributed as a pack: a single directory with pack.json at its root. The dashboard scans every entry in Dashboard:Packs:Paths (in order) and surfaces every pack's contents in the picker (libraries) and dashboard list (built-ins).

The shipped image already configures two paths in scan order:

  1. /app/data/packs — runtime-managed (volume, git installs, drag-and-drop)
  2. /app/builtin-packs — baked into the image layer (no volume shadowing on rebuild)

Derived images don't need to set any environment variable — just COPY into the second path:

dockerfile
FROM opentelemetrydashboard:latestCOPY my-pack/ /app/builtin-packs/my-pack/

When two paths expose packs with the same pack.json#id, the first in scan order wins and the rest are skipped with a warning — so a runtime install can override a baked-in default by sharing its id.

Two sample packs live under demo/packs/:

  • default/ (a submodule pointing at Adiakys/OtlpDashboard-default-pack) ships the .NET and PostgreSQL widget libraries.
  • default-dashboard/ ships the starter dashboard pre-bound to the demo compose stack.

Both are bind-mounted by docker-compose.yml, so docker compose up --build shows libraries + starter dashboard out of the box.

Install one of two ways:

  1. Drop a folder. Copy the pack into the packs path (volume-mount, Ansible, etc.). Click the refresh icon in the widget picker (or POST /api/v1/packs/reload) to re-scan.
  2. Install from a Git repo. Click the git-branch icon in the picker header (or POST /api/v1/packs/install with { url, ref, path? }). The server runs a shallow clone via LibGit2Sharp, parses pack.json, resolves HEAD to a commit SHA, and atomically moves the directory into the runtime-managed root. Allowed hosts are Dashboard:Packs:AllowedGitHosts (default github.com, gitlab.com). Use a tag for stable pinning; branches work but get a UI warning. The optional path parameter re-roots the install on a sub-directory, useful for monorepos that ship multiple packs side by side. Updates: the "Update" button on git-installed packs re-pulls the same ref (fetch && reset --hard).
Pack layout
my-pack/├── pack.json├── README.md            (optional — surfaced in the UI)├── LICENSE              (optional)├── libraries/│   ├── core/│   │   ├── manifest.json│   │   └── widgets/│   │       ├── sla-tracker/widget.json│   │       └── trace-heatmap/widget.json│   └── extras/│       ├── manifest.json│       └── widgets/…└── dashboards/    ├── default.json    └── ops-overview.json

pack.json:

json
{  "id": "team-otel-pack",  "name": "Team OTel Pack",  "version": "1.2.0",  "author": "[email protected]",  "license": "MIT",  "description": "Curated widgets and dashboards for service ownership reviews",  "homepage": "https://github.com/example/team-otel-pack",  "libraries": [    { "id": "core",   "path": "libraries/core" },    { "id": "extras", "path": "libraries/extras" }  ],  "dashboards": [    { "id": "default",      "path": "dashboards/default.json", "builtin": true },    { "id": "ops-overview", "path": "dashboards/ops-overview.json" }  ]}

id must match the directory name. Each library entry's manifest.json carries only id, name, description?, icon? — shipping metadata (version, author, license) lives at pack level. Dashboards flagged builtin: true are seeded into the dashboard store on first boot; the rest are installable templates.

Writing a widget

Each widgets/<kindId>/widget.json declares one widget. Two engines:

preset — wraps a builtin with a precooked config:

json
{  "name": "SLA Tracker",  "description": "p99 latency with SLO thresholds",  "icon": "i-ph-target",  "defaultSize": { "w": 4, "h": 3 },  "engine": "preset",  "baseKind": "metric-stat",  "defaultConfig": {    "calc": "last",    "unitKind": "ms",    "decimals": 1,    "thresholds": [      { "value": 0,   "color": "#7AAA7A" },      { "value": 200, "color": "#D9B566" },      { "value": 500, "color": "#E27A3F" }    ]  }}

baseKind is one of: metric-stat, metric-line, metric-sparkline, metric-gauge, metric-bar-gauge, metric-pie, metric-heatmap, recent-traces, top-traces, logs-stream, text. The shape of defaultConfig matches the corresponding form in the SPA — copy from a working instance (Save as widget produces a valid one).

spec — sandboxed HTML/SVG/CSS template with named metric bindings. Use this for card-style UIs that the standard chart widgets can't express: a database illustration whose fill level tracks a load metric, a grid of service tiles whose colour follows a per-service status metric, gauges with custom artwork, etc.

json
{  "name": "Database card",  "icon": "i-ph-database",  "defaultSize": { "w": 4, "h": 4 },  "engine": "spec",  "spec": {    "template": "<div class='db'>\n  <svg>...</svg>\n  <div class='stats'>\n    <strong class='{{ thresholdClass load.value load.thresholds }}'>{{ format load.value 'percent' 0 }}</strong>\n    <span>QPS {{ format qps.value 'ops' 1 }}</span>\n  </div>\n</div>",    "styles": ".db { display: flex; gap: 1rem; ... } .db .vellum-th-bad { color: var(--color-rust-600); }",    "dataBindings": [      { "name": "load", "type": "metric", "calc": "last", "unitKind": "percent",        "thresholds": [{ "value": 0, "color": "#7AAA7A" }, { "value": 70, "color": "#D9B566" }, { "value": 90, "color": "#E27A3F" }] },      { "name": "qps",  "type": "metric", "calc": "mean", "unitKind": "ops" }    ]  }}

The user installing the library picks one instrument per binding (load, qps, …) via the config drawer; the template/styles are immutable and ship with the library.

Template syntax (Mustache-light, no JS evaluation):

ConstructMeaning
{{ name }} / {{ name.path }}Interpolate a value (HTML-escaped).
{{ helper arg1 arg2 }}Call a whitelisted helper.
{{#if expr}}…{{/if}}Render block when truthy.
{{#each list as item}}…{{/each}}Loop, exposes item and _index.

Helpers available: format(value, unitKind, decimals?), percent(value, min, max), thresholdColor(value, thresholds), thresholdClass(value, thresholds) (returns vellum-th-ok / -warn / -bad), dateAgo(timestamp), dateTime(timestamp, preset?) (absolute date/time honoring the OS 12h/24h preference; preset defaults to datetime, also time/time-seconds/time-ms/datetime-seconds/datetime-long), pluralize(n, singular, plural), default(...values), comparators eq/neq/lt/lte/gt/gte (usable inside {{#if}}).

Binding shapes the template scope sees:

  • metric (default — splitBy not set): { value, unit, unitKind, thresholds }.
  • metric with splitBy: "service.name": array of { key, value, attrs, thresholds } — iterable with {{#each}}.
  • metric-series: array of raw { time, value, attributes } rows.

Sandboxing:

  • The Mustache renderer never evals — only whitelisted helpers run.
  • DOMPurify (lazy-loaded ~50 KB gzip) sanitises the rendered HTML: no <script>, no on*=, no javascript: URLs, no <iframe>.
  • CSS is auto-prefixed with the widget's instance scope so styles can't leak; @import, @font-face, expression(), and IE-era binding tricks are stripped before scoping.

Field rules enforced by the loader (engine: spec):

  • name ≤ 64 chars.
  • description ≤ 280 chars.
  • icon matches ^i-(ph|lucide)-[a-z0-9-]+$.
  • defaultSize.w ∈ [1, 12], defaultSize.h ∈ [1, 24].
  • defaultConfig ≤ 64 KiB; spec (the whole template+styles+bindings envelope) ≤ 256 KiB.

Invalid widgets are skipped (logged) and don't break the rest of the library.

The bundled demo/packs/default/libraries/postgres/ library ships postgres-server-card and db-scoreboard — two engine: spec widgets that double as live examples for authors of the spec engine.


Built-in dashboards

Dashboards ship inside packs — list them in pack.json#dashboards with builtin: true and the BuiltinDashboardSeeder folds each into the dashboard store on the first boot after install. The format matches the one the SPA emits via "Export JSON".

File format

Same envelope produced by the SPA's export, plus an optional top-level id:

json
{  "version": 1,  "id": "00000000-0000-0000-0000-000000000001",  "name": "Production Overview",  "widgets": [    { "id": "...", "kind": "std:metric-stat", "x": 0, "y": 0, "w": 4, "h": 3, "config": { /* opaque */ } }  ]}

Id resolution (precedence)

  1. Explicit id in the JSON wins, must parse as a Guid.
  2. default.json filename → 00000000-0000-0000-0000-000000000001 (the well-known default dashboard id). Convenient for distributing a non-empty default.
  3. Any other filename → SHA-256 of the filename, truncated to 16 bytes (RFC 9562 v8). Stable across deployments — same filename always produces the same dashboard id.

Idempotency

The seeder runs every boot but uses skip-if-exists semantics: an id already in the store is left alone. The single special case is the default dashboard — when default.json is present and the existing default row is still pristine (no widgets, RowVersion 0), the seeder upserts it once. Any user edit (saved widgets or RowVersion ≥ 1) makes the seeder back off.

To re-apply a built-in file, delete the dashboard via the UI first.

A demo lives at demo/packs/default-dashboard/dashboards/default.json (referenced from default-dashboard/pack.json with builtin: true), mounted by docker-compose.yml as /app/builtin-packs/default-dashboard.


Endpoints

MethodPathPurpose
GET/healthzLiveness + readiness probe (anonymous; checks DB, sink, retention, auth posture)
POST/v1/{traces,logs,metrics}OTLP HTTP/Protobuf ingestion
POST/api/v1/auth/login, /logoutSession-cookie login / logout (anonymous)
GET/api/v1/infoApp name (anonymous); version + retention + storage provider when authenticated
GET/api/v1/traces, /logs, /metricsQuery API (paginated). /metrics/points requires from/to
GET/POST/PUT/DELETE/api/v1/dashboardsDashboards CRUD. DELETE requires ?rowVersion=N (optimistic concurrency)
GET/POST/PUT/DELETE/api/v1/widgets/definitionsCustom widgets CRUD. DELETE requires ?rowVersion=N
GET/api/v1/widgets/librariesFlat library list for the picker
GET/api/v1/packsInstalled packs (libraries + dashboards)
POST/api/v1/packs/reloadRe-scan the packs paths
POST/api/v1/packs/installClone from git ({url,ref,path?})
POST/api/v1/packs/{id}/updateRe-pull a git-installed pack
DELETE/api/v1/packs/{id}Uninstall a pack
POST/GET/DELETE/mcpMCP server (when Dashboard:Mcp:Enabled)
(boot)seed dashboards from packsbuiltin: true dashboards loaded after migrations, idempotent

Errors return RFC 7807 application/problem+json with a traceId extension for log correlation. Per-IP rate limits apply to OTLP ingest (200 req/s default), the read API (60 req/s), mutations (10 req/s), and pack install (1 concurrent globally) — tunable under Dashboard:RateLimits:*.


Acknowledgements

Claude Code did a lot of the typing on this project. The direction, design, testing, and debugging stayed on me.


License

GPLv3.

來源:README.md,提交 5650b90

工具

0
SourceWeft 曾在隔離沙箱中以無憑證方式啟動該軟體包一次,以列出其工具,但未能完成:該軟體包類型無法在沙箱中執行。

版本歷史

1
  1. v26.11最新Nov 1, 2026

評分

已滿足適用檢查項的 50%

該評分衡量此收錄條目的完整程度,依據註冊中心條目、端點與公開來源計算得出。它不是對伺服器本身的評價,也不表示伺服器安全或目前可用:可連通性是帶日期的檢查結果,評分也不包含安裝量或使用者評價。

  • 近 30 天內可連通25 分

    請保持端點能夠回應 MCP 握手;SourceWeft 每 30 天探測一次。

  • 已宣告認證方式10 分

    請在註冊中心條目中宣告憑證請求標頭,或保持端點能夠回應探測,以便判定其認證方式。

  • 已列出工具15 分

    請讓伺服器在無憑證時回應 tools/list,或發布可在沙箱中無憑證啟動的軟體包。

  • 描述必需15 分
  • 圖示10 分
  • 文件15 分
  • 授權條款10 分