django-lambda-tasks 0.10.0__tar.gz → 0.11.1__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.
Files changed (117) hide show
  1. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.github/workflows/ci.yml +32 -0
  2. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/steering/product.md +27 -1
  3. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/steering/structure.md +3 -1
  4. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/steering/tech.md +1 -0
  5. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/PKG-INFO +52 -1
  6. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/README.md +51 -0
  7. django_lambda_tasks-0.11.1/example/example_app/tasks.py +31 -0
  8. django_lambda_tasks-0.11.1/example/example_app/urls.py +7 -0
  9. django_lambda_tasks-0.11.1/example/example_app/views.py +17 -0
  10. django_lambda_tasks-0.11.1/example/example_project/sentry.py +50 -0
  11. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/example/example_project/settings.py +7 -0
  12. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/lambda_tasks/models.py +138 -118
  13. django_lambda_tasks-0.11.1/lambda_tasks/sentry.py +186 -0
  14. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/pyproject.toml +2 -1
  15. django_lambda_tasks-0.11.1/tests/test_sentry.py +566 -0
  16. django_lambda_tasks-0.10.0/example/example_app/tasks.py +0 -18
  17. django_lambda_tasks-0.10.0/example/example_app/urls.py +0 -6
  18. django_lambda_tasks-0.10.0/example/example_app/views.py +0 -8
  19. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.github/dependabot.yml +0 -0
  20. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.github/workflows/release.yml +0 -0
  21. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.gitignore +0 -0
  22. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/async-local-execution/.config.kiro +0 -0
  23. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/async-local-execution/design.md +0 -0
  24. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/async-local-execution/requirements.md +0 -0
  25. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/async-local-execution/tasks.md +0 -0
  26. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/deferred-task-enqueue/.config.kiro +0 -0
  27. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/deferred-task-enqueue/design.md +0 -0
  28. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/deferred-task-enqueue/requirements.md +0 -0
  29. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/deferred-task-enqueue/tasks.md +0 -0
  30. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/eager-mode-example-app/.config.kiro +0 -0
  31. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/eager-mode-example-app/design.md +0 -0
  32. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/eager-mode-example-app/requirements.md +0 -0
  33. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/eager-mode-example-app/tasks.md +0 -0
  34. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/ignore-errors-decorator-option/.config.kiro +0 -0
  35. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/ignore-errors-decorator-option/design.md +0 -0
  36. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/ignore-errors-decorator-option/requirements.md +0 -0
  37. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/ignore-errors-decorator-option/tasks.md +0 -0
  38. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/import-string-task-resolution/.config.kiro +0 -0
  39. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/import-string-task-resolution/design.md +0 -0
  40. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/import-string-task-resolution/requirements.md +0 -0
  41. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/import-string-task-resolution/tasks.md +0 -0
  42. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/retry-delay/.config.kiro +0 -0
  43. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/retry-delay/design.md +0 -0
  44. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/retry-delay/requirements.md +0 -0
  45. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/retry-delay/tasks.md +0 -0
  46. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/rse-background-tasks/.config.kiro +0 -0
  47. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/rse-background-tasks/design.md +0 -0
  48. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/rse-background-tasks/requirements.md +0 -0
  49. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/rse-background-tasks/tasks.md +0 -0
  50. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/rse-background-tasks-bugfix/.config.kiro +0 -0
  51. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/rse-background-tasks-bugfix/bugfix.md +0 -0
  52. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/rse-background-tasks-bugfix/design.md +0 -0
  53. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/rse-background-tasks-bugfix/tasks.md +0 -0
  54. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/singleton-task/.config.kiro +0 -0
  55. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/singleton-task/design.md +0 -0
  56. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/singleton-task/requirements.md +0 -0
  57. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/singleton-task/tasks.md +0 -0
  58. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/ssm-environment-loader/.config.kiro +0 -0
  59. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/ssm-environment-loader/design.md +0 -0
  60. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/ssm-environment-loader/requirements.md +0 -0
  61. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/ssm-environment-loader/tasks.md +0 -0
  62. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/task-retry/.config.kiro +0 -0
  63. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/task-retry/design.md +0 -0
  64. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/task-retry/requirements.md +0 -0
  65. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.kiro/specs/task-retry/tasks.md +0 -0
  66. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.pre-commit-config.yaml +0 -0
  67. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/.vscode/settings.json +0 -0
  68. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/docker-compose.yml +0 -0
  69. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/example/README.md +0 -0
  70. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/example/docker-compose.yml +0 -0
  71. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/example/example_app/__init__.py +0 -0
  72. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/example/example_app/apps.py +0 -0
  73. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/example/example_project/__init__.py +0 -0
  74. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/example/example_project/urls.py +0 -0
  75. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/example/example_project/wsgi.py +0 -0
  76. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/example/manage.py +0 -0
  77. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/lambda_tasks/__init__.py +0 -0
  78. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/lambda_tasks/admin.py +0 -0
  79. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/lambda_tasks/apps.py +0 -0
  80. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/lambda_tasks/checks.py +0 -0
  81. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/lambda_tasks/decorators.py +0 -0
  82. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/lambda_tasks/environment_loader.py +0 -0
  83. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/lambda_tasks/handler.py +0 -0
  84. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/lambda_tasks/local_executor.py +0 -0
  85. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/lambda_tasks/logging.py +0 -0
  86. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/lambda_tasks/migrations/0001_initial.py +0 -0
  87. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/lambda_tasks/migrations/0002_alter_taskrecord_status.py +0 -0
  88. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/lambda_tasks/migrations/0003_taskrecord_taskrecord_kwargs_trgm.py +0 -0
  89. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/lambda_tasks/migrations/0004_remove_taskrecord_taskrecord_kwargs_trgm_and_more.py +0 -0
  90. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/lambda_tasks/migrations/__init__.py +0 -0
  91. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/lambda_tasks/secret_loader.py +0 -0
  92. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/lambda_tasks/settings.py +0 -0
  93. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/lambda_tasks/tasks.py +0 -0
  94. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/lambda_tasks/timeouts.py +0 -0
  95. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/conftest.py +0 -0
  96. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/settings.py +0 -0
  97. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/test_admin.py +0 -0
  98. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/test_batch.py +0 -0
  99. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/test_batch_worker.py +0 -0
  100. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/test_checks.py +0 -0
  101. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/test_decorator.py +0 -0
  102. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/test_decorators.py +0 -0
  103. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/test_deferred_enqueue.py +0 -0
  104. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/test_environment_loader.py +0 -0
  105. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/test_handler.py +0 -0
  106. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/test_kwargs_only.py +0 -0
  107. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/test_local_executor.py +0 -0
  108. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/test_logging.py +0 -0
  109. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/test_memory_limit.py +0 -0
  110. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/test_models.py +0 -0
  111. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/test_noop_execution.py +0 -0
  112. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/test_secret_loader.py +0 -0
  113. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/test_serializer.py +0 -0
  114. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/test_settings.py +0 -0
  115. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/test_tasks.py +0 -0
  116. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/test_timeout_validation.py +0 -0
  117. {django_lambda_tasks-0.10.0 → django_lambda_tasks-0.11.1}/tests/test_timeouts.py +0 -0
