Baserow sends operational metrics, sampled traces, and logs through OpenTelemetry. Use metrics for counts and latency trends over all requests, and use retained traces to diagnose individual requests.
The examples below use Honeycomb because the bundled development Collector exports to it. Other OTLP backends can build equivalent queries from the same metric names and attributes.
Configure every Baserow web and Celery process that sends telemetry through the tail-sampling Collector:
BASEROW_ENABLE_OTEL=true
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318
OTEL_TRACES_SAMPLER=always_on
OTEL_SEMCONV_STABILITY_OPT_IN=http
Configure the Collector separately:
HONEYCOMB_API_KEY=your-api-key
HONEYCOMB_METRICS_DATASET=baserow-metrics
Use the BASEROW_OTEL_SLOW_* settings to configure slow HTTP and Celery trace
classification, BASEROW_OTEL_LOG_LEVEL to control exported backend logs, and the
collector’s BASEROW_OTEL_* settings to tune its span and per-user metric budgets. See
Configuration and the bundled Compose/Collector configuration for
the current defaults.
The bundled configuration uses the Collector Contrib distribution because it requires
the tail-sampling and span_metrics connectors. A custom Collector build must include
both, and its span_metrics version must support
aggregation_cardinality_limit. The bundled Collector command also enables
processor.tailsamplingprocessor.recordpolicy; this annotates the selected composite
policy so short dependency spans are compacted from baseline traces only.
All spans belonging to one trace must reach the same tail-sampling Collector instance. Use trace-ID-aware routing when running multiple Collectors.
Use these separate metric families:
| Question | Metric | Unit | Dimensions |
|---|---|---|---|
| Endpoint traffic | http.server.request.duration |
Seconds | Templated http.route, http.request.method, http.response.status_code, and error type. |
| Authenticated users | baserow.http.server.user.request.duration |
Milliseconds | user.id, plus stable service and Collector identity. |
| Workspace invitations | baserow.workspace.invitation.created.calls |
Count | user.id, plus stable service and Collector identity. |
| Celery task execution | baserow.celery.task.duration |
Seconds | Stable task name, queue, and completion state. |
| Database/Redis dependency | baserow.dependency.duration |
Milliseconds | Stable database system, operation, status, service, and Collector identity. |
The endpoint histogram covers all requests. The user histogram is generated from
authenticated HTTP server spans before tail sampling, so trace sampling does not alter
its request count or duration distribution. The Collector discards the span-metrics
calls counter because HISTOGRAM_COUNT provides the same sampling-independent count from this one
histogram, avoiding a second datapoint for every active user and flush interval.
The dependency histogram is also generated from matching spans before trace tail sampling and baseline compaction. Its count and latency distribution therefore remain sampling-independent even when a trace is not retained. It deliberately excludes statements, table IDs, users, span names, and other high-cardinality dimensions.
The Celery histogram is emitted when a task completes, independently of trace retention. Its histogram count is the completed-task count. Keep task arguments, IDs, user IDs, and other unbounded values out of its dimensions.
Keep the families separate. Adding user.id to the endpoint metric or http.route to
the user metric would create a user x endpoint x method x status Cartesian product.
Never use a concrete URL, table ID, workspace ID, token ID, or other unbounded value as
an endpoint metric dimension.
Honeycomb ingests these OTLP histograms as native histogram columns. Only the base
field, such as http.server.request.duration, exists in the dataset schema; flattened
fields such as x.avg or x.p95 do not exist, and queries referencing them are
rejected. Aggregate directly on the base field:
HISTOGRAM_COUNT(x):
observations in the interval, which is the request count.SUM(x): sum of the observed values.AVG(x): average of the observed values.P50(x), P95(x), and P99(x): percentiles computed from the histogram buckets.HEATMAP(x): the distribution over time.COUNT_DATAPOINTS(x): ingested datapoints, which measures ingestion volume.Plain COUNT counts metric events, not histogram observations, and therefore is not a
request count.
Set query granularity to the metric capture interval or a multiple of it.
Build these queries in HONEYCOMB_METRICS_DATASET:
| Panel | Query |
|---|---|
| Requests per endpoint | HISTOGRAM_COUNT(http.server.request.duration) grouped by http.route. |
| Requests per method | HISTOGRAM_COUNT(http.server.request.duration) grouped by http.request.method. |
| Requests per status | HISTOGRAM_COUNT(http.server.request.duration) grouped by http.response.status_code. |
| Average endpoint duration | AVG(http.server.request.duration) grouped by http.route. |
| Endpoint p50, p95, or p99 | P50/P95/P99(http.server.request.duration) grouped by http.route. |
| Endpoint duration heatmap | HEATMAP(http.server.request.duration), filtered by http.route to focus on one route. |
| Top endpoints | HISTOGRAM_COUNT(http.server.request.duration) grouped by http.route, ordered descending. |
| Server errors by endpoint | Filter http.response.status_code >= 500, then count histogram observations grouped by http.route. |
Use only templated routes such as /api/database/rows/table/{table_id}/. If
http.route contains a concrete identifier, fix that instrumentation before using it
as a board dimension.
Build these queries in the same metrics dataset:
| Panel | Query |
|---|---|
| Requests per user | HISTOGRAM_COUNT(baserow.http.server.user.request.duration) grouped by user.id, where otel.metric.overflow != true. |
| Distinct active users | COUNT_DISTINCT(user.id) where baserow.http.server.user.request.duration exists and otel.metric.overflow != true. |
| Average duration per user | AVG(baserow.http.server.user.request.duration) grouped by user.id. |
| Per-user p50, p95, or p99 | P50/P95/P99(baserow.http.server.user.request.duration) grouped by user.id. |
| Per-user duration heatmap | HEATMAP(baserow.http.server.user.request.duration) filtered by user.id. |
| Average concurrent load per user | SUM(baserow.http.server.user.request.duration) grouped by user.id, divided by the result-bucket width in milliseconds. This is average load, not exact peak concurrency. |
| Cardinality overflow | HISTOGRAM_COUNT(baserow.http.server.user.request.duration) where otel.metric.overflow = true; alert when non-zero. |
| Per-user metric ingestion volume | COUNT_DATAPOINTS(baserow.http.server.user.request.duration) to measure the cost of the selected interval. |
The per-user metric includes authenticated requests only. Requests aggregated into an overflow row remain visible in overall counts, but their individual users cannot be identified.
To alert on invitation abuse, sum
baserow.workspace.invitation.created.calls over the trigger window, group by
user.id, exclude otel.metric.overflow=true, and alert when any user exceeds the
chosen limit. The counter records successful invitation creation and resend operations
without depending on trace retention or an HTTP route.
The otel.metric.overflow column appears in the dataset schema only after the first
overflow datapoint is ingested, and Honeycomb drops query filters on columns missing
from the schema. Until then the overflow filters cannot be saved and are unnecessary,
because no overflow rows exist. Add the filters and the overflow alert as soon as the
column appears.
Build these queries from baserow.dependency.duration in the metrics dataset. During
semantic-convention transitions, group by the populated stable attribute
(db.system.name or db.operation.name) and fall back to its legacy counterpart
(db.system or db.operation) when needed.
| Panel | Query |
|---|---|
| Dependency calls by system | HISTOGRAM_COUNT(baserow.dependency.duration) grouped by db.system.name (or db.system). |
| Dependency calls by operation | HISTOGRAM_COUNT(baserow.dependency.duration) grouped by db.operation.name (or db.operation). |
| Average dependency duration | AVG(baserow.dependency.duration) grouped by database system and operation. |
| Dependency p95 or p99 | P95/P99(baserow.dependency.duration) grouped by database system and operation. |
| Dependency duration heatmap | HEATMAP(baserow.dependency.duration), filtered to one database system or operation. |
| Dependency errors | Filter to error status, then HISTOGRAM_COUNT(baserow.dependency.duration) grouped by database system and operation. |
As with request histograms, plain COUNT counts exported metric datapoints rather than
dependency calls. Use HISTOGRAM_COUNT for call volume.
Build these queries from baserow.celery.task.duration in the metrics dataset:
| Panel | Query |
|---|---|
| Completed tasks by name | HISTOGRAM_COUNT(baserow.celery.task.duration) grouped by task_name. |
| Completed tasks by queue | HISTOGRAM_COUNT(baserow.celery.task.duration) grouped by queue. |
| Average task duration | AVG(baserow.celery.task.duration) grouped by task_name and optionally queue. |
| Task p95 or p99 | P95/P99(baserow.celery.task.duration) grouped by task_name. |
| Task duration heatmap | HEATMAP(baserow.celery.task.duration), filtered to one task or queue. |
| Failed task completions | Filter state = FAILURE, then HISTOGRAM_COUNT(baserow.celery.task.duration) grouped by task_name. |
Use the scheduled-task counter separately when the distinction between published and completed tasks matters. Do not infer task volume from sampled traces.
BASEROW_OTEL_USER_METRICS_FLUSH_INTERVAL controls resolution and datapoint volume. A
shorter interval produces finer charts and more datapoints for continuously active
series.
BASEROW_OTEL_USER_METRICS_CARDINALITY_LIMIT is a per-Collector circuit breaker for
unique metric dimension combinations. Once reached, new combinations are aggregated
under otel.metric.overflow=true instead of increasing series cardinality without a
bound. BASEROW_OTEL_USER_METRICS_SERIES_EXPIRATION removes a user’s series after the
configured idle period, preventing historical users from remaining in every subsequent
export.
Choose the limit above the measured peak unique users within the series-expiration
window, leave headroom for multiple services and resource scopes, and alert on
overflow. When using multiple Collectors, group by user.id without grouping by
collector.instance.id so the query combines a user’s observations from every
Collector.
The bundled Collector receives complete candidate traces and makes one tail-sampling
decision for the whole trace. Explicitly forced traces bypass the normal bounded
budget. Within that budget, errors and configured slow HTTP/Celery traces are
prioritized before representative baseline traffic. Error and slow traces can still be
dropped when their allocated capacity is exhausted. Baseline traces may have noisy
dependency detail compacted; all-traffic dependency counts and latency remain available
from baserow.dependency.duration.
Application spans form a semantic skeleton rather than mirroring every Python call. An
HTTP trace contains its request root, one concrete API view entry point such as
GridViewView.get, top-level calls to selected primary handlers or one outermost
action/job operation, and at most one level of explicitly important phases such as
permission evaluation or cache-miss model generation. Nested handler calls collapse
below the outer handler, and handler calls inside an action collapse below the action.
Every Celery execution starts a new task trace linked to the trace that published it,
then uses the task root followed by the same operation/phase levels.
Automatic database, Redis, and outbound HTTP spans remain children of their
nearest semantic span. DRF.initial separates authentication, standard permissions,
and throttling from the view body. Baserow signal dispatches produce one aggregate span
for all receivers and expose their names in baserow.signal.receivers, rather than
creating a span for every hook. Selected framework siblings show time outside the view
without tracing every middleware; for example, DRFResponse.render measures response
rendering.
Only HTTP server spans, Celery consumer spans, and internal metric observations may start traces. Parentless Redis commands, outbound calls, Silk persistence, handler methods, and other implementation spans are rejected at the SDK boundary. If these appear as isolated roots, at least one process is running older instrumentation or a different tracer provider.
For a specific request, add this backend query parameter:
?force_full_otel_trace=true
The SDK and Collector recognize the marker, and the Collector retains that complete trace outside the normal error, slow-request, and baseline budget.
This flag is unauthenticated and bypasses the bundled Collector’s normal trace budget. For public installations, restrict or rate-limit it at the reverse proxy, or add a Collector-side budget for forced traces. See Monitoring your Baserow server for the accepted-risk details.
Useful trace queries include:
baserow.http.request.slow=true for requests above the configured threshold.baserow.celery.task.slow=true for tasks in any queue above the configured
threshold.baserow.force_full_otel_trace=true for explicitly requested traces.Do not use retained trace counts as request, user, error, or action counts. The sampling policies deliberately retain different classes of traffic at different rates.
The current metrics intentionally do not preserve every possible trace grouping:
user.id x http.route
questions over all traffic.Add a purpose-built metric only after defining its allowed dimensions and cardinality budget. For billing, quotas, abuse controls, or contractual usage reporting, use a durable usage-accounting pipeline rather than best-effort OTLP metrics.
http.route values are templates and contain no concrete identifiers.otel.metric.overflow=true.?force_full_otel_trace=true produces a retained complete trace.For application and Collector setup, see Monitoring your Baserow server. For instrumentation conventions, see Working with metrics and logs as a developer.