Guides
Telemetry (OpenTelemetry)
Telemetry is off by default: nothing is exported unless you turn it on. With --otel, BrowserHive exports traces, metrics and logs over OTLP/HTTP to any compatible backend: an OpenTelemetry Collector, Grafana Tempo/Loki/Mimir, Jaeger, Honeycomb, Datadog Agent and others.
browserhive --admin --otel --otelEndpoint http://127.0.0.1:4318 --otelServiceName browserhive-devOr with standard OpenTelemetry variables:
BROWSERHIVE_OTEL=true OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4318 browserhive --adminSettings
Section titled “Settings”| Key | Default | Meaning |
|---|---|---|
otel |
false |
Turn export on. |
otelEndpoint |
http://127.0.0.1:4318 |
OTLP/HTTP base URL; /v1/traces, /v1/metrics, /v1/logs are appended. Also OTEL_EXPORTER_OTLP_ENDPOINT. |
otelProtocol |
http/protobuf |
or http/json. Also OTEL_EXPORTER_OTLP_PROTOCOL. |
otelHeaders |
none | k=v,k2=v2, for example an API key. Secret. Also OTEL_EXPORTER_OTLP_HEADERS. In the config file, keep the key itself in the environment: { "Authorization": "Bearer {env:OTLP_TOKEN}" } (references). |
otelServiceName |
browserhive |
service.name. Also OTEL_SERVICE_NAME. |
otelSampleRatio |
1 |
Parent-based trace sampling ratio. Also OTEL_TRACES_SAMPLER_ARG. |
otelSignals |
traces,metrics,logs |
Which signals to export. |
otelVerbose |
false |
Also export database query and DevTools-protocol command spans. |
otelTraceUrlTemplate |
unset | Dashboard deep link; {trace_id} is substituted. |
BROWSERHIVE_* variables win over OTEL_* ones. The otelEndpoint, otelProtocol, otelHeaders, otelServiceName and otelSampleRatio keys are rejected unless otel=true, so a typo cannot silently do nothing. When export fails five times in a row the System page shows a degradation; serving is never affected. browserhive doctor checks that the endpoint is reachable.
What is exported
Section titled “What is exported”Resource attributes: service.name, service.version, service.instance.id, host.name, os.type, browserhive.transport.
Traces
Section titled “Traces”One trace per tool call, rooted at mcp.tool_call with browserhive.tool, browserhive.session_id, browserhive.principal, browserhive.event_id, browserhive.ok and browserhive.error_code, plus the caller’s harness identity: browserhive.harness, browserhive.harness_source and, when one was declared, browserhive.model (also on session.create). Children cover what the call did: session.create with one span per creation phase (validate, admit, reserve, prepare profile, resolve identity, launch, install policies, start tracing, apply identity, register), browser.launch, page.navigate (sanitized URL, status code), vault.fill and its steps, attention.wait, and with --otelVerbose also db.query and cdp.command. HTTP requests (http.request), WebSocket commands (ws.command), migrations (db.migrate) and background sweeps are traced too.
If the MCP client sends a W3C traceparent in the tool call’s _meta, the tool span joins the client’s trace. HTTP requests adopt an inbound traceparent and return one.
Metrics
Section titled “Metrics”| Instrument | Type | Unit | Attributes | What it measures |
|---|---|---|---|---|
browserhive.tool_calls |
counter | {call} |
tool, ok, error_code (failed calls only), harness |
Tool calls. |
browserhive.tool_call.duration |
histogram | ms |
tool |
How long each tool call took. |
browserhive.sessions.active |
up-down counter | {session} |
harness |
Live sessions. |
browserhive.session.launch.duration |
histogram | ms |
channel, stealth |
From the create request until the browser is up, per successful launch. |
browserhive.session.lifetime |
histogram | ms |
closed_reason |
How long each closed session lived. |
browserhive.ws.connections |
up-down counter | {connection} |
Open dashboard WebSocket connections. | |
browserhive.ws.buffered_bytes |
gauge | By |
connection_id |
Bytes waiting to be sent, for the 50 most backed-up connections. |
browserhive.ws.frames_dropped |
counter | {frame} |
channel (screencast, logs, feed) |
Frames not delivered because a connection was congested. |
browserhive.db.write_queue.depth |
gauge | {write} |
Writes waiting to be saved. | |
browserhive.db.dropped_writes |
counter | {write} |
table |
Writes lost because the queue was full or the write failed. |
browserhive.db.size_bytes |
gauge | By |
Database size. | |
browserhive.browser.rss_bytes |
gauge | By |
session_id |
Memory of each session’s browser, all its processes added up (memory they share counts once per process), sampled every 10 seconds. On Windows the figure is the working set. |
browserhive.attention.open |
up-down counter | {request} |
kind (attention, vault_confirm) |
Operator requests waiting for an answer. |
browserhive.attention.wait |
histogram | ms |
status |
How long each attention request waited until it was answered, timed out or cancelled. |
browserhive.vault.fills |
counter | {fill} |
result |
Vault fills. |
browserhive.blocklist.hits |
counter | {hit} |
source |
Blocked requests. |
browserhive.retention.pruned_rows |
counter | {row} |
table |
Rows removed by retention. |
browserhive.notifications.deliveries |
counter | {delivery} |
channel_kind, status |
Notification deliveries by outcome: sent, retrying, dead, suppressed, superseded. |
browserhive.notifications.reports |
counter | {report} |
kind, outcome |
Digest and anomaly-alert decisions: sent, late, empty, skipped, manual, resolved, in_app. |
browserhive.notifications.actions |
counter | {press} |
channel_kind, outcome |
Presses of act buttons in Telegram, Discord and ntfy, by outcome (unknown for buttons BrowserHive did not create). |
browserhive.process.rss_bytes |
gauge | By |
Memory of the BrowserHive process. | |
browserhive.process.heap_bytes |
gauge | By |
JavaScript heap in use. | |
browserhive.process.event_loop_lag |
gauge | ms |
Event-loop delay (99th percentile since the previous export). |
harness on metrics is one of the known harness names, unknown or other (every name BrowserHive doesn’t know is folded into other), so it adds a bounded number of series. The model, workspace and extra labels are never metric attributes.
Metrics are exported every 30 seconds. Gauges and the WebSocket and write-queue totals are read at export time, and a gauge only reports what exists at that moment: a closed session or connection disappears from the next export. With telemetry off nothing is measured.
Every log record, with trace_id, span_id, request_id, session_id and principal attached, so logs and traces correlate in your backend.
Secrets are scrubbed before export, exactly as for local logs and the database. See Security.
Trace deep links from the dashboard
Section titled “Trace deep links from the dashboard”Every timeline row on a session page shows its trace_id. Set --otelTraceUrlTemplate and it becomes a link into your tracing UI:
| Backend | Template |
|---|---|
| Jaeger | http://localhost:16686/trace/{trace_id} |
| Grafana Tempo | a Grafana Explore URL for your Tempo data source, with {trace_id} as the query; copy one from Grafana (“Share → Copy link”) and replace the trace id with {trace_id} |
| Honeycomb | https://ui.honeycomb.io/<team>/environments/<env>/trace?trace_id={trace_id} |
The template can be changed without a restart.
A local stack in one directory
Section titled “A local stack in one directory”Grafana with Tempo (traces), Loki (logs) and Prometheus (metrics) behind an OpenTelemetry Collector. Create these files in an empty directory and run docker compose up -d.
docker-compose.yml:
services: otel-collector: image: otel/opentelemetry-collector-contrib:latest command: ["--config=/etc/otelcol-contrib/config.yaml"] volumes: ["./otel-collector.yaml:/etc/otelcol-contrib/config.yaml:ro"] ports: ["4318:4318"] depends_on: [tempo, loki]
tempo: image: grafana/tempo:latest command: ["-config.file=/etc/tempo.yaml"] volumes: ["./tempo.yaml:/etc/tempo.yaml:ro"]
loki: image: grafana/loki:latest command: ["-config.file=/etc/loki/local-config.yaml"]
prometheus: image: prom/prometheus:latest volumes: ["./prometheus.yml:/etc/prometheus/prometheus.yml:ro"]
grafana: image: grafana/grafana:latest environment: GF_AUTH_ANONYMOUS_ENABLED: "true" GF_AUTH_ANONYMOUS_ORG_ROLE: Admin volumes: ["./grafana-datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml:ro"] ports: ["3000:3000"] depends_on: [tempo, loki, prometheus]otel-collector.yaml:
receivers: otlp: protocols: http: endpoint: 0.0.0.0:4318
processors: batch: {}
exporters: otlp/tempo: endpoint: tempo:4317 tls: insecure: true otlphttp/loki: endpoint: http://loki:3100/otlp prometheus: endpoint: 0.0.0.0:8889
service: pipelines: traces: receivers: [otlp] processors: [batch] exporters: [otlp/tempo] logs: receivers: [otlp] processors: [batch] exporters: [otlphttp/loki] metrics: receivers: [otlp] processors: [batch] exporters: [prometheus]tempo.yaml:
server: http_listen_port: 3200distributor: receivers: otlp: protocols: grpc: endpoint: 0.0.0.0:4317storage: trace: backend: local local: path: /var/tempo/traces wal: path: /var/tempo/walprometheus.yml:
scrape_configs: - job_name: otel-collector scrape_interval: 15s static_configs: - targets: ["otel-collector:8889"]grafana-datasources.yaml:
apiVersion: 1datasources: - name: Prometheus type: prometheus uid: prometheus url: http://prometheus:9090 isDefault: true - name: Tempo type: tempo uid: tempo url: http://tempo:3200 jsonData: tracesToLogsV2: datasourceUid: loki filterByTraceID: true - name: Loki type: loki uid: loki url: http://loki:3100Then:
browserhive --admin --otel --otelServiceName browserhive-devOpen Grafana at http://localhost:3000 → Explore. Pick Tempo and search for service browserhive-dev to see tool-call traces, Loki for logs, Prometheus for metrics. The Collector’s Prometheus exporter converts metric names to Prometheus style, for example browserhive.tool_calls becomes browserhive_tool_calls_total.
Jaeger works the same way with less setup: docker run -p 16686:16686 -p 4318:4318 jaegertracing/all-in-one:latest accepts OTLP/HTTP on 4318 directly (traces only).