@@ -53,3 +53,35 @@ jobs:
53
53
  - name: Run tests
54
54
  run: |
55
55
  uv run pytest --cov-branch --cov-report term-missing --cov=tests/ --cov=lambda_tasks/
56
+
57
+ sentry-prerelease:
58
+ # Exercise the Sentry integration against the latest 3.x pre-release of
59
+ # sentry-sdk (stream mode). The committed dev dependency is pinned to the
60
+ # stable 2.x line; this job overrides it at runtime via `uv run --with`
61
+ # without touching pyproject.toml, so regressions in the 3.x API are caught
62
+ # before the SDK goes stable.
63
+ runs-on: ubuntu-latest
64
+ services:
65
+ postgres:
66
+ image: postgres:17
67
+ env:
68
+ POSTGRES_DB: lambda_tasks_test
69
+ POSTGRES_USER: postgres
70
+ POSTGRES_PASSWORD: postgres
71
+ ports:
72
+ - 5432:5432
73
+ options: >-
74
+ --health-cmd "pg_isready -U postgres"
75
+ --health-interval 10s
76
+ --health-timeout 5s
77
+ --health-retries 5
78
+ steps:
79
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
80
+ - name: Install uv and set the python version
81
+ uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
82
+ with:
83
+ python-version: '3.14'
84
+ - name: Run tests against the latest sentry-sdk 3.x pre-release
85
+ run: |
86
+ uv run --prerelease=allow --with "sentry-sdk>=3.0.0b1" \
87
+ pytest --cov-branch --cov-report term-missing --cov=tests/ --cov=lambda_tasks/
@@ -127,7 +127,7 @@ class SQSLambdaTaskMessage(BaseModel):
127
127
 
