Observability¶
TaskQ instruments itself with OpenTelemetry (vendor-neutral) and structured logging (structlog). No vendor SDK is bundled. You export data to any OTLP-compatible backend — Jaeger, Grafana Tempo, Honeycomb, Datadog, Sentry, Azure Monitor, PostHog — by pointing the standard OTel environment variables at your collector or agent.
Contents¶
- OpenTelemetry — setup
- Traces — span hierarchy
- Metrics reference
- Structured logging
- Log format examples
- Testing observability
- Disabling OTel
- External OTLP collector
- Error reporting (ErrorReporter Protocol)
1. OpenTelemetry — setup¶
Prerequisites¶
opentelemetry-api is a core dependency. The opentelemetry-sdk and
OTLP exporter require the [otel] extra:
Enabling and disabling¶
| Variable | Default | Effect |
|---|---|---|
TASKQ_OTEL_ENABLED |
true |
When false, all span and metric creation is suppressed; operations still succeed. |
Worker startup¶
worker_main calls set_otel_enabled(settings.otel_enabled) before the
TaskGroup opens. No application code needs to call this directly.
Exporter configuration¶
Configure the exporter with standard OTel environment variables. TaskQ does not override them.
| Variable | Example |
|---|---|
OTEL_EXPORTER_OTLP_ENDPOINT |
http://localhost:4317 |
OTEL_SERVICE_NAME |
my-app-worker |
OTEL_RESOURCE_ATTRIBUTES |
deployment.environment=production,k8s.pod.name=worker-0 |
Common receiver addresses:
- Datadog Agent:
http://localhost:4317(gRPC OTLP port) - Sentry Spotlight / OTel ingest: same
- Azure Monitor: OTLP URL derived from the App Insights connection string
- PostHog: PostHog Cloud OTLP endpoint
Instrumentation name¶
All spans and metrics are created under instrumentation name "taskq" (the
value of INSTRUMENTATION_NAME).
Semconv compliance¶
TaskQ uses OTel messaging semconv attribute names
(messaging.operation.type=publish, messaging.operation.type=process,
messaging.consumer.group.name, etc.) so that dashboards built against the
spec work without renaming.
2. Traces — span hierarchy¶
A complete job lifecycle produces four spans:
enqueue <actor> (PRODUCER)
└── process <actor> (CONSUMER, linked to PRODUCER via span link)
└── attempt.<N> (INTERNAL)
A fifth INTERNAL span wraps the batch dispatch SQL query:
Enqueue span¶
Name: enqueue <actor_name> (e.g. enqueue send_email)
Emitted by JobsClient.enqueue and SubJobEnqueuer.enqueue.
| Attribute | Value |
|---|---|
messaging.system |
"taskq" |
messaging.destination.name |
queue name |
messaging.operation.type |
"publish" |
messaging.message.id |
job UUID (set after the DB write) |
taskq.actor |
registered actor name |
taskq.identity_key |
identity key string, or "" |
The enqueue span's trace_id and span_id are stored in the jobs table row
(trace_id, span_id columns). At dispatch time the worker reads them and
attaches them to the consumer span as a span link
(links=[Link(...)]) — it does not parent the consumer span to them. The
consumer span therefore starts a new root trace, and a backend such as
Application Insights shows enqueue and execution under two unrelated
operation_Id values, joined only by the link. Parenting instead of linking is
a separate change: it needs the W3C traceparent sampling flags persisted on
the row, which the current two-column schema does not carry.
Dispatch span¶
Name: dispatch
Emitted by dispatch_batch each time the worker fetches a batch from
PostgreSQL.
| Attribute | Value |
|---|---|
taskq.queue |
first queue in the batch |
taskq.queues |
comma-separated list of queues |
taskq.batch_size |
requested batch limit |
Errors on the SQL call set StatusCode.ERROR and call span.record_exception.
Consumer span¶
Name: process <actor_name> (e.g. process send_email)
Emitted by dispatch_one_job. Carries a span link back to the enqueue
span so a trace viewer can correlate across the async boundary. If the stored
trace_id or span_id is malformed (non-hex), the link is silently skipped
and the consumer span is still created.
| Attribute | Value |
|---|---|
messaging.system |
"taskq" |
messaging.destination.name |
queue name |
messaging.operation.type |
"process" |
messaging.message.id |
job UUID |
messaging.consumer.group.name |
TASKQ_WORKER_GROUP setting |
taskq.actor |
registered actor name |
taskq.attempt |
attempt number (1-based) |
taskq.identity_key |
identity key string, or "" |
taskq.batch_id |
batch ID from job metadata, or "" |
The consumer span also carries lifecycle events added as span events:
| Event name | When added |
|---|---|
lifecycle.running |
job transitions to running |
lifecycle.succeeded |
actor returns without error |
lifecycle.cancelled |
job is cancelled |
lifecycle.scheduled |
job is snoozed or reservation unavailable |
lifecycle.failed |
actor raises an unrecoverable error |
Status is set to StatusCode.OK on success, StatusCode.ERROR on failure or
cancellation.
Attempt span¶
Name: attempt.<N> (e.g. attempt.1)
An INTERNAL child of the consumer span. Wraps the actual actor function call plus transaction commit for the transactional execution path.
Trace context propagation¶
When a job is enqueued inside an active OTel span, the span context
(trace_id as a 32-character hex string, span_id as a 16-character hex
string) is written to the jobs row. At dispatch time the worker reads those
columns, constructs a SpanContext with is_remote=True, and passes it as a
trace.Link to the consumer span. This allows end-to-end tracing across the
async enqueue-to-execute boundary without requiring the worker and the enqueueing
process to share a trace context in-band.
Accessing the span inside an actor¶
ctx.span is the live consumer span, or None when OTel is disabled:
from taskq import actor
from taskq.context import JobContext
@actor
async def my_actor(payload: Payload, ctx: JobContext[Payload]) -> Result:
span = ctx.span # opentelemetry.trace.Span | None
if span:
span.set_attribute("custom.key", "value")
...
3. Metrics reference¶
All instruments are created from the "taskq" meter. Instruments marked
unconditional are recorded even when TASKQ_OTEL_ENABLED=false because
they represent safety-critical signals.
Counters¶
| Metric name | Unit | Attributes | Description | Conditional? |
|---|---|---|---|---|
messaging.client.published.messages |
1 |
actor, queue |
Jobs successfully enqueued. | yes |
messaging.client.consumed.messages |
1 |
actor, queue, outcome |
Jobs consumed. outcome is one of succeeded, failed, cancelled, abandoned. A snoozed or rescheduled job maps to abandoned. |
yes |
taskq.cancellation.requested |
— | — | Incremented once per JobsClient.cancel() call regardless of outcome. |
unconditional |
taskq.cancellation.phase_transitions |
1 |
— | Cancel phase transitions (0→1, 1→2, etc.). | yes |
taskq.backpressure.errors |
— | actor, kind |
Enqueue rejections due to backpressure. kind is currently "max_pending". |
unconditional |
taskq.deadline_exceeded_sweep.jobs_failed |
1 |
actor |
Jobs failed by the deadline-exceeded sweep. | unconditional |
taskq.heartbeat.misses |
1 |
— | Heartbeat renewal failures. | yes |
taskq.leader.election_attempts |
1 |
— | Leader election attempts. | yes |
taskq.leader.election_failures |
1 |
— | Election attempts that did not win the lock. | yes |
taskq.error_reporter.failures |
1 |
reporter_type |
ErrorReporter invocation failures. |
yes |
taskq.progress.publish_failures |
1 |
— | Redis publish failures for progress fanout. | yes |
taskq.ratelimit.refund_failures |
1 |
bucket, backend |
Rate-limit refund/rollback failures. | yes |
taskq.pruned.jobs |
1 |
actor, status |
Jobs moved from jobs to jobs_archive by the prune sweep (Sweep 5). |
yes |
taskq.archived.jobs |
1 |
status |
Same prune-sweep event, status-only view (no actor dimension). | yes |
taskq.expired_archive.jobs |
1 |
status |
Jobs hard-deleted from jobs_archive by the archive expiry sweep (Sweep 6). |
yes |
taskq.maintenance_leader.sweep_rows |
— | sweep_name |
Rows affected per sweep tick. | yes |
Histograms¶
| Metric name | Unit | Attributes | Description |
|---|---|---|---|
messaging.process.duration |
s |
actor, queue |
End-to-end job execution duration from dispatch to terminal state. |
taskq.dispatch.duration |
s |
queue |
Batch dispatch SQL query latency (SQL execution only). |
taskq.lock.expires_in_seconds |
s |
— | Remaining lock TTL at each heartbeat renewal. Buckets: 0, 5, 10, 15, 20, 30, 45, 60 s. |
taskq.heartbeat.tick_duration_seconds |
s |
— | Wall-clock seconds per heartbeat tick. |
taskq.maintenance_leader.sweep_duration_ms |
ms |
— | Per-sweep-tick wall-clock duration. |
Observable gauges (polled)¶
| Metric name | Unit | Attributes | Description |
|---|---|---|---|
taskq.queue.depth |
1 |
queue |
Pending and scheduled jobs per queue. Sampled by the leader every 15 s. |
taskq.reservation.slots_used |
1 |
bucket |
In-use reservation slots per rate-limit bucket. Sampled by the leader every 15 s. |
taskq.maintenance_leader.is_leader |
1 |
worker_id |
1 on the elected leader pod, 0 on all others. |
taskq.cron.disabled_schedules |
1 |
— | Count of currently disabled cron schedules. |
taskq.heartbeat.consecutive_failures |
— | — | Consecutive heartbeat tick failures for this worker (sample-on-scrape). |
Up-down counters¶
| Metric name | Unit | Attributes | Description |
|---|---|---|---|
taskq.cron.consecutive_failures |
1 |
— | Consecutive cron execution failures, summed across schedules. On success the caller adds a negative delta equal to the current count for that schedule, so a non-zero total means at least one schedule is failing; the cron fired and cron schedule auto-disabled logs name which. |
Dimension cardinality¶
No metric carries worker_id, schedule_id, job_id or any other identity
value as a dimension, and new instruments must not add one.
Azure Monitor counts every unique combination of metric name, dimension key and
dimension value published in the last 12 hours as an active time series, caps
a subscription at 50,000 of them per region, allows 10 dimension keys per
metric, and documents fewer than 100 valid values per dimension as the
practical ceiling (up to 300 is a grey area; beyond that, use custom logs).
worker_id is a fresh UUID per worker process, so on Kubernetes every deploy,
restart and autoscale event mints new series. Exceeding the cap throttles
ingestion for every custom metric in the subscription, and throttled points
are not backfilled — the data is gone, so this is not repairable after the fact.
Per-worker and per-schedule attribution is on the channels where cardinality is free:
- Logs —
worker_idis bound via contextvars onto every log line;schedule_idis on thecron fired,cron schedule auto-disabledandcron-tick-lock-contendedlines. - Spans —
taskq.worker_idis an attribute of thecron firespan.
The record_* helpers still accept worker_id / schedule_id: they are part
of the published taskq.obs surface and the callers already hold the value.
They are simply not recorded as dimensions.
The one exception is taskq.maintenance_leader.is_leader, which is still
labeled by worker_id: the shipped Prometheus alert
(sum(taskq_maintenance_leader_is_leader) != 1) detects split-brain by summing
one series per pod, so the dimension is load-bearing there. Operators exporting
to Azure Monitor should drop it with an SDK View.
Metric recording and sampling independence¶
Counters and histograms in the dispatch and consume paths are recorded outside their corresponding span bodies. This ensures that a 100% sampled span does not inflate metric counts relative to a partially-sampled trace.
4. Structured logging¶
Setup¶
worker_main calls setup_logging(level=settings.log_level, log_format=settings.log_format).
setup_logging is idempotent — calling it a second time is a no-op.
Configuration¶
| Variable | Default | Values |
|---|---|---|
TASKQ_LOG_FORMAT |
json |
json (production), console (development) |
TASKQ_LOG_LEVEL |
INFO |
Any stdlib level name: DEBUG, INFO, WARNING, ERROR |
log_format rejects any value outside {"json", "console"} at
WorkerSettings load time.
Processor chain¶
The structlog processor chain applied to every log call (in order):
merge_contextvars— pulls inworker_id(and any other context vars bound by the worker)add_log_leveladd_logger_nameStackInfoRendererTimeStamper(fmt="iso", utc=True)— ISO 8601 UTC timestamp intimestampfield_otel_span_processor— injectstrace_idandspan_idfrom the active OTel span, if anyEventRenamer("event")— ensures the event key is alwayseventJSONRenderer(production) orConsoleRenderer(development)
Every processor is wrapped in a no-raise safety wrapper. A failing processor logs a warning and passes the event dict through unchanged; it never propagates to actor or user code.
Job-context fields¶
bind_job_context returns a new immutable BoundLogger pre-bound with:
| Field | Always present? |
|---|---|
job_id |
yes |
actor |
yes |
queue |
yes |
attempt |
yes |
trace_id |
yes (empty string when no active OTel span) |
worker_id |
via contextvars (bound once at worker startup) |
identity_key |
only when non-None |
span_id |
only when non-None |
batch_id |
only when non-None |
span_id, identity_key, and batch_id are omitted entirely when None
(they are not serialized as null).
Key log events¶
| Event | Level | Kind | Key fields | When |
|---|---|---|---|---|
state_change |
info | state_change |
from_state, to_state |
Any job status transition |
cancel_phase_change |
info | cancel_phase_change |
from_phase, to_phase |
Cancel phase escalation |
heartbeat-tick-success |
debug | — | worker_id, tick_duration_ms, jobs_extended, is_leader |
Each successful heartbeat tick |
heartbeat-tick-failure |
warning | — | worker_id, consecutive_failures, error_class, error |
Each failed heartbeat tick |
heartbeat-hook-failure |
warning | state_change |
worker_id, cause, error |
Cancel controller failure inside heartbeat transaction |
heartbeat-tick-unexpected-error |
error | — | worker_id |
Unexpected exception in heartbeat loop |
dispatch |
info | dispatch |
from_state, to_state, count, worker_id, queues, limit_n |
Each dispatch batch |
consume-rate-limit-denied-noop |
debug | — | from_state, to_state, cause |
Reservation denied but no state transition occurred |
prune |
info | prune |
status, count, cutoff_time, duration_ms |
Per-status batch result from the prune sweep (Sweep 5) |
archive_expiry |
info | archive_expiry |
status, count, expire_before, duration_ms |
Per-status batch result from the archive expiry sweep (Sweep 6) |
state_change events also carry cause, bucket_name, delay_seconds, and
similar context fields depending on the transition.
Payload redaction¶
obs.redact_payload(payload) returns the first 16 hex characters of the
SHA-256 digest of the JSON-serialized payload. Raw payload content is never
written to logs. Use this when you want to correlate log lines with a specific
payload without exposing its contents:
ctx.log.info(
"state_change",
kind="state_change",
from_state="pending",
to_state="running",
payload_hash=obs.redact_payload(payload),
)
Exception redaction¶
Exception text reaches both spans and JSON logs, and from there whatever
telemetry backend is configured. Before it does, TaskQ drops Postgres
DETAIL: lines and masks credentials in URI-shaped text.
Only DETAIL is dropped. It is the line that quotes caller-supplied row
values — Key (idempotency_key)=(tenant-4417) already exists. — and TaskQ's
idempotency_key, identity_key and fairness_key routinely hold tenant or
subject identifiers. HINT: (Postgres's suggested fix) and CONTEXT: (the
PL/pgSQL call stack) are structural, carry no row values, and are kept.
By default an operator sees:
duplicate key value violates unique constraint "jobs_idempotency_key_key"
HINT: Perhaps you meant to reference the column "jobs.queue".
CONTEXT: PL/pgSQL function taskq.enqueue(text) line 12 at SQL statement
| Variable | Default | Effect |
|---|---|---|
TASKQ_EXCEPTION_REDACTION_ENABLED |
true |
When false, DETAIL: lines are not dropped, on both the span and the log path. |
Setting it to false is a debugging aid, not a production setting. What you
gain is the offending row value, usually the fastest way to identify which
caller collided. What you expose is that value — a tenant or subject
identifier — written to spans, to logs, and to every configured telemetry
vendor, where it is retained under that vendor's policy rather than yours.
The worker logs an exception-redaction-disabled WARNING on every startup
while it is off, so an audit of startup logs will find it.
URI credential masking (postgresql://taskq:***@host) is not covered by
this toggle and is always applied. There is no debugging case for shipping a
password to a telemetry backend.
Logging inside an actor¶
ctx.log is a structlog.stdlib.BoundLogger already bound with job_id,
actor, queue, attempt, and trace_id. worker_id arrives via
contextvars. You do not need to add these fields manually.
from taskq import actor
from taskq.context import JobContext
@actor
async def my_actor(payload: Payload, ctx: JobContext[Payload]) -> Result:
ctx.log.info("processing-started", item_count=len(payload.items))
for item in payload.items:
ctx.log.debug("processing-item", item_id=item.id)
return Result(...)
5. Log format examples¶
JSON (TASKQ_LOG_FORMAT=json)¶
{
"event": "state_change",
"kind": "state_change",
"job_id": "018e1234-abcd-7000-8000-000000000001",
"actor": "send_email",
"queue": "default",
"attempt": 1,
"from_state": "running",
"to_state": "succeeded",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"span_id": "00f067aa0ba902b7",
"worker_id": "018e5678-ef01-7000-8000-000000000002",
"timestamp": "2025-01-15T10:23:45.123456Z",
"level": "info",
"logger": "taskq.worker._consumer"
}
Console (TASKQ_LOG_FORMAT=console)¶
2025-01-15T10:23:45.123456Z [info ] state_change actor=send_email attempt=1 from_state=running job_id=018e1234-... queue=default to_state=succeeded
The console renderer uses structlog.dev.ConsoleRenderer. Field ordering and
coloring depend on the structlog version.
6. Testing observability¶
Setting up a test tracer¶
taskq.testing.otel.setup_tracer creates an in-process TracerProvider
backed by ListSpanExporter and patches obs.get_tracer for the duration of
the test:
import pytest
from taskq.testing.otel import setup_tracer, ListSpanExporter
async def test_span_emitted(monkeypatch: pytest.MonkeyPatch) -> None:
provider, exporter = setup_tracer(monkeypatch)
# ... run enqueue or dispatch ...
consumer = exporter.span_named("process my_actor")
assert consumer is not None
assert consumer.kind == trace.SpanKind.CONSUMER
ListSpanExporter helpers:
| Method | Returns |
|---|---|
span_named(name) |
First ReadableSpan with that name, or None |
spans_named(name) |
All ReadableSpan objects with that name |
events_on(span_name, event_name) |
All events named event_name on the first span named span_name |
spans_with_kind(kind) |
All spans with the given SpanKind |
Setting up a test meter¶
taskq.testing.otel.setup_meter creates a per-test MeterProvider backed by
InMemoryMetricReader and patches the four core dispatch-path instruments:
from taskq.testing.otel import setup_meter, counter_value, histogram_points
async def test_metrics_recorded(monkeypatch: pytest.MonkeyPatch) -> None:
reader = setup_meter(monkeypatch)
# ... run enqueue and dispatch ...
assert counter_value(reader, "messaging.client.published.messages") >= 1
assert counter_value(reader, "messaging.client.consumed.messages") >= 1
assert len(histogram_points(reader, "messaging.process.duration")) >= 1
Metric query helpers:
| Helper | Returns |
|---|---|
collect_metrics(reader) |
All Metric objects from the reader |
counter_value(reader, name) |
Summed integer value for a counter |
counter_data_points(reader, name) |
list[NumberDataPoint] for a counter |
histogram_points(reader, name) |
list[HistogramDataPoint] for a histogram |
Autouse fixtures¶
taskq.testing.otel exports two autouse pytest fixtures that are imported
into conftest.py:
_otel_enabled_guard— snapshots and restores_otel_enabledaround each test_logging_configured_guard— resets structlog configuration and removesProcessorFormatterhandlers around each test
These fixtures run automatically for any test that imports from
taskq.testing.otel. See ../api-reference/testing.md for the full
fixture inventory.
Verifying trace context propagation¶
The integration tests in tests/test_otel_integration.py show the full
end-to-end pattern: enqueue, manually advance the job to running, call
dispatch_one_job, then assert that the consumer span carries a link whose
trace_id and span_id match the producer span:
producer = exporter.span_named("enqueue _integration_test_actor")
consumer = exporter.span_named("process _integration_test_actor")
assert len(consumer.links) == 1
assert consumer.links[0].context.trace_id == producer.get_span_context().trace_id
assert consumer.links[0].context.span_id == producer.get_span_context().span_id
7. Disabling OTel¶
Set TASKQ_OTEL_ENABLED=false to suppress all span and metric creation.
Structured logging is independent of this flag and remains active.
When OTel is disabled:
- safe_start_span yields a NonRecordingSpan (no-op).
- All record_* helpers return immediately without touching the meter.
- ctx.span is None.
- The unconditional counters (taskq.cancellation.requested,
taskq.backpressure.errors, taskq.deadline_exceeded_sweep.jobs_failed)
continue to record because they use the module-level instrument directly
without checking the flag.
8. External OTLP collector¶
Add an OTel collector alongside the TaskQ worker services. The collector receives spans and metrics on the standard OTLP gRPC port and forwards them to your backend.
# docker-compose.yml — collector service only
services:
otel-collector:
image: otel/opentelemetry-collector-contrib:latest
command: ["--config=/etc/otel-config.yaml"]
volumes:
- ./otel-config.yaml:/etc/otel-config.yaml:ro
ports:
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
Point the worker at the collector:
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317 \
OTEL_SERVICE_NAME=taskq-worker \
taskq worker --actors myapp.actors:registry
9. Error reporting (ErrorReporter Protocol)¶
OTel exception events and structured logs cover most error-observability
needs. For error handling that needs vendor-specific routing — DLQ to Sentry,
a custom alerting webhook, or an external audit log — TaskQ defines an
ErrorReporter Protocol that you implement and register as a DI provider.
The Protocol¶
ErrorReporter is a typing.Protocol with a single async method:
from typing import Protocol
from taskq.backend._protocol import JobRow
class ErrorReporter(Protocol):
async def report(self, job: JobRow, exception: BaseException) -> None: ...
The worker invokes report() after a job reaches a terminal failed state,
passing the final JobRow and the exception that caused the failure. The
argument order is (job, exception) — matching OnRetryExhausted (see
Retries — on_retry_exhausted hook).
The call is fire-and-forget with respect to the job lifecycle — a failing
reporter does not alter the job's terminal state.
The invocation is wrapped by invoke_error_reporter, which guards the call
with asyncio.wait_for using a timeout of 3 seconds (the error_reporter_timeout
default). TimeoutError and all other exceptions raised by report() are
caught, logged at WARNING, and counted on the taskq.error_reporter.failures
counter — they never propagate to the consumer loop.
NullErrorReporter (default)¶
When no ErrorReporter is registered, the worker uses NullErrorReporter,
whose report() is a no-op. This is the default out of the box — no error
reporting happens beyond OTel exception events and structured logs.
Registering a custom reporter¶
Implement the Protocol and register it as a DI provider. The worker resolves
it from the ProviderRegistry at dispatch time:
from taskq.di import ProviderRegistry, Scope
from taskq.obs.error_reporter import ErrorReporter
from taskq.backend._protocol import JobRow
class SentryErrorReporter:
"""Routes terminal failures to Sentry as breadcrumbs."""
async def report(self, job: JobRow, exception: BaseException) -> None:
# Send to Sentry, a webhook, an audit table, etc.
# job carries the full final row: id, actor, status, error_class,
# error_message, error_traceback, attempt, identity_key, ...
# exception is the BaseException that caused the terminal failure.
...
registry = ProviderRegistry()
# Register the Protocol type with a pre-built instance:
registry.register_value(ErrorReporter, Scope.PROCESS, SentryErrorReporter())
The reporter is resolved through the standard DI scope chain (see
Dependency Injection). Use Scope.PROCESS for
stateless reporters (most cases) or Scope.LOOP if the reporter holds a
connection that should be reused for the event-loop lifetime.
Keep report() fast and resilient
report() runs on the worker's consume path. The library's
invoke_error_reporter wrapper enforces a 3-second asyncio.wait_for
timeout (the error_reporter_timeout default) and catches all exceptions,
so a hanging or crashing reporter cannot block the terminal-state write
indefinitely or crash the worker. Even so, keep external calls short and
catch internal exceptions — a reporter that consistently times out delays
terminal writes by up to 3 seconds per failure and increments the
taskq.error_reporter.failures counter on each miss.
The taskq.error_reporter.failures metric¶
When a registered ErrorReporter.report() itself raises an exception, the
worker catches it, logs a warning, and increments the
taskq.error_reporter.failures counter:
| Metric name | Unit | Attributes | Description | Conditional? |
|---|---|---|---|---|
taskq.error_reporter.failures |
1 |
reporter_type |
ErrorReporter invocation failures. |
yes |
The reporter_type attribute is the class name of the reporter that raised
(e.g. "SentryErrorReporter", via type(reporter).__name__).
NullErrorReporter never raises, so this counter stays at zero unless a
custom reporter is installed and failing.
Related documentation¶
- actors.md —
@actordecorator,JobContext,ctx.log,ctx.span - workers.md — worker lifecycle,
WorkerSettings, pool configuration - ../api-reference/testing.md — test fixtures,
setup_tracer,setup_meter - cancellation.md — cancel phases,
cancel_phase_changelog events