Vendor-neutral observability bootstrap.
The library never imports vendor SDKs (Sentry, Datadog, PostHog, App Insights).
Instead, it emits OpenTelemetry spans/metrics/logs and lets operators wire any
OTLP-compatible backend by configuring environment variables (or by passing an
already-configured TracerProvider / MeterProvider).
Common deployment shapes:
- Datadog Agent — accepts OTLP on
localhost:4317. Set
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317.
- Sentry — Sentry Spotlight / Sentry OTel ingest, ditto.
- App Insights — set
OTEL_EXPORTER_OTLP_ENDPOINT to the Azure Monitor
connection string-derived OTLP URL (typically via the App Insights agent).
- PostHog — currently via PostHog Cloud OTLP endpoint, same env var.
For error reporting that doesn't fit OTel exception events (e.g., DLQ routing
to Sentry), users implement the ErrorReporter Protocol
as a DI provider — vendor-neutral and added with the observability surface.
The library depends only on opentelemetry-api at runtime. Operators
who want to configure providers programmatically (or use the in-process
testing utilities in taskq.testing) install the [otel] extra::
pip install "taskq-py[otel]"
which pulls in opentelemetry-sdk and opentelemetry-exporter-otlp.
For Prometheus scrapes, use the [prometheus] extra instead.
Semconv compliance: the library uses spec-compliant messaging semconv
attribute names (messaging.operation.type=publish,
messaging.operation.type=process, messaging.consumer.group.name, etc.)
so that operators who set OTEL_SEMCONV_STABILITY_OPT_IN=messaging get
consistent behavior. No runtime conditional branching on this env var is
needed — the attribute values are correct by construction.
INSTRUMENTATION_NAME
module-attribute
INSTRUMENTATION_NAME: str = 'taskq'
__all__
module-attribute
__all__ = [
"INSTRUMENTATION_NAME",
"ConsumedOutcome",
"ErrorReporter",
"ErrorReporterType",
"NullErrorReporter",
"bind_job_context",
"get_logger",
"get_meter",
"get_tracer",
"invoke_error_reporter",
"log_cancel_phase_change",
"log_state_change",
"record_archived_jobs",
"record_backpressure_error",
"record_cancel_requested",
"record_consumed_message",
"record_cron_failure",
"record_deadline_exceeded_swept",
"record_dispatch_duration",
"record_election_attempt",
"record_error_reporter_failure",
"record_expired_archive_jobs",
"record_heartbeat_miss",
"record_lock_expires_in_seconds",
"record_process_duration",
"record_progress_publish_failure",
"record_pruned_jobs",
"record_published_message",
"record_ratelimit_refund_failure",
"redact_payload",
"safe_start_span",
"set_otel_enabled",
"setup_logging",
"update_disabled_schedules_count",
"update_heartbeat_consecutive_failures",
"update_queue_depth_cache",
"update_reservation_slots_cache",
]
ConsumedOutcome
ConsumedOutcome = Literal[
"succeeded", "failed", "cancelled", "abandoned"
]
ErrorReporterType
ErrorReporterType = ErrorReporter
Structural type alias for DI registration and parameter annotations.
Use :class:ErrorReporter directly in most contexts; this alias is provided
for register_value(ErrorReporterType, ...) calls where a distinct type
object is needed for the registry key.
ErrorReporter
Bases: Protocol
Vendor-neutral hook for routing terminal job failures to external systems.
Implementations capture the error and job row, then forward to a
vendor-specific backend (Sentry, Datadog, a DLQ, etc.). The library
calls :meth:report when a job reaches a terminal failure state —
either because retries were exhausted or because the error was
non-retryable.
The call is wrapped in a try/except by :func:invoke_error_reporter;
a failing reporter never crashes the worker. Reporter failures are
counted on the taskq.error_reporter.failures counter with a
reporter_type attribute.
Register an :class:ErrorReporter instance as a DI provider
(registry.register_value(ErrorReporter, Scope.PROCESS, my_reporter))
or pass it directly to the worker bootstrap.
report
async
report(job: JobRow, exception: BaseException) -> None
Source code in src/taskq/obs/error_reporter.py
| async def report(self, job: JobRow, exception: BaseException) -> None: ...
|
NullErrorReporter
Default no-op :class:ErrorReporter — silently drops all reports.
Used when no vendor-specific error routing is configured. Instances
are stateless and safe to share.
report
async
report(job: JobRow, exception: BaseException) -> None
Source code in src/taskq/obs/error_reporter.py
| async def report(self, job: JobRow, exception: BaseException) -> None:
return None
|
get_meter
Return the library's meter. Honors any globally-configured provider.
Source code in src/taskq/obs/_otel.py
| def get_meter() -> Meter:
"""Return the library's meter. Honors any globally-configured provider."""
return metrics.get_meter(INSTRUMENTATION_NAME, _version())
|
get_tracer
Return the library's tracer. Honors any globally-configured provider.
Source code in src/taskq/obs/_otel.py
| def get_tracer() -> Tracer:
"""Return the library's tracer. Honors any globally-configured provider."""
return trace.get_tracer(INSTRUMENTATION_NAME, _version())
|
record_archived_jobs
record_archived_jobs(status: str, count: int = 1) -> None
Bump the archived.jobs counter.
Called alongside record_pruned_jobs at the prune sweep call site in leader.py.
Respects _otel_enabled — no-op when False.
Source code in src/taskq/obs/_otel.py
| def record_archived_jobs(status: str, count: int = 1) -> None:
"""Bump the archived.jobs counter.
Called alongside record_pruned_jobs at the prune sweep call site in leader.py.
Respects ``_otel_enabled`` — no-op when False.
"""
if not _otel_enabled:
return
_archived_jobs.add(count, {"status": status})
|
record_backpressure_error
record_backpressure_error(
actor: str, *, kind: str = "max_pending"
) -> None
Bump the backpressure.errors counter.
Unconditional (not gated by _otel_enabled): backpressure errors are
safety-critical signals that must be counted even when OTel is disabled,
so operators always have visibility into enqueue rejections.
Source code in src/taskq/obs/_otel.py
| def record_backpressure_error(actor: str, *, kind: str = "max_pending") -> None:
"""Bump the backpressure.errors counter.
Unconditional (not gated by ``_otel_enabled``): backpressure errors are
safety-critical signals that must be counted even when OTel is disabled,
so operators always have visibility into enqueue rejections.
"""
try:
_backpressure_errors.add(1, {"actor": actor, "kind": kind})
except Exception:
_log.warning("otel-metric-record-failed", instrument_name="taskq.backpressure.errors")
|
record_cancel_requested
record_cancel_requested() -> None
Bump the cancel-requested counter.
This counter is unconditional:
incremented once per JobsClient.cancel() call regardless of
cancellation_initiated outcome.
Source code in src/taskq/obs/_otel.py
| def record_cancel_requested() -> None:
"""Bump the cancel-requested counter.
This counter is unconditional:
incremented once per ``JobsClient.cancel()`` call regardless of
``cancellation_initiated`` outcome.
"""
try:
_cancellation_requested.add(1)
except Exception:
_log.warning("otel-metric-record-failed", instrument_name="taskq.cancellation.requested")
|
record_consumed_message
record_consumed_message(
actor: str, queue: str, *, outcome: ConsumedOutcome
) -> None
Bump the consumed-messages counter.
Called after job completion, outside the CONSUMER span body,
to ensure sampling independence.
Respects _otel_enabled — no-op when False.
outcome is constrained to the semconv-specified valid set
{succeeded, failed, cancelled, abandoned}.
The consumer-path AttemptOutcome includes "scheduled" for
snooze/retry/reservation-denial; callers must map that to
"abandoned" before calling (the consumer released the job back
to the queue without completing it).
Source code in src/taskq/obs/_otel.py
| def record_consumed_message(actor: str, queue: str, *, outcome: ConsumedOutcome) -> None:
"""Bump the consumed-messages counter.
Called after job completion, outside the CONSUMER span body,
to ensure sampling independence.
Respects ``_otel_enabled`` — no-op when False.
``outcome`` is constrained to the semconv-specified valid set
``{succeeded, failed, cancelled, abandoned}``.
The consumer-path ``AttemptOutcome`` includes ``"scheduled"`` for
snooze/retry/reservation-denial; callers must map that to
``"abandoned"`` before calling (the consumer released the job back
to the queue without completing it).
"""
if not _otel_enabled:
return
_consumed_messages.add(1, {"actor": actor, "queue": queue, "outcome": outcome})
|
record_cron_failure
record_cron_failure(schedule_id: str, delta: int) -> None
Record a cron failure delta on the UpDownCounter.
On failure, callers add +1 per failure. On success, callers add
-current_count for that schedule to reset the counter to zero —
a simple add(-1) would leave a non-zero cumulative value if
there were multiple consecutive failures.
Respects _otel_enabled — no-op when False.
Source code in src/taskq/obs/_otel.py
| def record_cron_failure(schedule_id: str, delta: int) -> None:
"""Record a cron failure delta on the UpDownCounter.
On failure, callers add ``+1`` per failure. On success, callers add
``-current_count`` for that schedule to reset the counter to zero —
a simple ``add(-1)`` would leave a non-zero cumulative value if
there were multiple consecutive failures.
Respects ``_otel_enabled`` — no-op when False.
"""
if not _otel_enabled:
return
_cron_consecutive_failures.add(delta, {"schedule_id": schedule_id})
|
record_deadline_exceeded_swept
record_deadline_exceeded_swept(
actor: str, count: int = 1
) -> None
Bump the deadline-exceeded sweep counter.
Unconditional (not gated by _otel_enabled): deadline-exceeded sweeps
indicate jobs that violated their execution budget — a correctness signal
that must be counted even when OTel is disabled, so operators always have
visibility into sweep activity.
Source code in src/taskq/obs/_otel.py
| def record_deadline_exceeded_swept(actor: str, count: int = 1) -> None:
"""Bump the deadline-exceeded sweep counter.
Unconditional (not gated by ``_otel_enabled``): deadline-exceeded sweeps
indicate jobs that violated their execution budget — a correctness signal
that must be counted even when OTel is disabled, so operators always have
visibility into sweep activity.
"""
try:
_deadline_exceeded_sweep_jobs_failed.add(count, {"actor": actor})
except Exception:
_log.warning(
"otel-metric-record-failed", instrument_name="taskq.deadline_exceeded_sweep.jobs_failed"
)
|
record_dispatch_duration
record_dispatch_duration(
queue: str, elapsed: float
) -> None
Record dispatch query latency on the histogram.
Called outside the dispatch span body for sampling independence.
Respects _otel_enabled — no-op when False.
Source code in src/taskq/obs/_otel.py
| def record_dispatch_duration(queue: str, elapsed: float) -> None:
"""Record dispatch query latency on the histogram.
Called outside the ``dispatch`` span body for sampling independence.
Respects ``_otel_enabled`` — no-op when False.
"""
if not _otel_enabled:
return
_dispatch_duration.record(elapsed, {"queue": queue})
|
record_election_attempt
record_election_attempt(
worker_id: str, *, won: bool
) -> None
Record a leader election attempt.
Always increments election_attempts; increments election_failures
only when the attempt did not win the lock.
Respects _otel_enabled — no-op when False.
Source code in src/taskq/obs/_otel.py
| def record_election_attempt(worker_id: str, *, won: bool) -> None:
"""Record a leader election attempt.
Always increments ``election_attempts``; increments ``election_failures``
only when the attempt did not win the lock.
Respects ``_otel_enabled`` — no-op when False.
"""
if not _otel_enabled:
return
_leader_election_attempts.add(1, {"worker_id": worker_id})
if not won:
_leader_election_failures.add(1, {"worker_id": worker_id})
|
record_error_reporter_failure
record_error_reporter_failure(reporter_type: str) -> None
Bump the error_reporter.failures counter.
Called at the error-reporter catch site when report() raises.
reporter_type is the exception-safe class name of the reporter
instance (bounded cardinality — one per registered implementation).
Respects _otel_enabled — no-op when False.
Source code in src/taskq/obs/_otel.py
| def record_error_reporter_failure(reporter_type: str) -> None:
"""Bump the error_reporter.failures counter.
Called at the error-reporter catch site when ``report()`` raises.
``reporter_type`` is the exception-safe class name of the reporter
instance (bounded cardinality — one per registered implementation).
Respects ``_otel_enabled`` — no-op when False.
"""
if not _otel_enabled:
return
_error_reporter_failures.add(1, {"reporter_type": reporter_type})
|
record_expired_archive_jobs
record_expired_archive_jobs(
status: str, count: int = 1
) -> None
Bump the expired_archive.jobs counter.
Called at the archive expiry sweep call site in leader.py.
Respects _otel_enabled — no-op when False.
Source code in src/taskq/obs/_otel.py
| def record_expired_archive_jobs(status: str, count: int = 1) -> None:
"""Bump the expired_archive.jobs counter.
Called at the archive expiry sweep call site in leader.py.
Respects ``_otel_enabled`` — no-op when False.
"""
if not _otel_enabled:
return
_expired_archive_jobs.add(count, {"status": status})
|
record_heartbeat_miss
record_heartbeat_miss(worker_id: str) -> None
Bump the heartbeat.misses counter.
Called in heartbeat.py on each heartbeat renewal failure.
Respects _otel_enabled — no-op when False.
Source code in src/taskq/obs/_otel.py
| def record_heartbeat_miss(worker_id: str) -> None:
"""Bump the heartbeat.misses counter.
Called in heartbeat.py on each heartbeat renewal failure.
Respects ``_otel_enabled`` — no-op when False.
"""
if not _otel_enabled:
return
_heartbeat_misses.add(1, {"worker_id": worker_id})
|
record_lock_expires_in_seconds
record_lock_expires_in_seconds(
worker_id: str, remaining_ttl: float
) -> None
Record remaining lock TTL on the histogram.
Called in heartbeat.py at each successful heartbeat renewal.
Respects _otel_enabled — no-op when False.
Source code in src/taskq/obs/_otel.py
| def record_lock_expires_in_seconds(worker_id: str, remaining_ttl: float) -> None:
"""Record remaining lock TTL on the histogram.
Called in heartbeat.py at each successful heartbeat renewal.
Respects ``_otel_enabled`` — no-op when False.
"""
if not _otel_enabled:
return
_lock_expires_in_seconds.record(remaining_ttl, {"worker_id": worker_id})
|
record_process_duration
record_process_duration(
actor: str, queue: str, elapsed: float
) -> None
Record job execution duration on the histogram.
Called outside the CONSUMER span body for sampling independence.
Respects _otel_enabled — no-op when False.
Custom buckets are the operator's responsibility via SDK Views.
Source code in src/taskq/obs/_otel.py
| def record_process_duration(actor: str, queue: str, elapsed: float) -> None:
"""Record job execution duration on the histogram.
Called outside the CONSUMER span body for sampling independence.
Respects ``_otel_enabled`` — no-op when False.
Custom buckets are the operator's responsibility via SDK Views.
"""
if not _otel_enabled:
return
_process_duration.record(elapsed, {"actor": actor, "queue": queue})
|
record_progress_publish_failure
record_progress_publish_failure(
channel: str, error_type: str
) -> None
Bump the progress.publish_failures counter.
channel must be 'per_job' or 'global' — bounded cardinality.
error_type is the exception class name (e.g. 'ResponseError').
Respects _otel_enabled — no-op when False.
Source code in src/taskq/obs/_otel.py
| def record_progress_publish_failure(channel: str, error_type: str) -> None:
"""Bump the progress.publish_failures counter.
``channel`` must be ``'per_job'`` or ``'global'`` — bounded cardinality.
``error_type`` is the exception class name (e.g. ``'ResponseError'``).
Respects ``_otel_enabled`` — no-op when False.
"""
if not _otel_enabled:
return
_progress_publish_failures.add(1, {"channel": channel, "error_type": error_type})
|
record_pruned_jobs
record_pruned_jobs(
actor: str, status: str, count: int = 1
) -> None
Bump the pruned.jobs counter.
Called at the prune sweep call site in leader.py.
Respects _otel_enabled — no-op when False.
Source code in src/taskq/obs/_otel.py
| def record_pruned_jobs(actor: str, status: str, count: int = 1) -> None:
"""Bump the pruned.jobs counter.
Called at the prune sweep call site in leader.py.
Respects ``_otel_enabled`` — no-op when False.
"""
if not _otel_enabled:
return
_pruned_jobs.add(count, {"actor": actor, "status": status})
|
record_published_message
record_published_message(actor: str, queue: str) -> None
Bump the published-messages counter.
Called after successful enqueue, outside the PRODUCER span body,
to ensure sampling independence.
Respects _otel_enabled — no-op when False.
Source code in src/taskq/obs/_otel.py
| def record_published_message(actor: str, queue: str) -> None:
"""Bump the published-messages counter.
Called after successful enqueue, outside the PRODUCER span body,
to ensure sampling independence.
Respects ``_otel_enabled`` — no-op when False.
"""
if not _otel_enabled:
return
_published_messages.add(1, {"actor": actor, "queue": queue})
|
record_ratelimit_refund_failure
record_ratelimit_refund_failure(
bucket: str, backend: str
) -> None
Bump the ratelimit.refund_failures counter.
Called at the rate-limit refund failure catch site.
Respects _otel_enabled — no-op when False.
Source code in src/taskq/obs/_otel.py
| def record_ratelimit_refund_failure(bucket: str, backend: str) -> None:
"""Bump the ratelimit.refund_failures counter.
Called at the rate-limit refund failure catch site.
Respects ``_otel_enabled`` — no-op when False.
"""
if not _otel_enabled:
return
_ratelimit_refund_failures.add(1, {"bucket": bucket, "backend": backend})
|
safe_start_span
safe_start_span(
name: str,
*,
kind: SpanKind | None = None,
attributes: Attributes = None,
links: Sequence[Link] | None = None,
new_root: bool = False,
) -> Generator[Span, None, None]
Start a span safely — never propagates exceptions from OTel API calls.
Checks _otel_enabled first; when False, yields a no-op
NonRecordingSpan. When True, delegates to
get_tracer().start_as_current_span with a try/except around
span creation only. Exceptions from code inside the with block
propagate normally — only OTel API failures (misconfiguration,
exporter unavailability) are suppressed.
When new_root=True, passes an empty Context() so the span
has no parent — it is a root span linked (not parented) to the
ambient trace. This satisfies the "linked, not parented"
requirement for PRODUCER spans in the cron loop.
Source code in src/taskq/obs/_otel.py
| @contextlib.contextmanager
def safe_start_span(
name: str,
*,
kind: trace.SpanKind | None = None,
attributes: Attributes = None,
links: Sequence[trace.Link] | None = None,
new_root: bool = False,
) -> Generator[Span, None, None]:
"""Start a span safely — never propagates exceptions from OTel API calls.
Checks ``_otel_enabled`` first; when ``False``, yields a no-op
``NonRecordingSpan``. When ``True``, delegates to
``get_tracer().start_as_current_span`` with a ``try/except`` around
span *creation* only. Exceptions from code inside the ``with`` block
propagate normally — only OTel API failures (misconfiguration,
exporter unavailability) are suppressed.
When ``new_root=True``, passes an empty ``Context()`` so the span
has no parent — it is a root span linked (not parented) to the
ambient trace. This satisfies the "linked, not parented"
requirement for PRODUCER spans in the cron loop.
"""
if not _otel_enabled:
yield trace.NonRecordingSpan(_NOOP_SPAN_CONTEXT)
return
ctx: Context | None = Context() if new_root else None
try:
span_cm = get_tracer().start_as_current_span(
name,
context=ctx,
kind=kind if kind is not None else trace.SpanKind.INTERNAL,
attributes=attributes,
links=links,
)
except Exception:
_log.warning("otel-span-creation-failed", span_name=name)
yield trace.NonRecordingSpan(_NOOP_SPAN_CONTEXT)
return
with span_cm as span:
yield span
|
set_otel_enabled
set_otel_enabled(enabled: bool) -> None
Set the module-level OTel enabled flag.
Called by worker startup code after loading WorkerSettings so that
all safe helpers check the flag without requiring a WorkerSettings
import at every call site. Avoids circular imports (modules like
dispatch.py import from obs and should not import from
settings.py in a circular path).
Source code in src/taskq/obs/_otel.py
| def set_otel_enabled(enabled: bool) -> None:
"""Set the module-level OTel enabled flag.
Called by worker startup code after loading ``WorkerSettings`` so that
all safe helpers check the flag without requiring a ``WorkerSettings``
import at every call site. Avoids circular imports (modules like
``dispatch.py`` import from ``obs`` and should not import from
``settings.py`` in a circular path).
"""
global _otel_enabled
_otel_enabled = enabled
|
update_disabled_schedules_count
update_disabled_schedules_count(count: int) -> None
Update the module-level disabled-schedules count.
Called by the leader's schedule management code when schedules are
disabled or re-enabled.
Source code in src/taskq/obs/_otel.py
| def update_disabled_schedules_count(count: int) -> None:
"""Update the module-level disabled-schedules count.
Called by the leader's schedule management code when schedules are
disabled or re-enabled.
"""
global _disabled_schedules_count
_disabled_schedules_count = count
|
update_heartbeat_consecutive_failures
update_heartbeat_consecutive_failures(
worker_id: str, count: int
) -> None
Update the module-level heartbeat consecutive-failures cache.
Called by the heartbeat loop after each tick so the synchronous
gauge callback can read the latest value on scrape.
Source code in src/taskq/obs/_otel.py
| def update_heartbeat_consecutive_failures(worker_id: str, count: int) -> None:
"""Update the module-level heartbeat consecutive-failures cache.
Called by the heartbeat loop after each tick so the synchronous
gauge callback can read the latest value on scrape.
"""
global _heartbeat_consecutive_failures_cache
_heartbeat_consecutive_failures_cache[worker_id] = count
|
update_queue_depth_cache
update_queue_depth_cache(data: dict[str, int]) -> None
Replace the queue-depth cache with fresh data from the leader's PG query.
Called by the background async task in the leader loop every 15s.
The synchronous gauge callback reads from this cache.
Source code in src/taskq/obs/_otel.py
| def update_queue_depth_cache(data: dict[str, int]) -> None:
"""Replace the queue-depth cache with fresh data from the leader's PG query.
Called by the background async task in the leader loop every 15s.
The synchronous gauge callback reads from this cache.
"""
global _queue_depth_cache
_queue_depth_cache = dict(data)
|
update_reservation_slots_cache
update_reservation_slots_cache(
data: dict[str, int],
) -> None
Replace the reservation-slots cache with fresh data from the leader's PG query.
Called by the background async task in the leader loop every 15s.
The synchronous gauge callback reads from this cache.
Source code in src/taskq/obs/_otel.py
| def update_reservation_slots_cache(data: dict[str, int]) -> None:
"""Replace the reservation-slots cache with fresh data from the leader's PG query.
Called by the background async task in the leader loop every 15s.
The synchronous gauge callback reads from this cache.
"""
global _reservation_slots_cache
_reservation_slots_cache = dict(data)
|
bind_job_context
bind_job_context(
log: BoundLogger,
*,
job_id: UUID,
actor: str,
queue: str,
attempt: int,
identity_key: str | None,
trace_id: str,
span_id: str | None = None,
batch_id: str | None = None,
) -> structlog.stdlib.BoundLogger
Bind job-scope fields to a logger, returning a new immutable BoundLogger.
identity_key, span_id, and batch_id are omitted from the bound
dict when None — not set to null or empty string . trace_id
is always bound (defaults to "" when no active OTel span per spec).
Returns a new BoundLogger; does not mutate the input.
Source code in src/taskq/obs/_structlog.py
| def bind_job_context(
log: structlog.stdlib.BoundLogger,
*,
job_id: UUID,
actor: str,
queue: str,
attempt: int,
identity_key: str | None,
trace_id: str,
span_id: str | None = None,
batch_id: str | None = None,
) -> structlog.stdlib.BoundLogger:
"""Bind job-scope fields to a logger, returning a new immutable BoundLogger.
``identity_key``, ``span_id``, and ``batch_id`` are omitted from the bound
dict when ``None`` — not set to null or empty string . ``trace_id``
is always bound (defaults to ``""`` when no active OTel span per spec).
Returns a new ``BoundLogger``; does not mutate the input.
"""
fields: dict[str, UUID | str | int] = {
"job_id": job_id,
"actor": actor,
"queue": queue,
"attempt": attempt,
"trace_id": trace_id,
}
if identity_key is not None:
fields["identity_key"] = identity_key
if span_id is not None:
fields["span_id"] = span_id
if batch_id is not None:
fields["batch_id"] = batch_id
return log.bind(**fields)
|
get_logger
get_logger(name: str) -> structlog.stdlib.BoundLogger
Return a structlog.stdlib.BoundLogger for the given dotted name.
Replaces direct structlog.get_logger() calls in library code so that
pyright strict mode gets an explicit return type (structlog.get_logger
returns Any).
Source code in src/taskq/obs/_structlog.py
| def get_logger(name: str) -> structlog.stdlib.BoundLogger:
"""Return a ``structlog.stdlib.BoundLogger`` for the given dotted name.
Replaces direct ``structlog.get_logger()`` calls in library code so that
pyright strict mode gets an explicit return type (``structlog.get_logger``
returns ``Any``).
"""
return structlog.get_logger(name)
|
log_cancel_phase_change
log_cancel_phase_change(
log: BoundLogger,
*,
from_phase: int,
to_phase: int,
**extra: object,
) -> None
Emit an INFO log line with kind="cancel_phase_change".
from_phase and to_phase are the cancel-phase integers before
and after the escalation. cancel_observed_at is NOT included — it
is per-handler context, not part of the canonical schema.
Source code in src/taskq/obs/_structlog.py
| def log_cancel_phase_change(
log: structlog.stdlib.BoundLogger,
*,
from_phase: int,
to_phase: int,
**extra: object,
) -> None:
"""Emit an INFO log line with ``kind="cancel_phase_change"``.
``from_phase`` and ``to_phase`` are the cancel-phase integers before
and after the escalation. ``cancel_observed_at`` is NOT included — it
is per-handler context, not part of the canonical schema.
"""
log.info(
"cancel_phase_change",
kind="cancel_phase_change",
from_phase=from_phase,
to_phase=to_phase,
**extra,
)
|
log_state_change
log_state_change(
log: BoundLogger,
*,
from_state: str,
to_state: str,
**extra: object,
) -> None
Emit an INFO log line with kind="state_change".
from_state and to_state are the job-status values before and
after the transition. All bound fields from the pre-bound log
(which carries job context from :func:bind_job_context) are included
automatically. The event name is "state-change" so the log is
queryable by both event and kind.
Source code in src/taskq/obs/_structlog.py
| def log_state_change(
log: structlog.stdlib.BoundLogger,
*,
from_state: str,
to_state: str,
**extra: object,
) -> None:
"""Emit an INFO log line with ``kind="state_change"``.
``from_state`` and ``to_state`` are the job-status values before and
after the transition. All bound fields from the pre-bound ``log``
(which carries job context from :func:`bind_job_context`) are included
automatically. The event name is ``"state-change"`` so the log is
queryable by both event and kind.
"""
log.info("state-change", kind="state_change", from_state=from_state, to_state=to_state, **extra)
|
redact_payload
redact_payload(payload: object) -> str
Return the first 16 characters of the SHA-256 hex digest of the JSON-serialized payload.
Raw payload content does not appear in the return value. Deterministic
for the same input.
Source code in src/taskq/obs/_structlog.py
| def redact_payload(payload: object) -> str:
"""Return the first 16 characters of the SHA-256 hex digest of the JSON-serialized payload.
Raw payload content does not appear in the return value. Deterministic
for the same input.
"""
serialized = dumps_str(payload).encode()
return hashlib.sha256(serialized).hexdigest()[:16]
|
setup_logging
setup_logging(
*, level: str = "INFO", log_format: str = "json"
) -> None
Configure structlog with the canonical processor chain.
Production (log_format="json"): JSONRenderer via
ProcessorFormatter stdlib bridge. Development (log_format="console"):
ConsoleRenderer via ProcessorFormatter. Idempotent — guarded
by _logging_configured flag. Not called at import time .
Source code in src/taskq/obs/_structlog.py
| def setup_logging(
*,
level: str = "INFO",
log_format: str = "json",
) -> None:
"""Configure structlog with the canonical processor chain.
Production (``log_format="json"``): ``JSONRenderer`` via
``ProcessorFormatter`` stdlib bridge. Development (``log_format="console"``):
``ConsoleRenderer`` via ``ProcessorFormatter``. Idempotent — guarded
by ``_logging_configured`` flag. Not called at import time .
"""
global _logging_configured
if _logging_configured:
return
shared_processors: list[structlog.types.Processor] = [
_safe_processor_wrapper(structlog.contextvars.merge_contextvars),
_safe_processor_wrapper(structlog.stdlib.add_log_level),
_safe_processor_wrapper(structlog.stdlib.add_logger_name),
_safe_processor_wrapper(structlog.processors.StackInfoRenderer()),
_safe_processor_wrapper(structlog.processors.TimeStamper(fmt="iso", utc=True)),
_safe_processor_wrapper(_otel_span_processor),
_safe_processor_wrapper(structlog.processors.EventRenamer("event")),
]
if log_format == "console":
renderer: structlog.types.Processor = structlog.dev.ConsoleRenderer()
else:
from taskq._json import structlog_serializer
renderer = structlog.processors.JSONRenderer(serializer=structlog_serializer)
structlog.configure(
processors=[
*shared_processors,
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
],
wrapper_class=structlog.stdlib.BoundLogger,
logger_factory=structlog.stdlib.LoggerFactory(),
cache_logger_on_first_use=True,
)
formatter = structlog.stdlib.ProcessorFormatter(
processors=[
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
renderer,
],
foreign_pre_chain=[
_safe_processor_wrapper(structlog.processors.TimeStamper(fmt="iso", utc=True)),
_safe_processor_wrapper(structlog.stdlib.add_log_level),
_safe_processor_wrapper(structlog.stdlib.ExtraAdder()),
],
)
handler = logging.StreamHandler()
handler.setFormatter(formatter)
if not any(
isinstance(h, logging.StreamHandler)
and isinstance(h.formatter, structlog.stdlib.ProcessorFormatter)
for h in logging.root.handlers
):
logging.root.addHandler(handler)
logging.root.setLevel(level)
_logging_configured = True
|
invoke_error_reporter
async
invoke_error_reporter(
reporter: ErrorReporter | None,
job: JobRow,
exception: BaseException,
timeout: float = 3.0,
*,
log: BoundLogger | None = None,
) -> None
Invoke reporter.report(), swallowing and counting failures.
A None reporter is treated as a no-op (equivalent to
:class:NullErrorReporter). Exceptions from report() are caught,
logged at WARNING, and counted on the taskq.error_reporter.failures
counter — they never propagate to the caller.
This mirrors the defensive pattern of
:func:~taskq.retry.invoke_on_retry_exhausted: a user-supplied hook
must never crash the worker's terminal-write path.
Source code in src/taskq/obs/error_reporter.py
| async def invoke_error_reporter(
reporter: ErrorReporter | None,
job: JobRow,
exception: BaseException,
timeout: float = 3.0, # noqa: ASYNC109 Why: parameter name matches the on_retry_exhausted/invoke_on_success convention; asyncio.wait_for requires a timeout value, not asyncio.timeout() context manager
*,
log: structlog.stdlib.BoundLogger | None = None,
) -> None:
"""Invoke *reporter*.report(), swallowing and counting failures.
A ``None`` reporter is treated as a no-op (equivalent to
:class:`NullErrorReporter`). Exceptions from ``report()`` are caught,
logged at WARNING, and counted on the ``taskq.error_reporter.failures``
counter — they never propagate to the caller.
This mirrors the defensive pattern of
:func:`~taskq.retry.invoke_on_retry_exhausted`: a user-supplied hook
must never crash the worker's terminal-write path.
"""
if reporter is None:
return
logger: structlog.stdlib.BoundLogger = log if log is not None else _log
reporter_type = type(reporter).__name__
try:
await asyncio.wait_for(reporter.report(job, exception), timeout=timeout)
except TimeoutError:
logger.warning(
"error-reporter-timeout",
job_id=job.id,
actor=job.actor,
reporter_type=reporter_type,
timeout_seconds=timeout,
)
except Exception as exc:
logger.warning(
"error-reporter-failed",
job_id=job.id,
actor=job.actor,
reporter_type=reporter_type,
error=repr(exc),
)
record_error_reporter_failure(reporter_type)
|