128
128
  ## Execution
129
129
 
130
- `SQSLambdaTaskMessage.execute_immediately(*, message_id: str)` in `models.py`:
130
+ `SQSLambdaTaskMessage.execute_immediately(*, message_id: str)` in `models.py` sets the `task_logger` `message_id` and enters `task_scope(...)` (Sentry transaction name + tags), then delegates the run to the private `_run(*, message_id)` method which:
131
131
  1. Checks for an existing `TaskRecord` with the same `pk` (`message_id`) via `get_or_create`
132
132
  2. If a record already exists (any status), logs and returns immediately — duplicate deliveries are silently skipped
133
133
  3. Resolves timeouts: decorator default → settings defaults (soft=270s, hard=300s), validated against queue max (900 for SQS, 3600 for Batch)
@@ -313,6 +313,32 @@ def my_task(*, user_id: int) -> None:
313
313
 
314
314
  `task_logger` is a `LoggerAdapter` wrapping the `lambda_tasks.task` logger. The executor sets the `message_id` before each task runs and clears it in a `finally` block. Using your own `logging.getLogger(__name__)` is fine — those records just won't carry the prefix.
315
315
 
316
+ ## Sentry
317
+
318
+ The library does not call `sentry_sdk.init(...)` — the host application owns Sentry setup (typically `DjangoIntegration` + `AwsLambdaIntegration`). The problem those integrations leave is attribution: on Lambda, `AwsLambdaIntegration` names every event's transaction after the Lambda function, so all tasks look alike; on AWS Batch there is no request and no Lambda integration, so the transaction/URL are `"--"`. Either way you cannot tell which task ran or raised.
319
+
320
+ `lambda_tasks.sentry.task_scope(*, task_name, message_id, n_retries)` fixes this. `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. Trace shape depends on whether a trace is already active:
321
+
322
+ - **Lambda** — `AwsLambdaIntegration` already started an invocation transaction, so the task is traced as a **child span** of it (not a second transaction). This is deliberate: a nested *transaction* per task inherited (rather than independently re-rolled) the invocation's sampling decision, doubling transaction volume and defeating `traces_sample_rate`. A child span is sent only if its parent is sampled, so sampling is respected. `_start_task_root` picks child-vs-root by checking `sentry_sdk.get_current_scope().transaction` (2.x only; on 3.0 `start_span` is automatically root-or-child).
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
328
+ - `lambda_task.message_id` = the `TaskRecord` primary key
329
+ - `lambda_task.n_retries` = the retry counter
330
+ - `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
+
332
+ `task_scope` deliberately does **not** call `scope.set_transaction_name(task_name)`: on Lambda that would rename the shared invocation transaction (fighting across sibling records in a batch) and, on SDK 3.0 stream mode, corrupt the root-segment naming. Error attribution relies on the `lambda_task` **tag** instead, which is reliable on both SDKs and independent of sampling.
333
+
334
+ **Reporting task failures:** the executor catches task exceptions (recording `FAILED` and not re-raising), so `lambda_tasks.sentry.capture_task_exception()` is called on the two **terminal** failure branches in `models.py` (plain failure and max-retries-exceeded) to report the error via `sentry_sdk.capture_exception()`. This is essential on 3.0, where a merely-caught exception produces no error event. The **retry** branch deliberately does not capture — transient, to-be-re-enqueued failures would flood Sentry; only terminal failures are reported.
335
+
336
+ **Error-level log forwarding:** on sentry-sdk 2.x, `LoggingIntegration` defaulted to `event_level=ERROR`, so `task_logger.error(...)` became Sentry events automatically. On 3.0 the default is `None`, so hosts that want error logs reported must configure `LoggingIntegration(event_level=logging.ERROR)` explicitly (the example app does this). This is a host-init concern, not something the library sets.
337
+
338
+ **SDK version compatibility:** `task_scope` 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
+ `task_scope` and `capture_task_exception` 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. No custom Sentry integration is needed — per-task tracing at the one execution entry point covers both Lambda and Batch.
341
+
316
342
  ## Built-in Tasks
