django-lambda-tasks 0.9.0__tar.gz → 0.9.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.9.0 → django_lambda_tasks-0.9.2}/.kiro/steering/product.md +2 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/steering/structure.md +1 -1
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/steering/tech.md +5 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/PKG-INFO +6 -1
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/lambda_tasks/handler.py +10 -7
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/lambda_tasks/tracing.py +72 -2
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/pyproject.toml +7 -1
- django_lambda_tasks-0.9.2/tests/test_tracing.py +539 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.github/dependabot.yml +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.github/workflows/ci.yml +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.github/workflows/release.yml +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.gitignore +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/async-local-execution/.config.kiro +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/async-local-execution/design.md +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/async-local-execution/requirements.md +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/async-local-execution/tasks.md +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/deferred-task-enqueue/.config.kiro +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/deferred-task-enqueue/design.md +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/deferred-task-enqueue/requirements.md +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/deferred-task-enqueue/tasks.md +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/eager-mode-example-app/.config.kiro +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/eager-mode-example-app/design.md +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/eager-mode-example-app/requirements.md +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/eager-mode-example-app/tasks.md +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/ignore-errors-decorator-option/.config.kiro +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/ignore-errors-decorator-option/design.md +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/ignore-errors-decorator-option/requirements.md +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/ignore-errors-decorator-option/tasks.md +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/import-string-task-resolution/.config.kiro +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/import-string-task-resolution/design.md +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/import-string-task-resolution/requirements.md +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/import-string-task-resolution/tasks.md +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/retry-delay/.config.kiro +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/retry-delay/design.md +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/retry-delay/requirements.md +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/retry-delay/tasks.md +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/rse-background-tasks/.config.kiro +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/rse-background-tasks/design.md +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/rse-background-tasks/requirements.md +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/rse-background-tasks/tasks.md +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/rse-background-tasks-bugfix/.config.kiro +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/rse-background-tasks-bugfix/bugfix.md +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/rse-background-tasks-bugfix/design.md +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/rse-background-tasks-bugfix/tasks.md +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/singleton-task/.config.kiro +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/singleton-task/design.md +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/singleton-task/requirements.md +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/singleton-task/tasks.md +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/ssm-environment-loader/.config.kiro +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/ssm-environment-loader/design.md +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/ssm-environment-loader/requirements.md +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/ssm-environment-loader/tasks.md +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/task-retry/.config.kiro +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/task-retry/design.md +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/task-retry/requirements.md +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/task-retry/tasks.md +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.pre-commit-config.yaml +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.vscode/settings.json +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/README.md +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/docker-compose.yml +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/example/README.md +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/example/docker-compose.yml +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/example/example_app/__init__.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/example/example_app/apps.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/example/example_app/tasks.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/example/example_app/urls.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/example/example_app/views.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/example/example_project/__init__.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/example/example_project/settings.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/example/example_project/urls.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/example/example_project/wsgi.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/example/manage.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/lambda_tasks/__init__.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/lambda_tasks/admin.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/lambda_tasks/apps.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/lambda_tasks/checks.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/lambda_tasks/decorators.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/lambda_tasks/environment_loader.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/lambda_tasks/local_executor.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/lambda_tasks/logging.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/lambda_tasks/migrations/0001_initial.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/lambda_tasks/migrations/0002_alter_taskrecord_status.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/lambda_tasks/migrations/0003_taskrecord_taskrecord_kwargs_trgm.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/lambda_tasks/migrations/0004_remove_taskrecord_taskrecord_kwargs_trgm_and_more.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/lambda_tasks/migrations/__init__.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/lambda_tasks/models.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/lambda_tasks/secret_loader.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/lambda_tasks/settings.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/lambda_tasks/tasks.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/lambda_tasks/timeouts.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/tests/conftest.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/tests/settings.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/tests/test_admin.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/tests/test_batch.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/tests/test_batch_worker.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/tests/test_checks.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/tests/test_decorator.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/tests/test_decorators.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/tests/test_deferred_enqueue.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/tests/test_environment_loader.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/tests/test_handler.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/tests/test_kwargs_only.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/tests/test_local_executor.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/tests/test_logging.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/tests/test_memory_limit.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/tests/test_models.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/tests/test_noop_execution.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/tests/test_secret_loader.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/tests/test_serializer.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/tests/test_settings.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/tests/test_tasks.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/tests/test_timeout_validation.py +0 -0
- {django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/tests/test_timeouts.py +0 -0
|
@@ -310,6 +310,8 @@ Behaviour:
|
|
|
310
310
|
- Builds a `TracerProvider` with an OTLP/HTTP span exporter via a `BatchSpanProcessor`. Spans go **directly** to the AWS X-Ray OTLP traces endpoint (`https://xray.<region>.amazonaws.com/v1/traces`) — no collector, no daemon, no Lambda layer.
|
|
311
311
|
- The exporter's `requests` session SigV4-signs each export with the ambient AWS credentials (task / execution role) via `botocore` — no API key to manage. Unsigned when no credentials are resolvable.
|
|
312
312
|
- Uses the AWS X-Ray id generator (`AwsXRayIdGenerator`) so the X-Ray backend accepts the trace IDs, and the X-Ray propagator (`AwsXRayLambdaPropagator` on Lambda, else `AwsXRayPropagator`). The X-Ray packages (`opentelemetry-sdk-extension-aws`, `opentelemetry-propagator-aws-xray`) are part of the `otel` extra.
|
|
313
|
+
- Library instrumentation: after the provider and propagator are installed, `configure_tracing()` calls `_instrument_libraries()`, which attaches the instrumentors for the libraries the tasks use — **psycopg 3** (SQL queries), **botocore** (every boto3 / aioboto3 call), **requests** and **httpx** (outbound HTTP), and **redis** (cache ops via `django_redis` → `redis-py`). Each instrumentor is wrapped independently and is best-effort: a missing or failing one is logged and skipped, never raised, so one broken package cannot stop the others or take down the worker. These are process-global and idempotent, and emit spans into the global provider (so it must exist first). Django request instrumentation (`DjangoInstrumentor`) is **not** installed here — a task has no Django request; the API (ECS) consumer installs that itself. sqlcommenter (`enable_commenter=True` on the psycopg instrumentor) is left off because it modifies the SQL text sent to PostgreSQL.
|
|
314
|
+
- Exporter self-trace suppression: the OTLP exporter POSTs spans over `requests` to the X-Ray endpoint, so the `requests` instrumentor is configured with `excluded_urls="xray\\..*\\.amazonaws\\.com"` to avoid tracing its own export calls (self-referential noise). The instrumentor also honours the `OTEL_PYTHON_REQUESTS_EXCLUDED_URLS` env var for per-environment overrides. botocore is not used by the exporter, so it needs no exclusion.
|
|
313
315
|
- The OpenTelemetry / `requests` imports are **deferred into the functions** (module scope imports only `os`), so the OTEL packages remain a genuinely optional extra: importing `lambda_tasks.handler` with them uninstalled is a no-op, not an `ImportError`. The disabled path (endpoint unset) never reaches any OTEL import.
|
|
314
316
|
- Lambda path: also calls `AwsLambdaInstrumentor().instrument()` (only when `AWS_LAMBDA_FUNCTION_NAME` is set). Batch path (`main()`): the instrumentor is skipped (no handler to wrap); the provider alone is configured.
|
|
315
317
|
- Flush on shutdown: `main()` calls `TracerProvider.force_flush()` / `shutdown()` before the one-shot process exits so short-lived Batch jobs do not drop buffered spans. The Lambda path needs no manual flush — `AwsLambdaInstrumentor` force-flushes per invocation.
|
|
@@ -45,7 +45,7 @@ django-lambda-tasks/
|
|
|
45
45
|
- `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`)
|
|
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
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`. At **import time** (before any invocation) it calls `configure_tracing()` and `instrument_lambda_handler()` from `tracing.py` so the Lambda instrumentor can wrap the handler before the runtime invokes it. 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, delegates to `handler()`, then force-flushes and shuts down the global `TracerProvider` (if any) before the one-shot process exits
|
|
48
|
-
- `tracing.py` — optional OpenTelemetry tracing, driven purely by standard OTEL env vars (no Django settings) so the package stays generic; no-op unless `OTEL_EXPORTER_OTLP_ENDPOINT` (or `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`) is set. All OpenTelemetry / `requests` imports are **deferred into the functions** (only `os` is imported at module scope) so the OTEL packages are a genuinely optional extra — importing `lambda_tasks.handler` without them is a no-op, not an `ImportError`. `configure_tracing()` installs a global `TracerProvider` using the X-Ray id generator (`AwsXRayIdGenerator`) with an OTLP/HTTP exporter whose `requests` session SigV4-signs each export to the AWS X-Ray traces endpoint (unsigned when no credentials resolve), and sets the X-Ray propagator (`AwsXRayLambdaPropagator` on Lambda, else `AwsXRayPropagator`). `instrument_lambda_handler()` calls `AwsLambdaInstrumentor().instrument()` only when tracing is enabled and `AWS_LAMBDA_FUNCTION_NAME` is set (Lambda path); skipped on the Batch path. `flush_tracing()` force-flushes and shuts down the provider (gated; used by the Batch entry point)
|
|
48
|
+
- `tracing.py` — optional OpenTelemetry tracing, driven purely by standard OTEL env vars (no Django settings) so the package stays generic; no-op unless `OTEL_EXPORTER_OTLP_ENDPOINT` (or `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`) is set. All OpenTelemetry / `requests` imports are **deferred into the functions** (only `os` is imported at module scope) so the OTEL packages are a genuinely optional extra — importing `lambda_tasks.handler` without them is a no-op, not an `ImportError`. `configure_tracing()` installs a global `TracerProvider` using the X-Ray id generator (`AwsXRayIdGenerator`) with an OTLP/HTTP exporter whose `requests` session SigV4-signs each export to the AWS X-Ray traces endpoint (unsigned when no credentials resolve), and sets the X-Ray propagator (`AwsXRayLambdaPropagator` on Lambda, else `AwsXRayPropagator`), then calls `_instrument_libraries()` to attach the library instrumentors (psycopg 3, botocore, requests, httpx, redis) — each wrapped independently and best-effort (a missing or failing instrumentor is logged, not raised), and the `requests` instrumentor is given an `excluded_urls` regex for the X-Ray OTLP endpoint so the exporter's own export POSTs are not self-traced. `instrument_lambda_handler()` calls `AwsLambdaInstrumentor().instrument()` only when tracing is enabled and `AWS_LAMBDA_FUNCTION_NAME` is set (Lambda path); skipped on the Batch path. `flush_tracing()` force-flushes and shuts down the provider (gated; used by the Batch entry point)
|
|
49
49
|
- `environment_loader.py` — loads env vars from a Secrets Manager secret at cold start; validates flat JSON format; idempotent via `_loaded` sentinel
|
|
50
50
|
- `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
|
|
51
51
|
- `logging.py` — `task_logger` singleton; `message_id` set/cleared around each task execution
|
|
@@ -28,6 +28,11 @@ Installed via the `otel` extra (`pip install django-lambda-tasks[otel]`). Enable
|
|
|
28
28
|
| `opentelemetry-instrumentation-aws-lambda` | Wraps the Lambda handler (`AwsLambdaInstrumentor`) |
|
|
29
29
|
| `opentelemetry-sdk-extension-aws` | X-Ray id generator (`AwsXRayIdGenerator`) |
|
|
30
30
|
| `opentelemetry-propagator-aws-xray` | X-Ray trace propagator |
|
|
31
|
+
| `opentelemetry-instrumentation-psycopg` | SQL query spans (psycopg 3) |
|
|
32
|
+
| `opentelemetry-instrumentation-botocore` | AWS SDK spans — every boto3 / aioboto3 call |
|
|
33
|
+
| `opentelemetry-instrumentation-requests` | Outbound HTTP spans via `requests` |
|
|
34
|
+
| `opentelemetry-instrumentation-httpx` | Outbound HTTP spans via `httpx` |
|
|
35
|
+
| `opentelemetry-instrumentation-redis` | Cache operation spans (`django_redis` → `redis-py`) |
|
|
31
36
|
| `requests` | OTLP/HTTP transport session (SigV4-signed via `botocore`) |
|
|
32
37
|
|
|
33
38
|
## Dev Dependencies
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: django-lambda-tasks
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.2
|
|
4
4
|
Summary: Run async tasks in a lambda function
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Requires-Dist: awslambdaric
|
|
@@ -11,6 +11,11 @@ Requires-Dist: redis
|
|
|
11
11
|
Provides-Extra: otel
|
|
12
12
|
Requires-Dist: opentelemetry-exporter-otlp-proto-http; extra == 'otel'
|
|
13
13
|
Requires-Dist: opentelemetry-instrumentation-aws-lambda; extra == 'otel'
|
|
14
|
+
Requires-Dist: opentelemetry-instrumentation-botocore; extra == 'otel'
|
|
15
|
+
Requires-Dist: opentelemetry-instrumentation-httpx; extra == 'otel'
|
|
16
|
+
Requires-Dist: opentelemetry-instrumentation-psycopg; extra == 'otel'
|
|
17
|
+
Requires-Dist: opentelemetry-instrumentation-redis; extra == 'otel'
|
|
18
|
+
Requires-Dist: opentelemetry-instrumentation-requests; extra == 'otel'
|
|
14
19
|
Requires-Dist: opentelemetry-propagator-aws-xray; extra == 'otel'
|
|
15
20
|
Requires-Dist: opentelemetry-sdk; extra == 'otel'
|
|
16
21
|
Requires-Dist: opentelemetry-sdk-extension-aws; extra == 'otel'
|
|
@@ -34,13 +34,6 @@ from lambda_tasks.tracing import (
|
|
|
34
34
|
|
|
35
35
|
logger = logging.getLogger(__name__)
|
|
36
36
|
|
|
37
|
-
# Initialise tracing at import time so the Lambda instrumentor can wrap the
|
|
38
|
-
# handler before the first invocation (which is earlier than the cold-start
|
|
39
|
-
# django.setup() inside handler()). No-op unless OTEL_EXPORTER_OTLP_ENDPOINT
|
|
40
|
-
# is set.
|
|
41
|
-
configure_tracing()
|
|
42
|
-
instrument_lambda_handler()
|
|
43
|
-
|
|
44
37
|
_cold_start_done: bool = False
|
|
45
38
|
|
|
46
39
|
|
|
@@ -191,5 +184,15 @@ def main() -> int:
|
|
|
191
184
|
return 0
|
|
192
185
|
|
|
193
186
|
|
|
187
|
+
# Initialise tracing at import time so the Lambda instrumentor can wrap the
|
|
188
|
+
# handler before the runtime invokes it (which is earlier than the cold-start
|
|
189
|
+
# django.setup() inside handler()). This MUST run after `handler` and `main`
|
|
190
|
+
# are defined: wrapt resolves the handler by attribute lookup on this module,
|
|
191
|
+
# and wrapping a name that does not exist yet raises AttributeError. No-op
|
|
192
|
+
# unless OTEL_EXPORTER_OTLP_ENDPOINT is set.
|
|
193
|
+
configure_tracing()
|
|
194
|
+
instrument_lambda_handler()
|
|
195
|
+
|
|
196
|
+
|
|
194
197
|
if __name__ == "__main__":
|
|
195
198
|
sys.exit(main())
|
|
@@ -25,12 +25,15 @@ import os
|
|
|
25
25
|
from typing import TYPE_CHECKING
|
|
26
26
|
|
|
27
27
|
if TYPE_CHECKING:
|
|
28
|
+
from collections.abc import Callable
|
|
29
|
+
|
|
28
30
|
import requests
|
|
29
31
|
from opentelemetry.sdk.trace import TracerProvider
|
|
30
32
|
|
|
31
33
|
logger = logging.getLogger(__name__)
|
|
32
34
|
|
|
33
35
|
_XRAY_SIGV4_SERVICE_NAME = "xray"
|
|
36
|
+
_LAMBDA_HANDLER = "lambda_tasks.handler.handler"
|
|
34
37
|
|
|
35
38
|
|
|
36
39
|
def tracing_is_enabled() -> bool:
|
|
@@ -149,6 +152,66 @@ def _set_xray_propagator() -> None:
|
|
|
149
152
|
)
|
|
150
153
|
|
|
151
154
|
|
|
155
|
+
def _instrument_libraries() -> None:
|
|
156
|
+
"""Instrument the libraries whose calls we want as spans.
|
|
157
|
+
|
|
158
|
+
SQL (psycopg 3), AWS SDK (botocore → boto3/aioboto3), outbound HTTP
|
|
159
|
+
(requests and httpx), and cache (redis). Each is wrapped independently and
|
|
160
|
+
is best-effort: a missing or failing instrumentor is logged, not raised, so
|
|
161
|
+
it cannot take down the worker. All are process-global and idempotent.
|
|
162
|
+
|
|
163
|
+
The OTLP span export itself goes out over ``requests`` to the X-Ray
|
|
164
|
+
endpoint; that URL is excluded from the requests instrumentor so the
|
|
165
|
+
exporter's own calls are not traced (which would be self-referential noise).
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
def _try(name: str, instrument: "Callable[[], None]") -> None:
|
|
169
|
+
try:
|
|
170
|
+
instrument()
|
|
171
|
+
except Exception:
|
|
172
|
+
logger.warning(
|
|
173
|
+
"Failed to instrument %s for tracing; continuing without it.",
|
|
174
|
+
name,
|
|
175
|
+
exc_info=True,
|
|
176
|
+
)
|
|
177
|
+
else:
|
|
178
|
+
logger.info("%s instrumentation installed.", name)
|
|
179
|
+
|
|
180
|
+
def _psycopg() -> None:
|
|
181
|
+
from opentelemetry.instrumentation.psycopg import PsycopgInstrumentor
|
|
182
|
+
|
|
183
|
+
PsycopgInstrumentor().instrument()
|
|
184
|
+
|
|
185
|
+
def _botocore() -> None:
|
|
186
|
+
from opentelemetry.instrumentation.botocore import BotocoreInstrumentor
|
|
187
|
+
|
|
188
|
+
BotocoreInstrumentor().instrument()
|
|
189
|
+
|
|
190
|
+
def _requests() -> None:
|
|
191
|
+
from opentelemetry.instrumentation.requests import RequestsInstrumentor
|
|
192
|
+
|
|
193
|
+
# Do not trace the exporter's own POST to the X-Ray OTLP endpoint.
|
|
194
|
+
RequestsInstrumentor().instrument(excluded_urls="xray\\..*\\.amazonaws\\.com")
|
|
195
|
+
|
|
196
|
+
def _httpx() -> None:
|
|
197
|
+
from opentelemetry.instrumentation.httpx import (
|
|
198
|
+
HTTPXClientInstrumentor,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
HTTPXClientInstrumentor().instrument()
|
|
202
|
+
|
|
203
|
+
def _redis() -> None:
|
|
204
|
+
from opentelemetry.instrumentation.redis import RedisInstrumentor
|
|
205
|
+
|
|
206
|
+
RedisInstrumentor().instrument()
|
|
207
|
+
|
|
208
|
+
_try("psycopg", _psycopg)
|
|
209
|
+
_try("botocore", _botocore)
|
|
210
|
+
_try("requests", _requests)
|
|
211
|
+
_try("httpx", _httpx)
|
|
212
|
+
_try("redis", _redis)
|
|
213
|
+
|
|
214
|
+
|
|
152
215
|
def configure_tracing() -> "TracerProvider | None":
|
|
153
216
|
"""Install the global tracer provider. No-op unless enabled.
|
|
154
217
|
|
|
@@ -177,6 +240,7 @@ def configure_tracing() -> "TracerProvider | None":
|
|
|
177
240
|
tracer_provider = _build_tracer_provider()
|
|
178
241
|
trace.set_tracer_provider(tracer_provider)
|
|
179
242
|
_set_xray_propagator()
|
|
243
|
+
_instrument_libraries()
|
|
180
244
|
except ImportError:
|
|
181
245
|
logger.warning(
|
|
182
246
|
"OTEL tracing is enabled but the OpenTelemetry packages are not "
|
|
@@ -199,8 +263,8 @@ def configure_tracing() -> "TracerProvider | None":
|
|
|
199
263
|
def instrument_lambda_handler() -> None:
|
|
200
264
|
"""Wrap the Lambda handler for tracing. No-op unless enabled.
|
|
201
265
|
|
|
202
|
-
Must be called at import time of the handler module,
|
|
203
|
-
invokes the handler.
|
|
266
|
+
Must be called at import time of the handler module, AFTER `handler` is
|
|
267
|
+
defined (see the handler module), before the runtime invokes the handler.
|
|
204
268
|
"""
|
|
205
269
|
if not tracing_is_enabled():
|
|
206
270
|
return
|
|
@@ -212,6 +276,12 @@ def instrument_lambda_handler() -> None:
|
|
|
212
276
|
)
|
|
213
277
|
return
|
|
214
278
|
|
|
279
|
+
# AwsLambdaInstrumentor derives the handler to wrap from
|
|
280
|
+
# ORIG_HANDLER / _HANDLER. Container images launched via awslambdaric pass
|
|
281
|
+
# the handler as a CMD arg, which is not exported as _HANDLER, so default
|
|
282
|
+
# it here. setdefault() leaves any explicitly configured value untouched.
|
|
283
|
+
os.environ.setdefault("ORIG_HANDLER", _LAMBDA_HANDLER)
|
|
284
|
+
|
|
215
285
|
try:
|
|
216
286
|
from opentelemetry.instrumentation.aws_lambda import (
|
|
217
287
|
AwsLambdaInstrumentor,
|
|
@@ -7,7 +7,7 @@ packages = ["lambda_tasks"]
|
|
|
7
7
|
|
|
8
8
|
[project]
|
|
9
9
|
name = "django-lambda-tasks"
|
|
10
|
-
version = "0.9.
|
|
10
|
+
version = "0.9.2"
|
|
11
11
|
description = "Run async tasks in a lambda function"
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
requires-python = ">=3.11"
|
|
@@ -27,6 +27,11 @@ otel = [
|
|
|
27
27
|
"opentelemetry-instrumentation-aws-lambda",
|
|
28
28
|
"opentelemetry-sdk-extension-aws",
|
|
29
29
|
"opentelemetry-propagator-aws-xray",
|
|
30
|
+
"opentelemetry-instrumentation-psycopg",
|
|
31
|
+
"opentelemetry-instrumentation-botocore",
|
|
32
|
+
"opentelemetry-instrumentation-requests",
|
|
33
|
+
"opentelemetry-instrumentation-httpx",
|
|
34
|
+
"opentelemetry-instrumentation-redis",
|
|
30
35
|
]
|
|
31
36
|
|
|
32
37
|
[dependency-groups]
|
|
@@ -41,6 +46,7 @@ dev = [
|
|
|
41
46
|
"pytest-cov",
|
|
42
47
|
"ty",
|
|
43
48
|
"psycopg",
|
|
49
|
+
"httpx",
|
|
44
50
|
"django-lambda-tasks[otel]",
|
|
45
51
|
]
|
|
46
52
|
|
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
"""Tests for lambda_tasks.tracing.
|
|
2
|
+
|
|
3
|
+
These paths are driven entirely by standard OTEL environment variables and do
|
|
4
|
+
not read Django settings, so environment is manipulated with monkeypatch and
|
|
5
|
+
override_settings is never used.
|
|
6
|
+
|
|
7
|
+
Each test resets the global tracer provider so installation is observable and
|
|
8
|
+
isolated. The OTEL SDK forbids overwriting a real TracerProvider once set, so
|
|
9
|
+
the suite swaps in a fresh default proxy provider before each test.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import builtins
|
|
13
|
+
import os
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
from unittest.mock import patch
|
|
17
|
+
|
|
18
|
+
import pytest
|
|
19
|
+
from opentelemetry import trace
|
|
20
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
21
|
+
|
|
22
|
+
from lambda_tasks import tracing
|
|
23
|
+
|
|
24
|
+
_ENDPOINT = "https://xray.us-east-1.amazonaws.com/v1/traces"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.fixture(autouse=True)
|
|
28
|
+
def _reset_global_tracer_provider():
|
|
29
|
+
"""Reset OTEL global state so each test starts from a clean default provider."""
|
|
30
|
+
import opentelemetry.trace as trace_module
|
|
31
|
+
|
|
32
|
+
original_provider = trace_module._TRACER_PROVIDER
|
|
33
|
+
original_set = trace_module._TRACER_PROVIDER_SET_ONCE
|
|
34
|
+
trace_module._TRACER_PROVIDER = None
|
|
35
|
+
trace_module._TRACER_PROVIDER_SET_ONCE = trace_module.Once()
|
|
36
|
+
try:
|
|
37
|
+
yield
|
|
38
|
+
finally:
|
|
39
|
+
trace_module._TRACER_PROVIDER = original_provider
|
|
40
|
+
trace_module._TRACER_PROVIDER_SET_ONCE = original_set
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _clear_otel_env(monkeypatch):
|
|
44
|
+
monkeypatch.delenv("OTEL_EXPORTER_OTLP_ENDPOINT", raising=False)
|
|
45
|
+
monkeypatch.delenv("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", raising=False)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _library_instrumentors():
|
|
49
|
+
"""The five instrumentor instances configure_tracing() should install."""
|
|
50
|
+
from opentelemetry.instrumentation.botocore import BotocoreInstrumentor
|
|
51
|
+
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
|
|
52
|
+
from opentelemetry.instrumentation.psycopg import PsycopgInstrumentor
|
|
53
|
+
from opentelemetry.instrumentation.redis import RedisInstrumentor
|
|
54
|
+
from opentelemetry.instrumentation.requests import RequestsInstrumentor
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
"Psycopg": PsycopgInstrumentor(),
|
|
58
|
+
"Botocore": BotocoreInstrumentor(),
|
|
59
|
+
"Requests": RequestsInstrumentor(),
|
|
60
|
+
"HTTPXClient": HTTPXClientInstrumentor(),
|
|
61
|
+
"Redis": RedisInstrumentor(),
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@pytest.fixture
|
|
66
|
+
def _uninstrument_libraries():
|
|
67
|
+
"""Uninstrument the library instrumentors after a test.
|
|
68
|
+
|
|
69
|
+
The instrumentors are process-global singletons; leaving them instrumented
|
|
70
|
+
would leak into other tests. Uninstrument before and after so the test
|
|
71
|
+
starts from a clean slate regardless of ordering.
|
|
72
|
+
"""
|
|
73
|
+
instrumentors = _library_instrumentors()
|
|
74
|
+
for instrumentor in instrumentors.values():
|
|
75
|
+
if instrumentor.is_instrumented_by_opentelemetry:
|
|
76
|
+
instrumentor.uninstrument()
|
|
77
|
+
try:
|
|
78
|
+
yield instrumentors
|
|
79
|
+
finally:
|
|
80
|
+
for instrumentor in instrumentors.values():
|
|
81
|
+
if instrumentor.is_instrumented_by_opentelemetry:
|
|
82
|
+
instrumentor.uninstrument()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class TestTracingIsEnabled:
|
|
86
|
+
def test_disabled_when_no_endpoint(self, monkeypatch):
|
|
87
|
+
_clear_otel_env(monkeypatch)
|
|
88
|
+
assert tracing.tracing_is_enabled() is False
|
|
89
|
+
|
|
90
|
+
def test_enabled_by_base_endpoint(self, monkeypatch):
|
|
91
|
+
_clear_otel_env(monkeypatch)
|
|
92
|
+
monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", _ENDPOINT)
|
|
93
|
+
assert tracing.tracing_is_enabled() is True
|
|
94
|
+
|
|
95
|
+
def test_enabled_by_per_signal_endpoint(self, monkeypatch):
|
|
96
|
+
_clear_otel_env(monkeypatch)
|
|
97
|
+
monkeypatch.setenv("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", _ENDPOINT)
|
|
98
|
+
assert tracing.tracing_is_enabled() is True
|
|
99
|
+
|
|
100
|
+
def test_disabled_when_endpoint_blank(self, monkeypatch):
|
|
101
|
+
_clear_otel_env(monkeypatch)
|
|
102
|
+
monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", " ")
|
|
103
|
+
assert tracing.tracing_is_enabled() is False
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class TestConfigureTracing:
|
|
107
|
+
def test_noop_when_disabled(self, monkeypatch):
|
|
108
|
+
_clear_otel_env(monkeypatch)
|
|
109
|
+
result = tracing.configure_tracing()
|
|
110
|
+
assert result is None
|
|
111
|
+
assert not isinstance(trace.get_tracer_provider(), TracerProvider)
|
|
112
|
+
|
|
113
|
+
def test_installs_provider_when_enabled(self, monkeypatch):
|
|
114
|
+
_clear_otel_env(monkeypatch)
|
|
115
|
+
monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", _ENDPOINT)
|
|
116
|
+
monkeypatch.setenv("AWS_REGION", "us-east-1")
|
|
117
|
+
|
|
118
|
+
result = tracing.configure_tracing()
|
|
119
|
+
|
|
120
|
+
assert isinstance(result, TracerProvider)
|
|
121
|
+
assert trace.get_tracer_provider() is result
|
|
122
|
+
|
|
123
|
+
def test_idempotent_on_second_call(self, monkeypatch):
|
|
124
|
+
_clear_otel_env(monkeypatch)
|
|
125
|
+
monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", _ENDPOINT)
|
|
126
|
+
monkeypatch.setenv("AWS_REGION", "us-east-1")
|
|
127
|
+
|
|
128
|
+
first = tracing.configure_tracing()
|
|
129
|
+
second = tracing.configure_tracing()
|
|
130
|
+
|
|
131
|
+
assert isinstance(first, TracerProvider)
|
|
132
|
+
assert second is None
|
|
133
|
+
assert trace.get_tracer_provider() is first
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class TestInstrumentLibraries:
|
|
137
|
+
def test_instruments_all_libraries_when_enabled(
|
|
138
|
+
self, monkeypatch, _uninstrument_libraries
|
|
139
|
+
):
|
|
140
|
+
_clear_otel_env(monkeypatch)
|
|
141
|
+
monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", _ENDPOINT)
|
|
142
|
+
monkeypatch.setenv("AWS_REGION", "us-east-1")
|
|
143
|
+
|
|
144
|
+
tracing.configure_tracing()
|
|
145
|
+
|
|
146
|
+
for name, instrumentor in _uninstrument_libraries.items():
|
|
147
|
+
assert instrumentor.is_instrumented_by_opentelemetry is True, name
|
|
148
|
+
|
|
149
|
+
def test_noop_when_disabled(self, monkeypatch, _uninstrument_libraries):
|
|
150
|
+
_clear_otel_env(monkeypatch)
|
|
151
|
+
|
|
152
|
+
result = tracing.configure_tracing()
|
|
153
|
+
|
|
154
|
+
assert result is None
|
|
155
|
+
for name, instrumentor in _uninstrument_libraries.items():
|
|
156
|
+
assert instrumentor.is_instrumented_by_opentelemetry is False, name
|
|
157
|
+
|
|
158
|
+
def test_one_failing_instrumentor_does_not_block_the_others(
|
|
159
|
+
self, monkeypatch, caplog, _uninstrument_libraries
|
|
160
|
+
):
|
|
161
|
+
_clear_otel_env(monkeypatch)
|
|
162
|
+
monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", _ENDPOINT)
|
|
163
|
+
monkeypatch.setenv("AWS_REGION", "us-east-1")
|
|
164
|
+
|
|
165
|
+
from opentelemetry.instrumentation.botocore import BotocoreInstrumentor
|
|
166
|
+
|
|
167
|
+
def _boom(self, **kwargs):
|
|
168
|
+
raise RuntimeError("botocore instrumentation failed")
|
|
169
|
+
|
|
170
|
+
monkeypatch.setattr(BotocoreInstrumentor, "_instrument", _boom)
|
|
171
|
+
|
|
172
|
+
with caplog.at_level("WARNING", logger="lambda_tasks.tracing"):
|
|
173
|
+
tracing.configure_tracing()
|
|
174
|
+
|
|
175
|
+
instrumentors = _uninstrument_libraries
|
|
176
|
+
assert instrumentors["Botocore"].is_instrumented_by_opentelemetry is False
|
|
177
|
+
for name in ("Psycopg", "Requests", "HTTPXClient", "Redis"):
|
|
178
|
+
assert instrumentors[name].is_instrumented_by_opentelemetry is True, name
|
|
179
|
+
|
|
180
|
+
assert any(
|
|
181
|
+
"Failed to instrument botocore" in record.message
|
|
182
|
+
for record in caplog.records
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def test_requests_instrumentor_excludes_xray_endpoint(
|
|
186
|
+
self, monkeypatch, _uninstrument_libraries
|
|
187
|
+
):
|
|
188
|
+
"""A POST to the X-Ray OTLP endpoint must produce no span.
|
|
189
|
+
|
|
190
|
+
The requests instrumentor is configured with an excluded_urls regex
|
|
191
|
+
matching the X-Ray host so the exporter's own export calls are not
|
|
192
|
+
traced (self-referential noise).
|
|
193
|
+
"""
|
|
194
|
+
_clear_otel_env(monkeypatch)
|
|
195
|
+
monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", _ENDPOINT)
|
|
196
|
+
monkeypatch.setenv("AWS_REGION", "us-east-1")
|
|
197
|
+
|
|
198
|
+
import requests
|
|
199
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
200
|
+
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
|
|
201
|
+
from opentelemetry.sdk.trace.export.in_memory_span_exporter import (
|
|
202
|
+
InMemorySpanExporter,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
exporter = InMemorySpanExporter()
|
|
206
|
+
provider = TracerProvider()
|
|
207
|
+
provider.add_span_processor(SimpleSpanProcessor(exporter))
|
|
208
|
+
|
|
209
|
+
with patch.object(tracing, "_build_tracer_provider", return_value=provider):
|
|
210
|
+
tracing.configure_tracing()
|
|
211
|
+
|
|
212
|
+
# Issue a request to the excluded X-Ray endpoint; the underlying
|
|
213
|
+
# socket is patched so no real network call is made. The instrumentor
|
|
214
|
+
# decides whether to create a span before the transport runs.
|
|
215
|
+
with patch.object(
|
|
216
|
+
requests.adapters.HTTPAdapter,
|
|
217
|
+
"send",
|
|
218
|
+
side_effect=requests.exceptions.ConnectionError("blocked"),
|
|
219
|
+
):
|
|
220
|
+
with pytest.raises(requests.exceptions.ConnectionError):
|
|
221
|
+
requests.post(_ENDPOINT, data=b"body")
|
|
222
|
+
|
|
223
|
+
assert exporter.get_finished_spans() == ()
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class TestInstrumentLambdaHandler:
|
|
227
|
+
def test_noop_off_lambda(self, monkeypatch):
|
|
228
|
+
_clear_otel_env(monkeypatch)
|
|
229
|
+
monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", _ENDPOINT)
|
|
230
|
+
monkeypatch.delenv("AWS_LAMBDA_FUNCTION_NAME", raising=False)
|
|
231
|
+
|
|
232
|
+
from opentelemetry.instrumentation.aws_lambda import AwsLambdaInstrumentor
|
|
233
|
+
|
|
234
|
+
instrumentor = AwsLambdaInstrumentor()
|
|
235
|
+
assert instrumentor.is_instrumented_by_opentelemetry is False
|
|
236
|
+
tracing.instrument_lambda_handler()
|
|
237
|
+
assert instrumentor.is_instrumented_by_opentelemetry is False
|
|
238
|
+
|
|
239
|
+
def test_noop_when_disabled(self, monkeypatch):
|
|
240
|
+
_clear_otel_env(monkeypatch)
|
|
241
|
+
monkeypatch.setenv("AWS_LAMBDA_FUNCTION_NAME", "my-fn")
|
|
242
|
+
|
|
243
|
+
from opentelemetry.instrumentation.aws_lambda import AwsLambdaInstrumentor
|
|
244
|
+
|
|
245
|
+
instrumentor = AwsLambdaInstrumentor()
|
|
246
|
+
assert instrumentor.is_instrumented_by_opentelemetry is False
|
|
247
|
+
tracing.instrument_lambda_handler()
|
|
248
|
+
assert instrumentor.is_instrumented_by_opentelemetry is False
|
|
249
|
+
|
|
250
|
+
def test_instruments_on_lambda_when_enabled(self, monkeypatch):
|
|
251
|
+
_clear_otel_env(monkeypatch)
|
|
252
|
+
monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", _ENDPOINT)
|
|
253
|
+
monkeypatch.setenv("AWS_LAMBDA_FUNCTION_NAME", "my-fn")
|
|
254
|
+
monkeypatch.setenv("ORIG_HANDLER", "lambda_tasks.handler.handler")
|
|
255
|
+
|
|
256
|
+
from opentelemetry.instrumentation.aws_lambda import AwsLambdaInstrumentor
|
|
257
|
+
|
|
258
|
+
instrumentor = AwsLambdaInstrumentor()
|
|
259
|
+
try:
|
|
260
|
+
tracing.instrument_lambda_handler()
|
|
261
|
+
assert instrumentor.is_instrumented_by_opentelemetry is True
|
|
262
|
+
finally:
|
|
263
|
+
if instrumentor.is_instrumented_by_opentelemetry:
|
|
264
|
+
instrumentor.uninstrument()
|
|
265
|
+
|
|
266
|
+
def test_defaults_orig_handler_when_unset(self, monkeypatch):
|
|
267
|
+
"""With ORIG_HANDLER unset the package defaults it to its own handler
|
|
268
|
+
so container-image deployments need not set the env var."""
|
|
269
|
+
_clear_otel_env(monkeypatch)
|
|
270
|
+
monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", _ENDPOINT)
|
|
271
|
+
monkeypatch.setenv("AWS_LAMBDA_FUNCTION_NAME", "my-fn")
|
|
272
|
+
monkeypatch.delenv("ORIG_HANDLER", raising=False)
|
|
273
|
+
monkeypatch.delenv("_HANDLER", raising=False)
|
|
274
|
+
|
|
275
|
+
from opentelemetry.instrumentation.aws_lambda import AwsLambdaInstrumentor
|
|
276
|
+
|
|
277
|
+
instrumentor = AwsLambdaInstrumentor()
|
|
278
|
+
try:
|
|
279
|
+
tracing.instrument_lambda_handler()
|
|
280
|
+
assert os.environ["ORIG_HANDLER"] == "lambda_tasks.handler.handler"
|
|
281
|
+
finally:
|
|
282
|
+
if instrumentor.is_instrumented_by_opentelemetry:
|
|
283
|
+
instrumentor.uninstrument()
|
|
284
|
+
|
|
285
|
+
def test_leaves_explicit_orig_handler_unchanged(self, monkeypatch):
|
|
286
|
+
"""An explicitly configured ORIG_HANDLER wins — setdefault must not
|
|
287
|
+
overwrite a consumer that invokes the handler at a different path."""
|
|
288
|
+
_clear_otel_env(monkeypatch)
|
|
289
|
+
monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", _ENDPOINT)
|
|
290
|
+
monkeypatch.setenv("AWS_LAMBDA_FUNCTION_NAME", "my-fn")
|
|
291
|
+
monkeypatch.setenv("ORIG_HANDLER", "custom.module.my_handler")
|
|
292
|
+
|
|
293
|
+
from opentelemetry.instrumentation.aws_lambda import AwsLambdaInstrumentor
|
|
294
|
+
|
|
295
|
+
instrumentor = AwsLambdaInstrumentor()
|
|
296
|
+
try:
|
|
297
|
+
tracing.instrument_lambda_handler()
|
|
298
|
+
assert os.environ["ORIG_HANDLER"] == "custom.module.my_handler"
|
|
299
|
+
finally:
|
|
300
|
+
if instrumentor.is_instrumented_by_opentelemetry:
|
|
301
|
+
instrumentor.uninstrument()
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
class TestOptionalDependency:
|
|
305
|
+
"""The OTEL packages are a genuinely optional extra.
|
|
306
|
+
|
|
307
|
+
Importing lambda_tasks.handler / lambda_tasks.tracing must not require
|
|
308
|
+
opentelemetry or requests to be installed, and the disabled no-op path
|
|
309
|
+
must never reach those imports.
|
|
310
|
+
"""
|
|
311
|
+
|
|
312
|
+
@staticmethod
|
|
313
|
+
def _block_otel_and_requests():
|
|
314
|
+
"""Return an __import__ replacement that hides OTEL and requests."""
|
|
315
|
+
real_import = builtins.__import__
|
|
316
|
+
|
|
317
|
+
def fake_import(name, *args, **kwargs):
|
|
318
|
+
if name.split(".")[0] in ("opentelemetry", "requests"):
|
|
319
|
+
raise ModuleNotFoundError(f"No module named {name!r}")
|
|
320
|
+
return real_import(name, *args, **kwargs)
|
|
321
|
+
|
|
322
|
+
return fake_import
|
|
323
|
+
|
|
324
|
+
def test_handler_imports_without_otel_installed(self):
|
|
325
|
+
"""lambda_tasks.handler imports cleanly with OTEL/requests absent.
|
|
326
|
+
|
|
327
|
+
Run in a subprocess so blocking the imports cannot perturb module
|
|
328
|
+
singletons the rest of the suite relies on.
|
|
329
|
+
"""
|
|
330
|
+
program = (
|
|
331
|
+
"import builtins\n"
|
|
332
|
+
"_real = builtins.__import__\n"
|
|
333
|
+
"def _fake(name, *a, **k):\n"
|
|
334
|
+
" if name.split('.')[0] in ('opentelemetry', 'requests'):\n"
|
|
335
|
+
" raise ModuleNotFoundError('No module named ' + name)\n"
|
|
336
|
+
" return _real(name, *a, **k)\n"
|
|
337
|
+
"builtins.__import__ = _fake\n"
|
|
338
|
+
"import os\n"
|
|
339
|
+
"os.environ.pop('OTEL_EXPORTER_OTLP_ENDPOINT', None)\n"
|
|
340
|
+
"os.environ.pop('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT', None)\n"
|
|
341
|
+
"import lambda_tasks.handler as h\n"
|
|
342
|
+
"assert h.configure_tracing() is None\n"
|
|
343
|
+
"assert h.instrument_lambda_handler() is None\n"
|
|
344
|
+
"assert h.flush_tracing() is None\n"
|
|
345
|
+
"print('ok')\n"
|
|
346
|
+
)
|
|
347
|
+
result = subprocess.run(
|
|
348
|
+
[sys.executable, "-c", program],
|
|
349
|
+
capture_output=True,
|
|
350
|
+
text=True,
|
|
351
|
+
env={**os.environ, "DJANGO_SETTINGS_MODULE": ""},
|
|
352
|
+
)
|
|
353
|
+
assert result.returncode == 0, result.stderr
|
|
354
|
+
assert "ok" in result.stdout
|
|
355
|
+
|
|
356
|
+
def test_disabled_entry_points_do_not_import_otel(self, monkeypatch):
|
|
357
|
+
"""With tracing disabled, configure/instrument are no-ops that never
|
|
358
|
+
touch opentelemetry — so they work with the packages absent."""
|
|
359
|
+
_clear_otel_env(monkeypatch)
|
|
360
|
+
monkeypatch.delenv("AWS_LAMBDA_FUNCTION_NAME", raising=False)
|
|
361
|
+
monkeypatch.setattr(builtins, "__import__", self._block_otel_and_requests())
|
|
362
|
+
|
|
363
|
+
assert tracing.tracing_is_enabled() is False
|
|
364
|
+
assert tracing.configure_tracing() is None
|
|
365
|
+
assert tracing.instrument_lambda_handler() is None
|
|
366
|
+
assert tracing.flush_tracing() is None
|
|
367
|
+
|
|
368
|
+
def test_enabled_but_otel_missing_logs_and_does_not_raise(
|
|
369
|
+
self, monkeypatch, caplog
|
|
370
|
+
):
|
|
371
|
+
"""configure_tracing() is enabled but OTEL is absent → logged no-op,
|
|
372
|
+
not an ImportError that would crash handler import."""
|
|
373
|
+
_clear_otel_env(monkeypatch)
|
|
374
|
+
monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", _ENDPOINT)
|
|
375
|
+
monkeypatch.setattr(builtins, "__import__", self._block_otel_and_requests())
|
|
376
|
+
|
|
377
|
+
with caplog.at_level("WARNING", logger="lambda_tasks.tracing"):
|
|
378
|
+
result = tracing.configure_tracing()
|
|
379
|
+
|
|
380
|
+
assert result is None
|
|
381
|
+
assert any(
|
|
382
|
+
"OpenTelemetry packages are not installed" in record.message
|
|
383
|
+
for record in caplog.records
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
class TestLoggingDiagnostics:
|
|
388
|
+
def test_missing_credentials_logs_warning(self, monkeypatch, caplog):
|
|
389
|
+
"""A span export with no resolvable AWS credentials logs a warning
|
|
390
|
+
(otherwise the unsigned request is silently rejected by X-Ray)."""
|
|
391
|
+
monkeypatch.setenv("AWS_REGION", "us-east-1")
|
|
392
|
+
|
|
393
|
+
session = tracing._build_signing_session()
|
|
394
|
+
|
|
395
|
+
prepared = (
|
|
396
|
+
__import__("requests")
|
|
397
|
+
.Request(method="POST", url=_ENDPOINT, data=b"body")
|
|
398
|
+
.prepare()
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
with (
|
|
402
|
+
patch(
|
|
403
|
+
"botocore.session.Session.get_credentials",
|
|
404
|
+
return_value=None,
|
|
405
|
+
),
|
|
406
|
+
caplog.at_level("WARNING", logger="lambda_tasks.tracing"),
|
|
407
|
+
):
|
|
408
|
+
session.auth(prepared)
|
|
409
|
+
|
|
410
|
+
assert any("unsigned" in record.message for record in caplog.records)
|
|
411
|
+
|
|
412
|
+
def test_missing_region_logs_warning(self, monkeypatch, caplog):
|
|
413
|
+
"""Building the signing session with no region resolvable logs a warning."""
|
|
414
|
+
monkeypatch.delenv("AWS_REGION", raising=False)
|
|
415
|
+
monkeypatch.delenv("AWS_DEFAULT_REGION", raising=False)
|
|
416
|
+
|
|
417
|
+
with caplog.at_level("WARNING", logger="lambda_tasks.tracing"):
|
|
418
|
+
tracing._build_signing_session()
|
|
419
|
+
|
|
420
|
+
assert any(
|
|
421
|
+
"No AWS region resolvable" in record.message for record in caplog.records
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
class TestHandlerImportOrderingRegression:
|
|
426
|
+
"""Regression test for the import-time ordering bug.
|
|
427
|
+
|
|
428
|
+
`instrument_lambda_handler()` must run AFTER `def handler` executes, because
|
|
429
|
+
AwsLambdaInstrumentor wraps the handler via wrapt attribute lookup on the
|
|
430
|
+
module. When the setup calls sat at the top of the module (before the def),
|
|
431
|
+
the lookup raised AttributeError, the best-effort wrapper swallowed it and
|
|
432
|
+
logged "Failed to instrument the Lambda handler...", and the handler was
|
|
433
|
+
never wrapped — so no SERVER span was produced.
|
|
434
|
+
|
|
435
|
+
Run in a subprocess so the fresh, fully-instrumented import cannot perturb
|
|
436
|
+
the module singletons the rest of the suite relies on.
|
|
437
|
+
"""
|
|
438
|
+
|
|
439
|
+
def test_fresh_import_wraps_handler_without_warning(self):
|
|
440
|
+
program = (
|
|
441
|
+
"import logging\n"
|
|
442
|
+
"records = []\n"
|
|
443
|
+
"class _Capture(logging.Handler):\n"
|
|
444
|
+
" def emit(self, record):\n"
|
|
445
|
+
" records.append(record.getMessage())\n"
|
|
446
|
+
"logging.getLogger('lambda_tasks.tracing').addHandler(_Capture())\n"
|
|
447
|
+
"logging.getLogger('lambda_tasks.tracing').setLevel(logging.DEBUG)\n"
|
|
448
|
+
"import lambda_tasks.handler as h\n"
|
|
449
|
+
"from opentelemetry.instrumentation.aws_lambda import "
|
|
450
|
+
"AwsLambdaInstrumentor\n"
|
|
451
|
+
"assert AwsLambdaInstrumentor().is_instrumented_by_opentelemetry, "
|
|
452
|
+
"'handler was not wrapped'\n"
|
|
453
|
+
"assert not any('Failed to instrument the Lambda handler' in m "
|
|
454
|
+
"for m in records), records\n"
|
|
455
|
+
"print('ok')\n"
|
|
456
|
+
)
|
|
457
|
+
result = subprocess.run(
|
|
458
|
+
[sys.executable, "-c", program],
|
|
459
|
+
capture_output=True,
|
|
460
|
+
text=True,
|
|
461
|
+
env={
|
|
462
|
+
**os.environ,
|
|
463
|
+
"OTEL_EXPORTER_OTLP_ENDPOINT": _ENDPOINT,
|
|
464
|
+
"AWS_LAMBDA_FUNCTION_NAME": "my-fn",
|
|
465
|
+
"AWS_REGION": "us-east-1",
|
|
466
|
+
"ORIG_HANDLER": "lambda_tasks.handler.handler",
|
|
467
|
+
"DJANGO_SETTINGS_MODULE": "",
|
|
468
|
+
},
|
|
469
|
+
)
|
|
470
|
+
assert result.returncode == 0, result.stderr
|
|
471
|
+
assert "ok" in result.stdout
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
class TestHandlerSpanParenting:
|
|
475
|
+
"""An invocation of the wrapped handler produces a SERVER span whose trace
|
|
476
|
+
contains the task's inner spans as descendants (single connected trace)."""
|
|
477
|
+
|
|
478
|
+
def test_inner_span_descends_from_handler_server_span(self, monkeypatch):
|
|
479
|
+
_clear_otel_env(monkeypatch)
|
|
480
|
+
monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", _ENDPOINT)
|
|
481
|
+
monkeypatch.setenv("AWS_LAMBDA_FUNCTION_NAME", "my-fn")
|
|
482
|
+
monkeypatch.setenv("AWS_REGION", "us-east-1")
|
|
483
|
+
monkeypatch.setenv("ORIG_HANDLER", "lambda_tasks.handler.handler")
|
|
484
|
+
|
|
485
|
+
from opentelemetry import trace as trace_api
|
|
486
|
+
from opentelemetry.instrumentation.aws_lambda import AwsLambdaInstrumentor
|
|
487
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
488
|
+
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
|
|
489
|
+
from opentelemetry.sdk.trace.export.in_memory_span_exporter import (
|
|
490
|
+
InMemorySpanExporter,
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
exporter = InMemorySpanExporter()
|
|
494
|
+
provider = TracerProvider()
|
|
495
|
+
provider.add_span_processor(SimpleSpanProcessor(exporter))
|
|
496
|
+
trace_api.set_tracer_provider(provider)
|
|
497
|
+
|
|
498
|
+
instrumentor = AwsLambdaInstrumentor()
|
|
499
|
+
|
|
500
|
+
def _inner_handler(event, context):
|
|
501
|
+
tracer = trace_api.get_tracer("test")
|
|
502
|
+
with tracer.start_as_current_span("inner-db-span"):
|
|
503
|
+
pass
|
|
504
|
+
return {"batchItemFailures": []}
|
|
505
|
+
|
|
506
|
+
monkeypatch.setattr(
|
|
507
|
+
"lambda_tasks.handler.handler", _inner_handler, raising=False
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
class _Context:
|
|
511
|
+
function_name = "my-fn"
|
|
512
|
+
function_version = "$LATEST"
|
|
513
|
+
invoked_function_arn = (
|
|
514
|
+
"arn:aws:lambda:us-east-1:123456789012:function:my-fn"
|
|
515
|
+
)
|
|
516
|
+
memory_limit_in_mb = 128
|
|
517
|
+
aws_request_id = "req-1"
|
|
518
|
+
|
|
519
|
+
try:
|
|
520
|
+
instrumentor.instrument()
|
|
521
|
+
wrapped = __import__("lambda_tasks.handler", fromlist=["handler"]).handler
|
|
522
|
+
wrapped({"Records": []}, _Context())
|
|
523
|
+
finally:
|
|
524
|
+
if instrumentor.is_instrumented_by_opentelemetry:
|
|
525
|
+
instrumentor.uninstrument()
|
|
526
|
+
|
|
527
|
+
spans = exporter.get_finished_spans()
|
|
528
|
+
server_spans = [s for s in spans if s.kind == trace_api.SpanKind.SERVER]
|
|
529
|
+
inner_spans = [s for s in spans if s.name == "inner-db-span"]
|
|
530
|
+
|
|
531
|
+
assert server_spans, f"no SERVER span produced; got {[s.name for s in spans]}"
|
|
532
|
+
assert inner_spans, "inner task span was not recorded"
|
|
533
|
+
|
|
534
|
+
server = server_spans[0]
|
|
535
|
+
inner = inner_spans[0]
|
|
536
|
+
|
|
537
|
+
assert inner.context.trace_id == server.context.trace_id
|
|
538
|
+
assert inner.parent is not None
|
|
539
|
+
assert inner.parent.span_id == server.context.span_id
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/async-local-execution/design.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/async-local-execution/tasks.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/deferred-task-enqueue/design.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/deferred-task-enqueue/tasks.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/eager-mode-example-app/design.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/eager-mode-example-app/tasks.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/retry-delay/.config.kiro
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/retry-delay/requirements.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/rse-background-tasks/design.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/rse-background-tasks/tasks.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/singleton-task/.config.kiro
RENAMED
|
File without changes
|
{django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/singleton-task/design.md
RENAMED
|
File without changes
|
{django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/singleton-task/requirements.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/ssm-environment-loader/design.md
RENAMED
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/ssm-environment-loader/tasks.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/.kiro/specs/task-retry/requirements.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_lambda_tasks-0.9.0 → django_lambda_tasks-0.9.2}/lambda_tasks/migrations/0001_initial.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|