Skip to main content
Glama
Adiakys

OtlpDashboard

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:

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)

# Backend
dotnet 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.


Related MCP server: otel-mcp-server

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:

Variable

Protects

DASHBOARD__BROWSERTOKEN

The SPA + read API (/api/v1/...) + MCP (/mcp)

DASHBOARD__OTLP__APIKEY

OTLP 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.

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

Or via env vars:

Dashboard__TelemetryLimits__MaxLogDays=30
Dashboard__TelemetryLimits__MaxTraceDays=7
Dashboard__TelemetryLimits__MaxMetricDays=7
Dashboard__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

Dashboard__Storage__Provider=Sqlite        # default
ConnectionStrings__Sqlite="Data Source=/app/data/telemetry.db"

Dashboard__Storage__Provider=PostgreSql
ConnectionStrings__PostgreSql="Host=...;Database=telemetry;Username=...;Password=..."

Dashboard__Storage__Provider=SqlServer
ConnectionStrings__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:

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:

Tool

Mirrors

query_logs

GET /api/v1/logs

list_log_services

GET /api/v1/logs/services

query_traces

GET /api/v1/traces

get_trace

GET /api/v1/traces/{traceId}

list_trace_services

GET /api/v1/traces/services

list_metrics

GET /api/v1/metrics

query_metric_points

GET /api/v1/metrics/points

list_metric_services

GET /api/v1/metrics/services

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