317
343
 
318
344
  `lambda_tasks.tasks` provides maintenance tasks that ship with the library:
@@ -16,6 +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 + capture_task_exception — per-task trace, tags, failure reporting
19
20
  │ ├── models.py # TaskRecord, SQSLambdaTaskMessage, SQSLambdaTask
20
21
  │ ├── settings.py # LambdaTasksSettings, constants, queue type helpers
21
22
  │ ├── secret_loader.py # Resolves LAMBDA_TASKS_SECRET_* env vars at cold start
@@ -41,12 +42,13 @@ django-lambda-tasks/
41
42
  ## Module Responsibilities
42
43
 
43
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
44
- - `models.py` — `TaskRecord` (Django ORM), `SQSLambdaTaskMessage` (Pydantic, SQS schema + execution logic), `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`)
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`)
45
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
46
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`. 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()`
47
48
  - `environment_loader.py` — loads env vars from a Secrets Manager secret at cold start; validates flat JSON format; idempotent via `_loaded` sentinel
48
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
49
50
  - `logging.py` — `task_logger` singleton; `message_id` set/cleared around each task execution
51
+ - `sentry.py` — `task_scope` context manager + `capture_task_exception` helper. `task_scope` opens an isolated `new_scope` and traces the task: a **child span** of the invocation transaction on Lambda (where `AwsLambdaIntegration` already started one), or its **own root transaction** on Batch (nothing active). The child-span-on-Lambda behaviour avoids a second per-task transaction that 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 the shared invocation transaction and break 3.0 segment naming); error attribution rides on the `lambda_task` tag. `capture_task_exception` calls `sentry_sdk.capture_exception()` and is invoked on the terminal failure branches in `models.py` (plain failure + max-retries), since the executor catches exceptions without re-raising and 3.0 does not auto-report caught exceptions; the retry branch does not capture. 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`. Note: `logger.error → Sentry event` needs `LoggingIntegration(event_level=logging.ERROR)` in the host init on 3.0 (default on 2.x); a host concern, demonstrated in the example app
50
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
51
53
  - `admin.py` — Django admin registration for `TaskRecord`
52
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,6 +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`, `capture_task_exception`); verified against 2.74 and 3.0.0b4. Not a runtime dependency |
26
27
 
27
28
  ## Package Manager
28
29
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: django-lambda-tasks
3
- Version: 0.10.0
3
+ Version: 0.11.1
4
4
  Summary: Run async tasks in a lambda function
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: awslambdaric
@@ -444,6 +444,57 @@ fields @timestamp, @message
444
444
  | sort @timestamp asc
445
445
  ```
