django-lambda-tasks 0.11.1__tar.gz → 0.11.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/steering/product.md +14 -13
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/steering/structure.md +3 -3
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/steering/tech.md +1 -1
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/PKG-INFO +25 -16
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/README.md +24 -15
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/example/README.md +13 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/example/example_project/sentry.py +13 -6
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/lambda_tasks/handler.py +39 -15
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/lambda_tasks/models.py +3 -3
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/lambda_tasks/sentry.py +31 -16
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/pyproject.toml +1 -1
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/tests/test_handler.py +140 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/tests/test_sentry.py +79 -22
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.github/dependabot.yml +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.github/workflows/ci.yml +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.github/workflows/release.yml +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.gitignore +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/specs/async-local-execution/.config.kiro +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/specs/async-local-execution/design.md +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/specs/async-local-execution/requirements.md +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/specs/async-local-execution/tasks.md +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/specs/deferred-task-enqueue/.config.kiro +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/specs/deferred-task-enqueue/design.md +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/specs/deferred-task-enqueue/requirements.md +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/specs/deferred-task-enqueue/tasks.md +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/specs/eager-mode-example-app/.config.kiro +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/specs/eager-mode-example-app/design.md +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/specs/eager-mode-example-app/requirements.md +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/specs/eager-mode-example-app/tasks.md +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/specs/ignore-errors-decorator-option/.config.kiro +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/specs/ignore-errors-decorator-option/design.md +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/specs/ignore-errors-decorator-option/requirements.md +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/specs/ignore-errors-decorator-option/tasks.md +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/specs/import-string-task-resolution/.config.kiro +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/specs/import-string-task-resolution/design.md +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/specs/import-string-task-resolution/requirements.md +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/specs/import-string-task-resolution/tasks.md +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/specs/retry-delay/.config.kiro +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/specs/retry-delay/design.md +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/specs/retry-delay/requirements.md +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/specs/retry-delay/tasks.md +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/specs/rse-background-tasks/.config.kiro +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/specs/rse-background-tasks/design.md +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/specs/rse-background-tasks/requirements.md +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/specs/rse-background-tasks/tasks.md +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/specs/rse-background-tasks-bugfix/.config.kiro +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/specs/rse-background-tasks-bugfix/bugfix.md +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/specs/rse-background-tasks-bugfix/design.md +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/specs/rse-background-tasks-bugfix/tasks.md +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/specs/singleton-task/.config.kiro +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/specs/singleton-task/design.md +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/specs/singleton-task/requirements.md +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/specs/singleton-task/tasks.md +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/specs/ssm-environment-loader/.config.kiro +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/specs/ssm-environment-loader/design.md +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/specs/ssm-environment-loader/requirements.md +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/specs/ssm-environment-loader/tasks.md +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/specs/task-retry/.config.kiro +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/specs/task-retry/design.md +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/specs/task-retry/requirements.md +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.kiro/specs/task-retry/tasks.md +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.pre-commit-config.yaml +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/.vscode/settings.json +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/docker-compose.yml +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/example/docker-compose.yml +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/example/example_app/__init__.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/example/example_app/apps.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/example/example_app/tasks.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/example/example_app/urls.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/example/example_app/views.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/example/example_project/__init__.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/example/example_project/settings.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/example/example_project/urls.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/example/example_project/wsgi.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/example/manage.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/lambda_tasks/__init__.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/lambda_tasks/admin.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/lambda_tasks/apps.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/lambda_tasks/checks.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/lambda_tasks/decorators.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/lambda_tasks/environment_loader.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/lambda_tasks/local_executor.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/lambda_tasks/logging.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/lambda_tasks/migrations/0001_initial.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/lambda_tasks/migrations/0002_alter_taskrecord_status.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/lambda_tasks/migrations/0003_taskrecord_taskrecord_kwargs_trgm.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/lambda_tasks/migrations/0004_remove_taskrecord_taskrecord_kwargs_trgm_and_more.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/lambda_tasks/migrations/__init__.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/lambda_tasks/secret_loader.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/lambda_tasks/settings.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/lambda_tasks/tasks.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/lambda_tasks/timeouts.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/tests/conftest.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/tests/settings.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/tests/test_admin.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/tests/test_batch.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/tests/test_batch_worker.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/tests/test_checks.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/tests/test_decorator.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/tests/test_decorators.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/tests/test_deferred_enqueue.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/tests/test_environment_loader.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/tests/test_kwargs_only.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/tests/test_local_executor.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/tests/test_logging.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/tests/test_memory_limit.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/tests/test_models.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/tests/test_noop_execution.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/tests/test_secret_loader.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/tests/test_serializer.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/tests/test_settings.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/tests/test_tasks.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/tests/test_timeout_validation.py +0 -0
- {django_lambda_tasks-0.11.1 → django_lambda_tasks-0.11.2}/tests/test_timeouts.py +0 -0
|
@@ -315,29 +315,30 @@ def my_task(*, user_id: int) -> None:
|
|
|
315
315
|
|
|
316
316
|
## Sentry
|
|
317
317
|
|
|
318
|
-
The library does not call `sentry_sdk.init(...)` — the host application owns Sentry setup
|
|
318
|
+
The library does not call `sentry_sdk.init(...)` — the host application owns Sentry setup. The **recommended** host configuration is `DjangoIntegration` + `LoggingIntegration(event_level=logging.ERROR)`, and **without** `AwsLambdaIntegration`. `AwsLambdaIntegration` names every event's transaction after the Lambda function, which hides which task ran; omitting it lets each task be traced under its own task-named transaction.
|
|
319
319
|
|
|
320
|
-
`lambda_tasks.sentry.task_scope(*, task_name, message_id, n_retries)`
|
|
320
|
+
`lambda_tasks.sentry.task_scope(*, task_name, message_id, n_retries)` does the per-task tracing. `execute_immediately` wraps the task body in it, which — when a Sentry client is active — opens an isolated `sentry_sdk.new_scope()` and traces the task. With `AwsLambdaIntegration` removed there is no invocation transaction competing for the name, so on **both** Lambda and Batch the task starts its **own transaction named after the task**. It records, both on the span and as scope tags:
|
|
321
321
|
|
|
322
|
-
-
|
|
323
|
-
- **Batch** (nothing active) — the task starts its **own root transaction** so it is still traced.
|
|
324
|
-
|
|
325
|
-
It records, both on the span and as scope tags:
|
|
326
|
-
|
|
327
|
-
- `lambda_task` = fully-qualified task name (e.g. `myapp.tasks.my_task`) — also the task span's name
|
|
322
|
+
- `lambda_task` = fully-qualified task name (e.g. `myapp.tasks.my_task`) — also the transaction/span name
|
|
328
323
|
- `lambda_task.message_id` = the `TaskRecord` primary key
|
|
329
324
|
- `lambda_task.n_retries` = the retry counter
|
|
330
325
|
- `lambda_task.runtime` = the execution environment, from the AWS environment: `AWS_LAMBDA_FUNCTION_NAME` on Lambda or `AWS_BATCH_JQ_NAME` (Batch job queue name); omitted when neither is present
|
|
331
326
|
|
|
332
|
-
|
|
327
|
+
Error attribution also relies on the `lambda_task` **tag**, so it is reliable even when the trace is not sampled.
|
|
328
|
+
|
|
329
|
+
**If a host keeps `AwsLambdaIntegration` anyway:** the library still behaves correctly. `_start_task_root` detects the active invocation transaction (via `sentry_sdk.get_current_scope().transaction` on 2.x; on 3.0 `start_span` is automatically root-or-child) and traces the task as a **child span** of it rather than a second, competing transaction. A nested *transaction* per task would inherit (not independently re-roll) the invocation's sampling decision, doubling transaction volume and defeating `traces_sample_rate`; a child span is sent only if its parent is sampled. In that case the transaction name is the Lambda function name and the task name lives on the `lambda_task` tag.
|
|
330
|
+
|
|
331
|
+
`task_scope` deliberately does **not** call `scope.set_transaction_name(task_name)`: when an invocation transaction is active it would rename the shared transaction (fighting across sibling records in a batch) and, on SDK 3.0 stream mode, corrupt the root-segment naming. The `lambda_task` tag is the reliable, version-independent attribution.
|
|
332
|
+
|
|
333
|
+
**Reporting task failures:** the executor catches task exceptions (recording `FAILED` and not re-raising). On the two **terminal** failure branches in `models.py` (plain failure and max-retries-exceeded) it logs the exception at ERROR via `task_logger.error(error, exc_info=True)`. With the assumed host config `LoggingIntegration(event_level=logging.ERROR)`, that log becomes a Sentry event, attributed to the task by the active `task_scope` tags. No explicit `capture_exception` call is made — the error log is the single reporting path, so there are no duplicate events. The **retry** branch logs at WARNING and is deliberately not reported — transient, to-be-re-enqueued failures would flood Sentry; only terminal failures are reported.
|
|
333
334
|
|
|
334
|
-
**Reporting task failures
|
|
335
|
+
**Reporting failures outside a task:** pre-execution failures in the handler (malformed SQS message, un-importable task, misconfiguration, cold-start errors) happen before `task_scope` opens. `handler.py` logs them at ERROR inside `lambda_tasks.sentry.handler_error_scope(message_id=...)`, a context manager that tags an isolated scope (`lambda_task.handler_error` + the message id) so the resulting log-driven Sentry event is identifiable. This relies on the same `LoggingIntegration(event_level=ERROR)` as task failures. Cold-start failures are additionally re-raised so AWS marks the invocation failed and redrives.
|
|
335
336
|
|
|
336
|
-
**Error-level log forwarding:**
|
|
337
|
+
**Error-level log forwarding (required):** both task-failure and handler-failure reporting flow through `logger.error(..., exc_info=True)` + the host's `LoggingIntegration(event_level=logging.ERROR)`. On sentry-sdk 2.x `event_level=ERROR` was the default; on 3.0 the default is `None`, so the host **must** configure it explicitly for error reporting to work (it is part of the recommended config and the example app). The library does not call `capture_exception` itself, so this integration is not optional for error visibility.
|
|
337
338
|
|
|
338
|
-
**SDK version compatibility:**
|
|
339
|
+
**SDK version compatibility:** the integration is future-proof across the sentry-sdk 2.x→3.0 boundary and verified against both 2.74 and 3.0.0b4. On 2.x it uses `start_transaction` + `set_tag` for a root and `start_span` for a child; on 3.0 (stream mode) `start_span` is both root and child and metadata is attached via `set_attribute`. The correct API is detected at runtime via `hasattr`, so no code change is needed when the host upgrades the SDK.
|
|
339
340
|
|
|
340
|
-
`task_scope` and `
|
|
341
|
+
`task_scope` and `handler_error_scope` are no-ops when `sentry_sdk` is not installed or no client is initialised, so Sentry is not a dependency of the library. `sentry-sdk` is a dev dependency only (pinned `>=2,<3`), for the test suite; a dedicated CI job additionally runs the suite against the latest sentry-sdk 3.x pre-release on Python 3.14.
|
|
341
342
|
|
|
342
343
|
## Built-in Tasks
|
|
343
344
|
|
|
@@ -16,7 +16,7 @@ django-lambda-tasks/
|
|
|
16
16
|
│ ├── decorators.py # LambdaTaskWrapper, @lambda_task
|
|
17
17
|
│ ├── handler.py # AWS Lambda entry point + AWS Batch container entry point (main)
|
|
18
18
|
│ ├── logging.py # task_logger — invocation-scoped LoggerAdapter
|
|
19
|
-
│ ├── sentry.py # task_scope +
|
|
19
|
+
│ ├── sentry.py # task_scope + handler_error_scope — per-task trace, tags, failure attribution
|
|
20
20
|
│ ├── models.py # TaskRecord, SQSLambdaTaskMessage, SQSLambdaTask
|
|
21
21
|
│ ├── settings.py # LambdaTasksSettings, constants, queue type helpers
|
|
22
22
|
│ ├── secret_loader.py # Resolves LAMBDA_TASKS_SECRET_* env vars at cold start
|
|
@@ -44,11 +44,11 @@ django-lambda-tasks/
|
|
|
44
44
|
- `decorators.py` — defines `LambdaTaskWrapper` and `@lambda_task` decorator factory; enforces kwargs-only at decoration time; validates timeouts (> 0, soft < hard) at decoration time; upper-bound timeout validation deferred to execution time based on queue type
|
|
45
45
|
- `models.py` — `TaskRecord` (Django ORM), `SQSLambdaTaskMessage` (Pydantic, SQS schema + execution logic; `execute_immediately` owns the logging + Sentry context and delegates the run to a private `_run` method), `SQSLambdaTask` (Pydantic, holds message + routing; `_execute()` inspects queue config to publish to SQS, submit to Batch via `submit_batch_job`, execute eagerly, or submit to the local process pool; `execute_on_commit()` registers `_execute` with `transaction.on_commit`)
|
|
46
46
|
- `local_executor.py` — `ProcessPoolExecutor`-based async local execution; `get_pool()` lazily creates a module-level pool; `submit_task()` fire-and-forget submission; `_execute_in_worker()` deserializes and runs the task in a child process; `_pool_initializer()` calls `django.setup()` per worker (and sets `SIGINT` to `SIG_IGN` so workers ignore Ctrl+C); `_install_shutdown_handlers()` installs main-thread `SIGINT`/`SIGTERM` handlers that release the pool before chaining to the previous handler
|
|
47
|
-
- `handler.py` — Lambda entry point (`handler(event, context)`); cold-start init (memory limit → temporary log handler → `resolve_environment()` → `resolve_secrets_into_env()` → handler removed → conditional `django.setup()`) runs inside the handler on first invocation, guarded by `_cold_start_done` sentinel; calls `db.close_old_connections()` before each SQS record to handle stale connections on warm starts; processes SQS records independently; returns `batchItemFailures
|
|
47
|
+
- `handler.py` — Lambda entry point (`handler(event, context)`); cold-start init (memory limit → temporary log handler → `resolve_environment()` → `resolve_secrets_into_env()` → handler removed → conditional `django.setup()`) runs inside the handler on first invocation, guarded by `_cold_start_done` sentinel; calls `db.close_old_connections()` before each SQS record to handle stale connections on warm starts; processes SQS records independently; returns `batchItemFailures`; logs pre-execution failures (malformed message, import error, misconfiguration) at ERROR inside `handler_error_scope` so the host's `LoggingIntegration` reports them tagged `lambda_task.handler_error`; cold-start failures are reported the same way and re-raised. Also provides `main()` as the AWS Batch container entry point (`python -m lambda_tasks.handler`) — reads `LAMBDA_TASKS_MESSAGE` env var, constructs a synthetic SQS event, and delegates to `handler()`
|
|
48
48
|
- `environment_loader.py` — loads env vars from a Secrets Manager secret at cold start; validates flat JSON format; idempotent via `_loaded` sentinel
|
|
49
49
|
- `secret_loader.py` — resolves `LAMBDA_TASKS_SECRET_*` env vars from Secrets Manager before Django starts; validates format, detects conflicts, batches API calls; idempotent via `_loaded` sentinel
|
|
50
50
|
- `logging.py` — `task_logger` singleton; `message_id` set/cleared around each task execution
|
|
51
|
-
- `sentry.py` — `task_scope` context manager + `
|
|
51
|
+
- `sentry.py` — `task_scope` context manager + `handler_error_scope` context manager. `task_scope` opens an isolated `new_scope` and traces the task. With the **recommended** host config (no `AwsLambdaIntegration`) nothing is active, so the task starts its **own transaction named after the task** on both Lambda and Batch. If a host keeps `AwsLambdaIntegration`, the task is instead traced as a **child span** of the active invocation transaction (detected via `sentry_sdk.get_current_scope().transaction` on 2.x; `start_span` is auto root-or-child on 3.0) — never a second, competing transaction, which would double trace volume and defeat `traces_sample_rate`. Records `lambda_task` / `lambda_task.message_id` / `lambda_task.n_retries` / `lambda_task.runtime` as span metadata and scope tags (runtime read from `AWS_LAMBDA_FUNCTION_NAME` or `AWS_BATCH_JQ_NAME`). Deliberately does not call `set_transaction_name` (would rename an active invocation transaction and break 3.0 segment naming); error attribution rides on the `lambda_task` tag. **Failure reporting** is via error logs, not explicit captures: the terminal failure branches in `models.py` (plain failure + max-retries) call `task_logger.error(error, exc_info=True)`, and the host's `LoggingIntegration(event_level=ERROR)` turns that into the Sentry event (the retry branch logs at WARNING and is not reported). `handler_error_scope(message_id=...)` is a context manager used in `handler.py` to tag an isolated scope (`lambda_task.handler_error` + message id) around the handler's `logger.error`, so pre-task failures are reported and identifiable via the same LoggingIntegration. Future-proof across the SDK 2.x→3.0 boundary (verified against 2.74 and 3.0.0b4): `start_transaction`/`set_tag`/child `start_span` on 2.x, `start_span`/`set_attribute` on 3.0, detected at runtime via `hasattr`. No-op when `sentry_sdk` is absent or uninitialised (`sentry-sdk` is a dev dependency only, pinned `>=2,<3`). Wrapped around the task body in `SQSLambdaTaskMessage.execute_immediately`. Error reporting requires `LoggingIntegration(event_level=logging.ERROR)` in the host init (default on 2.x, must be set on 3.0); demonstrated in the example app
|
|
52
52
|
- `settings.py` — constants (`MAX_DELAY`, `MAX_TIMEOUT`, `MAX_BATCH_TIMEOUT`); `_is_sqs_queue()` and `_is_batch_queue()` helpers; `LambdaTasksSettings` instantiated fresh per use (reads live Django settings); `queue_max_timeout()` method returns 900 or 3600 based on queue type
|
|
53
53
|
- `admin.py` — Django admin registration for `TaskRecord`
|
|
54
54
|
- `apps.py` — Django `AppConfig`; `ready()` imports `checks` to register deployment checks and installs the local-executor shutdown signal handlers when `LOCAL_WORKERS > 0`
|
|
@@ -23,7 +23,7 @@ inclusion: always
|
|
|
23
23
|
|---|---|---|
|
|
24
24
|
| `pytest` | `>= 9.0.2` | Test runner |
|
|
25
25
|
| `hypothesis` | present | Property-based testing (`.hypothesis/` directory exists) |
|
|
26
|
-
| `sentry-sdk` | `>=2,<3` | Testing the Sentry integration (`task_scope`, `
|
|
26
|
+
| `sentry-sdk` | `>=2,<3` | Testing the Sentry integration (`task_scope`, `handler_error_scope`); verified against 2.74 and 3.0.0b4. Not a runtime dependency |
|
|
27
27
|
|
|
28
28
|
## Package Manager
|
|
29
29
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: django-lambda-tasks
|
|
3
|
-
Version: 0.11.
|
|
3
|
+
Version: 0.11.2
|
|
4
4
|
Summary: Run async tasks in a lambda function
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Requires-Dist: awslambdaric
|
|
@@ -446,48 +446,57 @@ fields @timestamp, @message
|
|
|
446
446
|
|
|
447
447
|
### Sentry
|
|
448
448
|
|
|
449
|
-
If your application uses Sentry, task errors are automatically attributed to the task that raised them
|
|
449
|
+
If your application uses Sentry, task errors are automatically attributed to the task that raised them, and each task is traced under its own task-named transaction.
|
|
450
450
|
|
|
451
|
-
|
|
451
|
+
#### Recommended `sentry_sdk.init(...)`
|
|
452
|
+
|
|
453
|
+
Use `DjangoIntegration` + `LoggingIntegration(event_level=logging.ERROR)`, and **do not** add `AwsLambdaIntegration`:
|
|
452
454
|
|
|
453
455
|
```python
|
|
454
456
|
import logging
|
|
457
|
+
from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration
|
|
458
|
+
from sentry_sdk.integrations.django import DjangoIntegration
|
|
455
459
|
from sentry_sdk.integrations.logging import LoggingIntegration
|
|
456
460
|
|
|
457
461
|
sentry_sdk.init(
|
|
458
462
|
dsn=SENTRY_DSN,
|
|
459
463
|
integrations=[
|
|
460
464
|
DjangoIntegration(),
|
|
461
|
-
AwsLambdaIntegration(),
|
|
462
|
-
# Only needed on sentry-sdk 3.0+: its default event_level is None, so
|
|
463
|
-
# logger.error(...) no longer becomes a Sentry event. On 2.x this was
|
|
464
|
-
# the default and setting it is harmless.
|
|
465
465
|
LoggingIntegration(event_level=logging.ERROR),
|
|
466
466
|
],
|
|
467
|
+
disabled_integrations=[
|
|
468
|
+
AwsLambdaIntegration(),
|
|
469
|
+
],
|
|
467
470
|
traces_sample_rate=0.1,
|
|
468
471
|
)
|
|
469
472
|
```
|
|
470
473
|
|
|
471
|
-
|
|
474
|
+
Two deliberate choices:
|
|
472
475
|
|
|
473
|
-
|
|
476
|
+
- **No `AwsLambdaIntegration`.** It names every event's transaction after the Lambda function, which hides which task ran — all tasks would look the same. Without it, each task's trace is named after the task (see below). You lose `AwsLambdaIntegration`'s automatic reporting of handler-level failures, but the library reports those itself (see "Failures outside a task"), so nothing is lost in practice.
|
|
477
|
+
- **`LoggingIntegration(event_level=logging.ERROR)`.** This makes `logger.error(...)` (including `task_logger`) become Sentry events. On sentry-sdk 2.x `event_level=ERROR` was the default; on 3.0 the default is `None`, so you must set it explicitly. It is harmless on 2.x.
|
|
474
478
|
|
|
475
|
-
|
|
476
|
-
- **On Batch** (no Lambda integration, nothing active), the task starts **its own root transaction** so it is still traced.
|
|
479
|
+
#### What the library does per task
|
|
477
480
|
|
|
478
|
-
|
|
481
|
+
When a task runs, `execute_immediately()` opens an isolated Sentry scope and traces the task. With `AwsLambdaIntegration` removed there is no invocation transaction competing for the name, so on **both** Lambda and Batch the task starts its **own transaction named after the task**. It records, as both span metadata and scope tags:
|
|
479
482
|
|
|
480
|
-
- `lambda_task` — the fully-qualified task name (e.g. `myapp.tasks.send_welcome_email`)
|
|
483
|
+
- `lambda_task` — the fully-qualified task name (e.g. `myapp.tasks.send_welcome_email`); also the transaction/span name.
|
|
481
484
|
- `lambda_task.message_id`, `lambda_task.n_retries`
|
|
482
485
|
- `lambda_task.runtime` — the execution environment, from the AWS environment: `AWS_LAMBDA_FUNCTION_NAME` on Lambda or `AWS_BATCH_JQ_NAME` (the Batch job queue name) on Batch. Omitted when neither is set (local/eager).
|
|
483
486
|
|
|
484
|
-
Error attribution relies on the `lambda_task` **tag**,
|
|
487
|
+
Error attribution also relies on the `lambda_task` **tag**, so it is reliable even when the trace is not sampled.
|
|
488
|
+
|
|
489
|
+
> If you keep `AwsLambdaIntegration` for other reasons, the library still does the right thing: it detects the active invocation transaction and traces the task as a **child span** of it (rather than a second, competing transaction, which would double trace volume and defeat `traces_sample_rate`). In that case the transaction name is the Lambda function name and the task name lives on the `lambda_task` tag.
|
|
485
490
|
|
|
486
491
|
#### Task failures are reported
|
|
487
492
|
|
|
488
|
-
The executor catches a failing task's exception (recording a `FAILED` `TaskRecord`) rather than re-raising it.
|
|
493
|
+
The executor catches a failing task's exception (recording a `FAILED` `TaskRecord`) rather than re-raising it. On the **terminal failure paths** — a plain failure and max-retries-exceeded — the library logs the exception at ERROR (`task_logger.error(..., exc_info=True)`), which your `LoggingIntegration(event_level=ERROR)` turns into a Sentry event, attributed to the task via the active scope tags. This is why that integration is part of the recommended config: without it, a merely-caught exception produces no error event on 3.0.
|
|
494
|
+
|
|
495
|
+
Transient, retryable failures (a task that will be re-enqueued) are logged at WARNING and **not** reported to Sentry, to avoid flooding on every retry; only the final, terminal failure is reported.
|
|
496
|
+
|
|
497
|
+
#### Failures outside a task
|
|
489
498
|
|
|
490
|
-
|
|
499
|
+
Pre-execution failures in the Lambda/Batch handler — a malformed SQS message, a task that cannot be imported, a misconfiguration, or a cold-start error — happen before the task scope opens and are caught by the handler. The handler logs them at ERROR inside a `handler_error_scope` that tags the event `lambda_task.handler_error` (plus the message id). With `LoggingIntegration(event_level=ERROR)` that log becomes a single, tagged Sentry event — so you are still alerted even though `AwsLambdaIntegration` is not installed. Cold-start failures are also re-raised so AWS marks the invocation failed and redrives it.
|
|
491
500
|
|
|
492
501
|
#### SDK version compatibility
|
|
493
502
|
|
|
@@ -434,48 +434,57 @@ fields @timestamp, @message
|
|
|
434
434
|
|
|
435
435
|
### Sentry
|
|
436
436
|
|
|
437
|
-
If your application uses Sentry, task errors are automatically attributed to the task that raised them
|
|
437
|
+
If your application uses Sentry, task errors are automatically attributed to the task that raised them, and each task is traced under its own task-named transaction.
|
|
438
438
|
|
|
439
|
-
|
|
439
|
+
#### Recommended `sentry_sdk.init(...)`
|
|
440
|
+
|
|
441
|
+
Use `DjangoIntegration` + `LoggingIntegration(event_level=logging.ERROR)`, and **do not** add `AwsLambdaIntegration`:
|
|
440
442
|
|
|
441
443
|
```python
|
|
442
444
|
import logging
|
|
445
|
+
from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration
|
|
446
|
+
from sentry_sdk.integrations.django import DjangoIntegration
|
|
443
447
|
from sentry_sdk.integrations.logging import LoggingIntegration
|
|
444
448
|
|
|
445
449
|
sentry_sdk.init(
|
|
446
450
|
dsn=SENTRY_DSN,
|
|
447
451
|
integrations=[
|
|
448
452
|
DjangoIntegration(),
|
|
449
|
-
AwsLambdaIntegration(),
|
|
450
|
-
# Only needed on sentry-sdk 3.0+: its default event_level is None, so
|
|
451
|
-
# logger.error(...) no longer becomes a Sentry event. On 2.x this was
|
|
452
|
-
# the default and setting it is harmless.
|
|
453
453
|
LoggingIntegration(event_level=logging.ERROR),
|
|
454
454
|
],
|
|
455
|
+
disabled_integrations=[
|
|
456
|
+
AwsLambdaIntegration(),
|
|
457
|
+
],
|
|
455
458
|
traces_sample_rate=0.1,
|
|
456
459
|
)
|
|
457
460
|
```
|
|
458
461
|
|
|
459
|
-
|
|
462
|
+
Two deliberate choices:
|
|
460
463
|
|
|
461
|
-
|
|
464
|
+
- **No `AwsLambdaIntegration`.** It names every event's transaction after the Lambda function, which hides which task ran — all tasks would look the same. Without it, each task's trace is named after the task (see below). You lose `AwsLambdaIntegration`'s automatic reporting of handler-level failures, but the library reports those itself (see "Failures outside a task"), so nothing is lost in practice.
|
|
465
|
+
- **`LoggingIntegration(event_level=logging.ERROR)`.** This makes `logger.error(...)` (including `task_logger`) become Sentry events. On sentry-sdk 2.x `event_level=ERROR` was the default; on 3.0 the default is `None`, so you must set it explicitly. It is harmless on 2.x.
|
|
462
466
|
|
|
463
|
-
|
|
464
|
-
- **On Batch** (no Lambda integration, nothing active), the task starts **its own root transaction** so it is still traced.
|
|
467
|
+
#### What the library does per task
|
|
465
468
|
|
|
466
|
-
|
|
469
|
+
When a task runs, `execute_immediately()` opens an isolated Sentry scope and traces the task. With `AwsLambdaIntegration` removed there is no invocation transaction competing for the name, so on **both** Lambda and Batch the task starts its **own transaction named after the task**. It records, as both span metadata and scope tags:
|
|
467
470
|
|
|
468
|
-
- `lambda_task` — the fully-qualified task name (e.g. `myapp.tasks.send_welcome_email`)
|
|
471
|
+
- `lambda_task` — the fully-qualified task name (e.g. `myapp.tasks.send_welcome_email`); also the transaction/span name.
|
|
469
472
|
- `lambda_task.message_id`, `lambda_task.n_retries`
|
|
470
473
|
- `lambda_task.runtime` — the execution environment, from the AWS environment: `AWS_LAMBDA_FUNCTION_NAME` on Lambda or `AWS_BATCH_JQ_NAME` (the Batch job queue name) on Batch. Omitted when neither is set (local/eager).
|
|
471
474
|
|
|
472
|
-
Error attribution relies on the `lambda_task` **tag**,
|
|
475
|
+
Error attribution also relies on the `lambda_task` **tag**, so it is reliable even when the trace is not sampled.
|
|
476
|
+
|
|
477
|
+
> If you keep `AwsLambdaIntegration` for other reasons, the library still does the right thing: it detects the active invocation transaction and traces the task as a **child span** of it (rather than a second, competing transaction, which would double trace volume and defeat `traces_sample_rate`). In that case the transaction name is the Lambda function name and the task name lives on the `lambda_task` tag.
|
|
473
478
|
|
|
474
479
|
#### Task failures are reported
|
|
475
480
|
|
|
476
|
-
The executor catches a failing task's exception (recording a `FAILED` `TaskRecord`) rather than re-raising it.
|
|
481
|
+
The executor catches a failing task's exception (recording a `FAILED` `TaskRecord`) rather than re-raising it. On the **terminal failure paths** — a plain failure and max-retries-exceeded — the library logs the exception at ERROR (`task_logger.error(..., exc_info=True)`), which your `LoggingIntegration(event_level=ERROR)` turns into a Sentry event, attributed to the task via the active scope tags. This is why that integration is part of the recommended config: without it, a merely-caught exception produces no error event on 3.0.
|
|
482
|
+
|
|
483
|
+
Transient, retryable failures (a task that will be re-enqueued) are logged at WARNING and **not** reported to Sentry, to avoid flooding on every retry; only the final, terminal failure is reported.
|
|
484
|
+
|
|
485
|
+
#### Failures outside a task
|
|
477
486
|
|
|
478
|
-
|
|
487
|
+
Pre-execution failures in the Lambda/Batch handler — a malformed SQS message, a task that cannot be imported, a misconfiguration, or a cold-start error — happen before the task scope opens and are caught by the handler. The handler logs them at ERROR inside a `handler_error_scope` that tags the event `lambda_task.handler_error` (plus the message id). With `LoggingIntegration(event_level=ERROR)` that log becomes a single, tagged Sentry event — so you are still alerted even though `AwsLambdaIntegration` is not installed. Cold-start failures are also re-raised so AWS marks the invocation failed and redrives it.
|
|
479
488
|
|
|
480
489
|
#### SDK version compatibility
|
|
481
490
|
|
|
@@ -47,3 +47,16 @@ The task runs in a background worker process and the response confirms it was tr
|
|
|
47
47
|
Visit: `http://127.0.0.1:8000/admin/` → Lambda Tasks → Task records
|
|
48
48
|
|
|
49
49
|
Log in with the superuser credentials you created above.
|
|
50
|
+
|
|
51
|
+
## Testing the Sentry integration (optional)
|
|
52
|
+
|
|
53
|
+
Set `SENTRY_DSN` to see per-task traces and error attribution in Sentry. The init lives in `example_project/sentry.py` and uses `DjangoIntegration` + `LoggingIntegration(event_level=ERROR)` **without** `AwsLambdaIntegration`, so each task is traced under its own task-named transaction (see the library README's Sentry section for why).
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
SENTRY_DSN="https://<key>@<org>.ingest.sentry.io/<project>" uv run python manage.py runserver
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
- `http://127.0.0.1:8000/trigger/?name=Alice` — a successful task; produces a transaction named `example_app.tasks.greet`.
|
|
60
|
+
- `http://127.0.0.1:8000/trigger-error/?name=Bob` — a failing task; produces an error attributed to `example_app.tasks.boom` (via the library's `capture_exception` on the terminal failure path).
|
|
61
|
+
|
|
62
|
+
Without `SENTRY_DSN` set, Sentry initialisation is skipped and the app runs exactly as above.
|
|
@@ -36,14 +36,21 @@ def init_sentry() -> None:
|
|
|
36
36
|
dsn=dsn,
|
|
37
37
|
integrations=[
|
|
38
38
|
DjangoIntegration(),
|
|
39
|
-
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
39
|
+
# LoggingIntegration(event_level=ERROR) is REQUIRED, not optional:
|
|
40
|
+
# the library reports task and handler failures by logging them at
|
|
41
|
+
# ERROR (not by calling capture_exception), so this integration is
|
|
42
|
+
# what turns those logs into Sentry events. On sentry-sdk 2.x
|
|
43
|
+
# event_level=ERROR was the default; on 3.0 the default is None, so
|
|
44
|
+
# it must be set explicitly.
|
|
45
45
|
LoggingIntegration(event_level=logging.ERROR),
|
|
46
46
|
],
|
|
47
|
+
disabled_integrations=[
|
|
48
|
+
# AwsLambdaIntegration is intentionally omitted. It names every
|
|
49
|
+
# event's transaction after the Lambda function, which hides the
|
|
50
|
+
# task name. Without it, each task's trace is named after the task
|
|
51
|
+
# (see lambda_tasks.sentry).
|
|
52
|
+
AwsLambdaIntegration(),
|
|
53
|
+
],
|
|
47
54
|
# Sample every transaction so the per-task trace is always sent while
|
|
48
55
|
# you are testing. Lower this in a real deployment.
|
|
49
56
|
traces_sample_rate=float(os.environ.get("SENTRY_TRACES_SAMPLE_RATE", "1.0")),
|
|
@@ -26,6 +26,7 @@ from django.apps import apps
|
|
|
26
26
|
|
|
27
27
|
from lambda_tasks.environment_loader import resolve_environment
|
|
28
28
|
from lambda_tasks.secret_loader import resolve_secrets_into_env
|
|
29
|
+
from lambda_tasks.sentry import handler_error_scope
|
|
29
30
|
|
|
30
31
|
logger = logging.getLogger(__name__)
|
|
31
32
|
|
|
@@ -124,7 +125,17 @@ def handler(event: dict, context: object) -> dict:
|
|
|
124
125
|
Returns a partial-batch failure report so AWS only re-drives failed records.
|
|
125
126
|
Signature is fixed by AWS and uses two args only.
|
|
126
127
|
"""
|
|
127
|
-
|
|
128
|
+
try:
|
|
129
|
+
_perform_cold_start()
|
|
130
|
+
except Exception:
|
|
131
|
+
# Cold-start failures (secret resolution, django.setup(), memory limit)
|
|
132
|
+
# escape handler() so the invocation is marked failed and redriven, as
|
|
133
|
+
# AWS expects. The error log is reported to Sentry by the host's
|
|
134
|
+
# LoggingIntegration; handler_error_scope tags it as a handler error.
|
|
135
|
+
# Re-raise to preserve the AWS failure contract.
|
|
136
|
+
with handler_error_scope():
|
|
137
|
+
logger.error("Cold-start initialisation failed", exc_info=True)
|
|
138
|
+
raise
|
|
128
139
|
|
|
129
140
|
# Local import due to AppRegistryNotReady
|
|
130
141
|
from lambda_tasks.models import SQSLambdaTaskMessage
|
|
@@ -138,10 +149,13 @@ def handler(event: dict, context: object) -> dict:
|
|
|
138
149
|
record["body"]
|
|
139
150
|
).execute_immediately(message_id=record["messageId"])
|
|
140
151
|
except Exception:
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
152
|
+
# Pre-execution failures (malformed message, import error,
|
|
153
|
+
# misconfiguration) happen before execute_immediately opens its
|
|
154
|
+
# task_scope. The error log is reported to Sentry by the host's
|
|
155
|
+
# LoggingIntegration; handler_error_scope tags it with the message
|
|
156
|
+
# id and marks it a handler error.
|
|
157
|
+
with handler_error_scope(message_id=record.get("messageId")):
|
|
158
|
+
logger.error("Failed to process SQS record", exc_info=True)
|
|
145
159
|
batch_item_failures.append({"itemIdentifier": record["messageId"]})
|
|
146
160
|
|
|
147
161
|
return {"batchItemFailures": batch_item_failures}
|
|
@@ -154,21 +168,31 @@ def main() -> int:
|
|
|
154
168
|
constructs a synthetic SQS event, and delegates to handler().
|
|
155
169
|
|
|
156
170
|
Returns 0 on success, 1 on failure.
|
|
171
|
+
|
|
172
|
+
Wrapped in a top-level handler so that failures on the Batch path — a
|
|
173
|
+
missing message, or an exception escaping cold-start/``handler()`` (e.g.
|
|
174
|
+
secret resolution or ``django.setup()``) — are reported to Sentry. The
|
|
175
|
+
Batch container has no AWS-side Sentry integration, so without this such
|
|
176
|
+
failures would only reach stderr.
|
|
157
177
|
"""
|
|
158
178
|
logging.basicConfig(level=logging.INFO)
|
|
159
179
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
180
|
+
try:
|
|
181
|
+
message_json = os.environ.get("LAMBDA_TASKS_MESSAGE")
|
|
182
|
+
if not message_json:
|
|
183
|
+
raise RuntimeError("LAMBDA_TASKS_MESSAGE environment variable is not set.")
|
|
184
|
+
|
|
185
|
+
message_id = os.environ.get("AWS_BATCH_JOB_ID", str(uuid.uuid4()))
|
|
186
|
+
|
|
187
|
+
result = handler(
|
|
188
|
+
event={"Records": [{"messageId": message_id, "body": message_json}]},
|
|
189
|
+
context=None,
|
|
190
|
+
)
|
|
191
|
+
except Exception:
|
|
192
|
+
with handler_error_scope(message_id=os.environ.get("AWS_BATCH_JOB_ID")):
|
|
193
|
+
logger.error("AWS Batch entry point failed", exc_info=True)
|
|
163
194
|
return 1
|
|
164
195
|
|
|
165
|
-
message_id = os.environ.get("AWS_BATCH_JOB_ID", str(uuid.uuid4()))
|
|
166
|
-
|
|
167
|
-
result = handler(
|
|
168
|
-
event={"Records": [{"messageId": message_id, "body": message_json}]},
|
|
169
|
-
context=None,
|
|
170
|
-
)
|
|
171
|
-
|
|
172
196
|
if result["batchItemFailures"]:
|
|
173
197
|
return 1
|
|
174
198
|
else:
|
|
@@ -21,7 +21,7 @@ from redis.exceptions import LockError
|
|
|
21
21
|
|
|
22
22
|
from lambda_tasks.local_executor import submit_task
|
|
23
23
|
from lambda_tasks.logging import task_logger
|
|
24
|
-
from lambda_tasks.sentry import
|
|
24
|
+
from lambda_tasks.sentry import task_scope
|
|
25
25
|
from lambda_tasks.settings import (
|
|
26
26
|
MAX_DELAY,
|
|
27
27
|
BatchQueueConfig,
|
|
@@ -210,7 +210,8 @@ class SQSLambdaTaskMessage(BaseModel):
|
|
|
210
210
|
elif effective_retry_on and isinstance(error, effective_retry_on):
|
|
211
211
|
conf = LambdaTasksSettings()
|
|
212
212
|
if self.n_retries >= conf.MAX_RETRIES:
|
|
213
|
-
|
|
213
|
+
task_logger.error("Maximum retries exceeded", exc_info=True)
|
|
214
|
+
|
|
214
215
|
record.status = TaskStatus.FAILED
|
|
215
216
|
record.traceback = traceback.format_exc()
|
|
216
217
|
record.end_time = now()
|
|
@@ -252,7 +253,6 @@ class SQSLambdaTaskMessage(BaseModel):
|
|
|
252
253
|
else:
|
|
253
254
|
task_logger.error(error, exc_info=True)
|
|
254
255
|
|
|
255
|
-
capture_task_exception()
|
|
256
256
|
record.status = TaskStatus.FAILED
|
|
257
257
|
record.traceback = traceback.format_exc()
|
|
258
258
|
record.end_time = now()
|
|
@@ -120,23 +120,38 @@ def _start_task_root(*, task_name: str) -> Any:
|
|
|
120
120
|
return sentry_sdk.start_transaction(name=task_name, op="task")
|
|
121
121
|
|
|
122
122
|
|
|
123
|
-
def
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
123
|
+
def handler_error_scope(
|
|
124
|
+
*, message_id: str | None = None
|
|
125
|
+
) -> "contextlib.AbstractContextManager[Any]":
|
|
126
|
+
"""Scope a handler-level failure's tags so a subsequent error *log* carries them.
|
|
127
|
+
|
|
128
|
+
Pre-execution failures in the Lambda/Batch handler (malformed message,
|
|
129
|
+
un-importable task, misconfiguration, cold-start errors) happen outside any
|
|
130
|
+
task scope. The handler logs them at ERROR; with the host's
|
|
131
|
+
``LoggingIntegration(event_level=ERROR)`` that log becomes the Sentry event.
|
|
132
|
+
Entering this context first tags an isolated scope so the resulting event is
|
|
133
|
+
distinguishable (``lambda_task.handler_error``) and carries the message id.
|
|
134
|
+
|
|
135
|
+
Usage::
|
|
136
|
+
|
|
137
|
+
with handler_error_scope(message_id=...):
|
|
138
|
+
logger.error("...", exc_info=True)
|
|
139
|
+
|
|
140
|
+
A no-op (returns a null context) when ``sentry_sdk`` is unavailable or
|
|
141
|
+
uninitialised.
|
|
137
142
|
"""
|
|
138
|
-
if sentry_sdk and sentry_sdk.is_initialized():
|
|
139
|
-
|
|
143
|
+
if not (sentry_sdk and sentry_sdk.is_initialized()):
|
|
144
|
+
return contextlib.nullcontext()
|
|
145
|
+
|
|
146
|
+
@contextlib.contextmanager
|
|
147
|
+
def _scope() -> Iterator[None]:
|
|
148
|
+
with sentry_sdk.new_scope() as scope:
|
|
149
|
+
scope.set_tag("lambda_task.handler_error", True)
|
|
150
|
+
if message_id is not None:
|
|
151
|
+
scope.set_tag("lambda_task.message_id", message_id)
|
|
152
|
+
yield
|
|
153
|
+
|
|
154
|
+
return _scope()
|
|
140
155
|
|
|
141
156
|
|
|
142
157
|
@contextlib.contextmanager
|