{
  "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:

FROM opentelemetrydashboard:latest
COPY 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:

{
  "id": "team-otel-pack",
  "name": "Team OTel Pack",
  "version": "1.2.0",
  "author": "platform@example.com",
  "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:

{
  "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.

{
  "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):

Construct

Meaning

{{ 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:

{
  "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

Method

Path

Purpose

GET

/healthz

Liveness + readiness probe (anonymous; checks DB, sink, retention, auth posture)

POST

/v1/{traces,logs,metrics}

OTLP HTTP/Protobuf ingestion

POST

/api/v1/auth/login, /logout

Session-cookie login / logout (anonymous)

GET

/api/v1/info

App name (anonymous); version + retention + storage provider when authenticated

GET

/api/v1/traces, /logs, /metrics

Query API (paginated). /metrics/points requires from/to

GET/POST/PUT/DELETE

/api/v1/dashboards

Dashboards CRUD. DELETE requires ?rowVersion=N (optimistic concurrency)

GET/POST/PUT/DELETE

/api/v1/widgets/definitions

Custom widgets CRUD. DELETE requires ?rowVersion=N

GET

/api/v1/widgets/libraries

Flat library list for the picker

GET

/api/v1/packs

Installed packs (libraries + dashboards)

POST

/api/v1/packs/reload

Re-scan the packs paths

POST

/api/v1/packs/install

Clone from git ({url,ref,path?})

POST

/api/v1/packs/{id}/update

Re-pull a git-installed pack

DELETE

/api/v1/packs/{id}

Uninstall a pack

POST/GET/DELETE

/mcp

MCP server (when Dashboard:Mcp:Enabled)

(boot)

seed dashboards from packs

builtin: 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.

Available Tools

8 tools
get_traceA
Read-onlyIdempotent

Return every span belonging to the given trace id, with each span's resource service.name. Errors if no spans match. Truncated=true means the trace exceeded the per-trace span cap and the spans returned are an early prefix.

NameRequiredDescriptionDefault
traceIdYesTrace id (32-character lowercase hex).

TDQS

A3.8/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With readOnlyHint/idempotentHint already covering the safety profile, the description adds real behavioral context: failure on no matches, the per-trace span cap, and the semantics of truncated=true (early prefix). It stops short of naming the cap value or suggesting a follow-up when truncated, keeping it from a 5.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three tight sentences: what is returned, what failure looks like, then the truncation caveat. No filler and the core behavior is front-loaded.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a single-parameter read tool with no output schema, the description covers return contents (spans plus service.name), the error case, and the truncation edge case. Minor gaps remain around the cap size and span ordering, but nothing critical to calling it correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% and the single traceId parameter is fully documented in the schema (32-char lowercase hex). The description adds nothing beyond 'the given trace id', so the baseline 3 applies — the schema does all the work here.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description uses a specific verb+resource ('Return every span belonging to the given trace id') and additionally states the enrichment ('each span's resource service.name'). This clearly separates it from query_traces or list_trace_services, though it never names those siblings explicitly.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Usage is implied by the required traceId ('the given trace id'), and the error condition when no spans match is stated. However, there is no explicit guidance on when to prefer this over query_traces for trace lookup, nor any prerequisites mentioned.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_log_servicesA
Read-onlyIdempotent

Distinct service.name values attached to log records inside the given UTC window. Sorted alphabetically.

NameRequiredDescriptionDefault
toYesWindow end (ISO-8601 UTC). Required, > from.
fromYesWindow start (ISO-8601 UTC). Required.

TDQS

A3.8/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already cover readOnlyHint and idempotentHint, so the bar is lower, and the description still adds genuinely useful behavior: results are de-duplicated ('distinct') and deterministically ordered ('sorted alphabetically'). It omits edge-case behavior such as what happens with an empty window or whether the list is capped, keeping it short of a 5.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two short sentences, front-loaded with what is returned before the ordering detail. Every clause carries information; nothing is repeated from the schema or annotations.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a two-parameter read-only listing tool with no output schema, the description effectively characterizes the return value (distinct service names, alphabetical) so an agent knows what it gets. Minor gaps around empty-result and list-size behavior are the only missing pieces.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100% and both parameters are fully documented (ISO-8601 UTC, to > from). The description only restates that the pair forms a UTC window, adding little beyond the schema, so the baseline 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states the exact resource ('distinct service.name values') and scopes it to log records inside a UTC window, which implicitly separates it from list_metric_services and list_trace_services. It doesn't explicitly name those siblings, so differentiation is inferable rather than stated.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Usage is only implied: the window scoping suggests this is a discovery/enumeration tool to run before query_logs, but the description never says when to use it, when not to, or what alternative to pick. An agent must infer the discovery workflow from the return shape alone.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_metricsA
Read-onlyIdempotent

Every instrument currently in the store, with point count and originating service. The four-field key (resourceHash, scopeName, name, kind) identifies a single time-series for query_metric_points.

NameRequiredDescriptionDefault

No parameters

TDQS

A3.7/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The readOnlyHint and idempotentHint annotations already establish the safety profile. The description adds useful return-value context by naming the point count, originating service, and key fields, but does not cover pagination, ordering, or scale considerations for enumerating all instruments.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two tightly written sentences front-load the tool's purpose and then explain the key linkage to query_metric_points. Every sentence carries useful information without repetition.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a zero-parameter listing tool without an output schema, the description does a good job explaining the returned fields and the key structure. It could be slightly more complete by mentioning pagination or whether the full store is always returned, but the core context is present.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

This tool has zero input parameters, so there is no parameter semantics to explain. The baseline score for a parameterless tool applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states that this tool returns every instrument in the store along with point count and originating service, giving a specific resource and return content. It connects to query_metric_points but does not explicitly distinguish itself from list_metric_services, so sibling differentiation is incomplete.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The second sentence implies that the returned four-field key is used by query_metric_points, which gives some usage context. However, it does not explicitly say when to use this tool versus list_metric_services or query_metric_points, leaving the guidance implied rather than stated.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_metric_servicesA
Read-onlyIdempotent

Distinct service.name values across the recorded instruments. Sorted alphabetically.

NameRequiredDescriptionDefault

No parameters

TDQS

A3.5/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint and idempotentHint, so safety and repeatability are covered. The description adds one useful behavioral detail beyond annotations: results are sorted alphabetically. It does not describe return shape, empty-result behavior, or any scoping/pagination behavior.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two short sentences, both front-loaded with the essential output description and ordering. No filler or repetition; every phrase contributes information.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a zero-parameter, read-only list tool with annotations and no output schema, the description is nearly complete: it tells the agent exactly what values come back and how they are ordered. The only minor gap is that it relies on 'recorded instruments' rather than explicitly naming metrics.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

This tool takes zero parameters, so the baseline is 4. There are no parameter semantics to explain, and the schema is empty as expected for a pure enumeration endpoint.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states the resource precisely: distinct service.name values, with the source identified as 'recorded instruments' and the output ordering given. It is distinguishable from list_log_services and list_trace_services by its instrument/metric framing, though it never explicitly says 'metrics' and omits a leading verb.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

There is no when-to-use guidance, no prerequisite context, and no routing to alternatives such as list_log_services, list_trace_services, or query_metric_points. The intended use is only implied by the tool name and resource description.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_trace_servicesA
Read-onlyIdempotent

Distinct service.name values attached to traces whose earliest span falls inside the given UTC window. Sorted alphabetically.

NameRequiredDescriptionDefault
toYesWindow end (ISO-8601 UTC). Required, > from.
fromYesWindow start (ISO-8601 UTC). Required.

TDQS

A3.9/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint and idempotentHint, so safety is covered. The description adds real behavioral context beyond that: results are distinct service.name values, the window is evaluated against each trace's earliest span, and output is alphabetically sorted.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two sentences, zero waste. The core operation is front-loaded and the ordering guarantee follows as a short trailing clause.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description compensates by describing what comes back (distinct service.name values, alphabetically sorted). The only minor gap is that no sibling or pagination/edge-case guidance is included, but nothing essential to correct invocation is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so both params are documented and a 3 baseline applies. The description goes beyond that by clarifying that the from/to window filters on the earliest span timestamp rather than any span, which is meaningful semantic information not present in the schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb+resource: distinct service.name values attached to traces, with the window defined by earliest span and an ordering guarantee. The word 'traces' implicitly distinguishes it from list_log_services and list_metric_services, though those siblings are never named explicitly.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The scope statement ('traces whose earliest span falls inside the given UTC window') implies when the tool applies, but there is no explicit when-to-use or when-not guidance and no reference to query_traces or the sibling service-list tools as alternatives.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

query_logsA
Read-onlyIdempotent

List log records inside a UTC time window, ordered by timestamp descending. Supports keyset pagination via the 'cursor' returned in 'nextCursor'.

NameRequiredDescriptionDefault
toYesWindow end (ISO-8601 UTC). Must be strictly greater than 'from'. Required.
fromYesWindow start (ISO-8601 UTC, e.g. 2026-04-23T12:00:00Z). Required.
limitNoMaximum records returned. Defaults to QueryApi.DefaultLimit; capped at QueryApi.MaxLimit.
cursorNoOpaque continuation token returned by a previous call. Pass to fetch the next page.
serviceNoFilter by service.name (exact match).
traceIdNoFilter by trace id (32-char lowercase hex).
minSeverityNoFilter records with severity_number >= this value (OTLP 0-24).

TDQS

A3.8/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnly and idempotent, so the safety profile is covered. The description adds non-obvious behavior: results are ordered by timestamp descending and pagination is keyset-based via a 'nextCursor' token, which is genuinely useful beyond the annotations. It stops short of stating the concrete default/max limit values or any rate limits.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two tight sentences with zero filler; the ordering guarantee is front-loaded before the pagination note. Every clause carries information an agent needs.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema the description usefully names the 'nextCursor' field, which is the one return detail an agent must know to page. For a 7-parameter filtered query tool this is nearly sufficient, though a note on default page size or the existence of service/trace/severity filters would close the gap.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so every parameter including cursor, limit and the severity filter is already documented in the schema. The description only echoes the cursor contract already present, adding no new per-parameter semantics. Baseline 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ('List log records') scoped to a UTC time window with an explicit ordering. The resource noun separates it from query_traces and query_metric_points, though it never names those siblings directly.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Usage is implied: call this to enumerate log records in a window. It explains pagination mechanics but gives no explicit when-to-use versus query_traces or when to reach for list_log_services instead of filtering by service here.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

query_metric_pointsA
Read-onlyIdempotent

Return the points of a single instrument time-series identified by (resourceHash, scopeName, instrumentName, kind) inside the required UTC window. Use list_metrics first to discover the four-field key. Truncated=true means the window contains more points than the configured cap and the points returned are an early prefix.

NameRequiredDescriptionDefault
toYesWindow end (ISO-8601 UTC). Required, > from.
fromYesWindow start (ISO-8601 UTC). Required.
kindYesInstrument kind: Gauge, Sum, Histogram, ExponentialHistogram, Summary.
scopeNameYesScope name (use empty string for the anonymous scope).
resourceHashYesResource hash (lowercase hex) — see list_metrics output.
instrumentNameYesInstrument name.
includeAttributesNoSet true to hydrate per-point attribute maps. Default false (cheaper for single-value widgets).

TDQS

A4.2/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations establish readOnly/idempotent, and the description adds genuinely non-obvious behavior: a server-side cap can truncate results, and truncated=true means the returned points are an early prefix rather than the full window. That is the key caveat an agent needs before drawing conclusions from partial data. It does not mention ordering or what a point object looks like, keeping it from a 5.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three sentences, front-loaded with what is returned and the identity key, then the prerequisite, then the truncation caveat. No filler or restatement of the tool name.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description still covers the essential return semantics (points plus a truncation flag) and the discovery prerequisite. Minor gap: it says nothing about point ordering or how to page past a truncated window, which an agent would need for complete coverage of a capped result set.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% and each parameter already carries a description including enum-free kind values and the empty-string anonymous scope. The description adds only the framing that the four fields together form a key and that the window must be UTC — modest value on top of a fully documented schema, so the baseline 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Specific verb and resource: 'return the points of a single instrument time-series', with the exact composite key that identifies it. It is easily distinguished from list_metrics (discovery) and from the query_logs/query_traces siblings, which target different signal types.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

'Use list_metrics first to discover the four-field key' names the prerequisite discovery step and the sibling that supplies it. No explicit when-not-to-use guidance, but for a lookup tool gated on a composite key the sequencing instruction covers the main misuse risk.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

query_tracesA
Read-onlyIdempotent

List trace summaries inside a UTC time window, ordered by start time descending. Supports keyset pagination via 'cursor'/'nextCursor'.

NameRequiredDescriptionDefault
toYesWindow end (ISO-8601 UTC). Required, > from.
fromYesWindow start (ISO-8601 UTC). Required.
limitNoMaximum traces returned. Defaults to QueryApi.DefaultLimit; capped at QueryApi.MaxLimit.
cursorNoOpaque continuation token from a previous call.
serviceNoFilter by service.name (exact match on root span's resource).

TDQS

A3.9/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already cover the safety profile (readOnly, idempotent), and the description adds real behavior: deterministic ordering by start time descending and a keyset pagination contract. It still omits whether the window is inclusive/exclusive and that results are summaries rather than full spans, but it adds meaningful context beyond the annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two sentences, zero filler, with the core purpose and ordering front-loaded before the pagination detail. Every clause carries information an agent needs.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

There is no output schema, and the description partially compensates by naming the summary return type and the nextCursor field. Combined with the fully documented input schema, an agent has enough to call it correctly; only window-boundary semantics are left unstated.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so baseline is 3, but the description adds value by pairing the input 'cursor' with a 'nextCursor' continuation token that exists only in the response, clarifying the pagination loop. It does not restate the limit defaults or service filter semantics, which the schema already handles.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ('List trace summaries'), the scoping condition (UTC time window), and the sort order (start time descending). It is clearly distinct from get_trace (single trace) and list_trace_services, though no sibling is named explicitly.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Usage is only implied: the tool is for browsing traces within a window. There is no explicit when-to-use/when-not-to-use guidance and no pointer to get_trace for full trace detail, which is the obvious companion. The pagination sentence does signal a follow-up call pattern.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 8 tool updates
    • First observedget_trace
    • First observedlist_log_services
    • First observedlist_metric_services
    • First observedlist_metrics
    • First observedlist_trace_services
    • First observedquery_logs
    • First observedquery_metric_points
    • First observedquery_traces

TDQS

A4/5.0

Scored across 8 tools

Disambiguation5/5

Each tool has a distinct signal+action scope: listing services per telemetry type, querying records/points per signal, and fetching a single trace by id. The only potential overlap between list_metrics and query_metric_points is clearly resolved by the descriptions: one discovers instruments, the other reads points for a specific series.

Naming Consistency5/5

All names use a consistent snake_case verb_noun pattern (get_trace, list_*_services, query_logs, query_traces, query_metric_points). The get/list/query verbs are used coherently for detail, discovery, and time-window retrieval.

Tool Count5/5

Eight tools is well-scoped for a read-only OTLP observability dashboard covering logs, metrics, and traces. Each tool maps to a clear part of the discovery/query workflow without obvious filler.

Completeness4/5

The surface covers service discovery and querying for all three telemetry signals, plus trace detail retrieval. A notable minor gap is that query_logs and query_traces appear to support only time-window queries, with no service filter despite dedicated service-listing tools, so agents may need to scan and filter client-side.

Maintenance

ActivityMaintained
ResponsivenessWithin a week

Related MCP Connectors

Related MCP Servers

  • F
    license
    A
    quality
    D
    maintenance
    Enables querying OpenTelemetry logs stored in OpenSearch across development and production environments. Provides tools for searching logs by various criteria including free-text Lucene queries, trace IDs, service names, error levels, and specific fields.
    8
    -
  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables natural language querying and analysis of OpenTelemetry traces, metrics, and logs stored in Elasticsearch/OpenSearch, allowing AI assistants to investigate performance issues, find root causes, and explore system behavior.
    3 npm
    14
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Provides read-only access to Loki, Prometheus, and Tempo APIs, enabling natural language queries for logs, metrics, and traces. Supports multiple instances and authentication via bearer tokens.
    1
    MIT
  • A
    license
    B
    quality
    D
    maintenance
    Enables querying Grafana Tempo traces and spans with flexible filtering, including TraceQL queries, tag-based filters, duration ranges, and aggregated statistics.
    6
    7 npm
    GPL 3.0