446
446
 
447
+ ### Sentry
448
+
449
+ If your application uses Sentry, task errors are automatically attributed to the task that raised them. Without this, Sentry's `AwsLambdaIntegration` names every event's transaction after the Lambda function (so all tasks look the same), and on AWS Batch there is no transaction at all (it shows as `"--"`) — so you cannot tell which task failed.
450
+
451
+ Keep your existing `sentry_sdk.init(...)`. On **sentry-sdk 2.x** no change is needed. On **sentry-sdk 3.0+** one option changed default and must be set explicitly if you want error-level logs reported (see below):
452
+
453
+ ```python
454
+ import logging
455
+ from sentry_sdk.integrations.logging import LoggingIntegration
456
+
457
+ sentry_sdk.init(
458
+ dsn=SENTRY_DSN,
459
+ integrations=[
460
+ 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
+ LoggingIntegration(event_level=logging.ERROR),
466
+ ],
467
+ traces_sample_rate=0.1,
468
+ )
469
+ ```
470
+
471
+ #### What the library does per task
472
+
473
+ When a task runs, `execute_immediately()` opens an isolated Sentry scope and traces the task:
474
+
475
+ - **On Lambda**, the task is traced as a **child span** of the invocation transaction that `AwsLambdaIntegration` already started — not a second transaction. This keeps task tracing from doubling your transaction volume and respects `traces_sample_rate`.
476
+ - **On Batch** (no Lambda integration, nothing active), the task starts **its own root transaction** so it is still traced.
477
+
478
+ Either way it records, as both span metadata and scope tags:
479
+
480
+ - `lambda_task` — the fully-qualified task name (e.g. `myapp.tasks.send_welcome_email`). This is also the task span's name.
481
+ - `lambda_task.message_id`, `lambda_task.n_retries`
482
+ - `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
+
484
+ Error attribution relies on the `lambda_task` **tag**, not on the transaction name, so it is reliable even when the trace is not sampled and regardless of whether the task ran as a child span (Lambda) or a root (Batch).
485
+
486
+ #### Task failures are reported
487
+
488
+ The executor catches a failing task's exception (recording a `FAILED` `TaskRecord`) rather than re-raising it. So the library calls `sentry_sdk.capture_exception()` itself on the **terminal failure paths** — a plain failure and max-retries-exceeded — to report the error to Sentry. This is essential on 3.0, where a merely-caught exception produces no error event at all.
489
+
490
+ Transient, retryable failures (a task that will be re-enqueued) are **not** reported, to avoid flooding Sentry on every retry; only the final, terminal failure is captured.
491
+
492
+ #### SDK version compatibility
493
+
494
+ The integration is future-proof across the sentry-sdk 2.x→3.0 boundary and verified against both 2.74 and 3.0.0b4. It uses `start_transaction`/`set_tag` on 2.x and `start_span`/`set_attribute` on 3.0 (stream mode), chosen automatically at runtime — no change needed when you upgrade.
495
+
496
+ `sentry_sdk` is not a dependency of this library — if it is not installed or not initialised, all of the above is a silent no-op.
497
+
447
498
  ---
448
499
 
449
500
  ## Automatic retries
@@ -432,6 +432,57 @@ fields @timestamp, @message
432
432
  | sort @timestamp asc
433
433
  ```
434
434
 
