
OtlpDashboard
io.github.Adiakys由 GitHub 账号 Adiakys 发布v26.11更新于 Nov 1, 2026
Query OpenTelemetry logs, traces and metrics from a self-hosted OTLP dashboard
概览
自托管 OpenTelemetry 仪表盘的只读 MCP 端点,让助手查询已存储的日志、链路追踪和指标。
- 可以做什么
- 按条件查询日志,并列出产生日志的服务
- 查询链路追踪、按 ID 获取单条追踪,并列出追踪服务
- 列出可用指标、按时间范围查询指标数据点,并列出指标服务
- 所有工具对应仪表盘的只读 API,返回相同的遥测数据
- 适用场景
- 适合已经运行该 OTLP 仪表盘、希望让助手在不打开网页界面的情况下排查故障、查看链路或核对指标的场景。它不是独立的遥测数据源,只能读取仪表盘已采集的数据。
- 运行要求
- 需要一个运行中的 OtlpDashboard 实例(Docker 镜像或本地 .NET 构建),并设置 DashboardMcpEnabled 为 true,服务会以 Streamable HTTP 挂载在 4318 端口的 /mcp。若设置了 DASHBOARD__BROWSERTOKEN,客户端需在 Authorization 头中携带该令牌。仅支持桌面客户端。
安装
在 SourceWeft 中
- 打开 控制台中的 OtlpDashboard,将其添加到工作区。
- 为需要使用其工具的对话启用该服务。
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:
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)
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:
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__BROWSERTOKENempty also makes the SPA skip the login screen (/api/v1/inforeportsrequireAuth: 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.
Or via env vars:
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
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:
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:
Wire it into Claude Code by dropping a .mcp.json at your project root:
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
- Open a dashboard, click Edit, add a builtin widget.
- Configure it (metric binding, thresholds, range, …).
- 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:
/app/data/packs— runtime-managed (volume, git installs, drag-and-drop)/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:
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 atAdiakys/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:
- 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. - Install from a Git repo. Click the git-branch icon in the picker
header (or
POST /api/v1/packs/installwith{ url, ref, path? }). The server runs a shallow clone via LibGit2Sharp, parsespack.json, resolves HEAD to a commit SHA, and atomically moves the directory into the runtime-managed root. Allowed hosts areDashboard:Packs:AllowedGitHosts(defaultgithub.com, gitlab.com). Use a tag for stable pinning; branches work but get a UI warning. The optionalpathparameter 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
pack.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:
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.
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):
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 —splitBynot set):{ value, unit, unitKind, thresholds }.metricwithsplitBy: "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>, noon*=, nojavascript: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.iconmatches^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:
Id resolution (precedence)
- Explicit
idin the JSON wins, must parse as a Guid. default.jsonfilename →00000000-0000-0000-0000-000000000001(the well-known default dashboard id). Convenient for distributing a non-empty default.- 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
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
来源:README.md,提交 5650b90
工具
0版本历史
1- v26.11最新Nov 1, 2026
评分
已满足适用检查项的 50%
该评分衡量此收录条目的完整程度,依据注册中心条目、端点与公开来源计算得出。它不是对服务器本身的评价,也不表示服务器安全或当前可用:可连通性是带日期的检查结果,评分也不包含安装量或用户评价。
- 近 30 天内可连通25 分
请保持端点能够响应 MCP 握手;SourceWeft 每 30 天探测一次。
- 已声明认证方式10 分
请在注册中心条目中声明凭据请求头,或保持端点能够响应探测,以便判定其认证方式。
- 已列出工具15 分
请让服务器在无凭据时响应 tools/list,或发布可在沙箱中无凭据启动的软件包。
- 描述必需15 分
- 图标10 分
- 文档15 分
- 许可证10 分