435
+ ### Sentry
436
+
437
+ If your application uses Sentry, task errors are automatically attributed to the task that raised them. Without this, Sentry's `AwsLambdaIntegration` names every event's transaction after the Lambda function (so all tasks look the same), and on AWS Batch there is no transaction at all (it shows as `"--"`) — so you cannot tell which task failed.
438
+
439
+ Keep your existing `sentry_sdk.init(...)`. On **sentry-sdk 2.x** no change is needed. On **sentry-sdk 3.0+** one option changed default and must be set explicitly if you want error-level logs reported (see below):
440
+
441
+ ```python
442
+ import logging
443
+ from sentry_sdk.integrations.logging import LoggingIntegration
444
+
445
+ sentry_sdk.init(
446
+ dsn=SENTRY_DSN,
447
+ integrations=[
448
+ 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
+ LoggingIntegration(event_level=logging.ERROR),
454
+ ],
455
+ traces_sample_rate=0.1,
456
+ )
457
+ ```
458
+
459
+ #### What the library does per task
460
+
461
+ When a task runs, `execute_immediately()` opens an isolated Sentry scope and traces the task:
462
+
463
+ - **On Lambda**, the task is traced as a **child span** of the invocation transaction that `AwsLambdaIntegration` already started — not a second transaction. This keeps task tracing from doubling your transaction volume and respects `traces_sample_rate`.
464
+ - **On Batch** (no Lambda integration, nothing active), the task starts **its own root transaction** so it is still traced.
465
+
466
+ Either way it records, as both span metadata and scope tags:
467
+
468
+ - `lambda_task` — the fully-qualified task name (e.g. `myapp.tasks.send_welcome_email`). This is also the task span's name.
469
+ - `lambda_task.message_id`, `lambda_task.n_retries`
470
+ - `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
+
472
+ Error attribution relies on the `lambda_task` **tag**, not on the transaction name, so it is reliable even when the trace is not sampled and regardless of whether the task ran as a child span (Lambda) or a root (Batch).
473
+
474
+ #### Task failures are reported
475
+
476
+ The executor catches a failing task's exception (recording a `FAILED` `TaskRecord`) rather than re-raising it. So the library calls `sentry_sdk.capture_exception()` itself on the **terminal failure paths** — a plain failure and max-retries-exceeded — to report the error to Sentry. This is essential on 3.0, where a merely-caught exception produces no error event at all.
477
+
478
+ Transient, retryable failures (a task that will be re-enqueued) are **not** reported, to avoid flooding Sentry on every retry; only the final, terminal failure is captured.
479
+
480
+ #### SDK version compatibility
481
+
482
+ The integration is future-proof across the sentry-sdk 2.x→3.0 boundary and verified against both 2.74 and 3.0.0b4. It uses `start_transaction`/`set_tag` on 2.x and `start_span`/`set_attribute` on 3.0 (stream mode), chosen automatically at runtime — no change needed when you upgrade.
483
+
484
+ `sentry_sdk` is not a dependency of this library — if it is not installed or not initialised, all of the above is a silent no-op.
485
+
435
486
  ---
436
487
 
437
488
  ## Automatic retries
@@ -0,0 +1,31 @@
1
+ from time import sleep
2
+
3
+ from lambda_tasks.decorators import lambda_task
4
+ from lambda_tasks.logging import task_logger
5
+
6
+
7
+ @lambda_task()
8
+ def greet(*, name: str) -> str:
9
+ """A successful task — produces one per-task Sentry transaction named
10
+ ``example_app.tasks.greet``."""
11
+ task_logger.info(f"Running greet with: {name=}")
12
+
13
+ for ii in range(3):
14
+ task_logger.info(f"Sleeping: {ii=}")
15
+ sleep(1)
16
+
17
+ try:
18
+ raise RuntimeError("testing")
19
+ except Exception:
20
+ task_logger.error("Raising", exc_info=True)
21
+
22
+ return f"Hello, {name}!"
23
+
24
+
25
+ @lambda_task()
26
+ def boom(*, name: str) -> str:
27
+ """A failing task — the executor records it as a FAILED TaskRecord and the
28
+ error is reported to Sentry attributed to ``example_app.tasks.boom`` (not
29
+ the Lambda function name), inside its own per-task trace."""
30
+ task_logger.info(f"Running boom with: {name=}")
31
+ raise RuntimeError(f"boom for {name}")
@@ -0,0 +1,7 @@
1
+ from django.urls import path
2
+ from example_app.views import trigger, trigger_error
3
+
4
+ urlpatterns = [
5
+ path("trigger/", trigger),
6
+ path("trigger-error/", trigger_error),
7
+ ]
@@ -0,0 +1,17 @@
1
+ from django.http import HttpRequest, HttpResponse
2
+ from example_app.tasks import boom, greet
3
+
4
+
5
+ def trigger(request: HttpRequest) -> HttpResponse:
6
+ """Enqueue the successful task — produces a per-task Sentry transaction."""
7
+ name = request.GET.get("name", "world")
8
+ greet.execute_on_commit(name=name)
9
+ return HttpResponse(f"greet task triggered for: {name}")
10
+
11
+
12
+ def trigger_error(request: HttpRequest) -> HttpResponse:
13
+ """Enqueue the failing task — produces a Sentry error attributed to the
14
+ task name, inside its own per-task trace."""
15
+ name = request.GET.get("name", "world")
16
+ boom.execute_on_commit(name=name)
17
+ return HttpResponse(f"boom task triggered for: {name}")
@@ -0,0 +1,50 @@
1
+ """Optional Sentry initialisation for the example app.
2
+
3
+ Demonstrates how a host application wires Sentry so that
4
+ ``django-lambda-tasks`` can attribute per-task traces and errors. This mirrors
5
+ a typical production ``sentry_sdk.init(...)`` (Django + AWS Lambda integrations
6
+ and tracing), but is driven entirely by environment variables so the example
7
+ runs with or without Sentry configured.
8
+
9
+ ``init_sentry()`` is called from ``settings.py`` at import time. Because the
10
+ local process-pool workers call ``django.setup()`` (which imports settings) in
11
+ each child process, this runs in the web process *and* in every task worker —
12
+ which is exactly what is needed for the per-task trace to be captured where the
13
+ task actually executes.
14
+ """
15
+
16
+ import logging
17
+ import os
18
+
19
+
20
+ def init_sentry() -> None:
21
+ """Initialise Sentry if ``SENTRY_DSN`` is set; otherwise do nothing.
22
+
23
+ Idempotent enough for the example's purposes: ``sentry_sdk.init`` is safe to
24
+ call once per process, and this is only invoked from settings import.
25
+ """
26
+ dsn = os.environ.get("SENTRY_DSN")
27
+ if not dsn:
28
+ return
29
+
30
+ import sentry_sdk
31
+ from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration
32
+ from sentry_sdk.integrations.django import DjangoIntegration
33
+ from sentry_sdk.integrations.logging import LoggingIntegration
34
+
35
+ sentry_sdk.init(
36
+ dsn=dsn,
37
+ integrations=[
38
+ DjangoIntegration(),
39
+ AwsLambdaIntegration(),
40
+ # On sentry-sdk 2.x the logging integration defaulted to
41
+ # event_level=ERROR, so logger.error(...) calls (e.g. task_logger)
42
+ # became Sentry events automatically. On 3.0 the default is None, so
43
+ # error logs no longer surface as events. Set it explicitly so
44
+ # task_logger.error output appears in Sentry on both SDK versions.
45
+ LoggingIntegration(event_level=logging.ERROR),
46
+ ],
47
+ # Sample every transaction so the per-task trace is always sent while
48
+ # you are testing. Lower this in a real deployment.
49
+ traces_sample_rate=float(os.environ.get("SENTRY_TRACES_SAMPLE_RATE", "1.0")),
50
+ )
@@ -88,3 +88,10 @@ LOGGING = {
88
88
  "level": "INFO",
89
89
  },
90
90
  }
91
+
92
+ # Optional Sentry integration — set SENTRY_DSN to enable. Initialised here (at
93
+ # settings import) so it also runs inside each local process-pool worker, which
94
+ # imports settings via django.setup(). See example_project/sentry.py.
95
+ from example_project.sentry import init_sentry # noqa: E402
96
+
97
+ init_sentry()