flagsmith-common 3.6.0__tar.gz → 3.7.0__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 (107) hide show
  1. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/PKG-INFO +4 -2
  2. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/README.md +2 -1
  3. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/pyproject.toml +2 -1
  4. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/core/main.py +23 -16
  5. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/test_tools/types.py +1 -1
  6. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/task_processor/decorators.py +5 -1
  7. flagsmith_common-3.7.0/src/task_processor/migrations/0014_add_trace_context.py +23 -0
  8. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/task_processor/models.py +4 -1
  9. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/task_processor/processor.py +15 -0
  10. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/task_processor/types.py +2 -0
  11. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/LICENSE +0 -0
  12. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/__init__.py +0 -0
  13. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/core/__init__.py +0 -0
  14. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/core/app.py +0 -0
  15. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/core/cli/__init__.py +0 -0
  16. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/core/cli/healthcheck.py +0 -0
  17. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/core/constants.py +0 -0
  18. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/core/logging.py +0 -0
  19. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/core/management/__init__.py +0 -0
  20. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/core/management/commands/__init__.py +0 -0
  21. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/core/management/commands/docgen.py +0 -0
  22. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/core/management/commands/start.py +0 -0
  23. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/core/management/commands/waitfordb.py +0 -0
  24. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/core/metrics.py +0 -0
  25. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/core/middleware.py +0 -0
  26. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/core/otel.py +0 -0
  27. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/core/sentry.py +0 -0
  28. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/core/templates/docgen-metrics.md +0 -0
  29. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/core/urls.py +0 -0
  30. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/core/utils.py +0 -0
  31. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/core/views.py +0 -0
  32. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/environments/permissions.py +0 -0
  33. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/features/__init__.py +0 -0
  34. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/features/multivariate/__init__.py +0 -0
  35. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/features/multivariate/serializers.py +0 -0
  36. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/features/serializers.py +0 -0
  37. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/features/versioning/__init__.py +0 -0
  38. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/features/versioning/serializers.py +0 -0
  39. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/gunicorn/__init__.py +0 -0
  40. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/gunicorn/conf.py +0 -0
  41. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/gunicorn/constants.py +0 -0
  42. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/gunicorn/logging.py +0 -0
  43. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/gunicorn/metrics.py +0 -0
  44. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/gunicorn/metrics_server.py +0 -0
  45. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/gunicorn/middleware.py +0 -0
  46. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/gunicorn/processors.py +0 -0
  47. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/gunicorn/utils.py +0 -0
  48. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/lint_tests.py +0 -0
  49. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/migrations/__init__.py +0 -0
  50. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/migrations/helpers/__init__.py +0 -0
  51. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/migrations/helpers/postgres_helpers.py +0 -0
  52. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/organisations/permissions.py +0 -0
  53. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/projects/permissions.py +0 -0
  54. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/prometheus/__init__.py +0 -0
  55. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/prometheus/utils.py +0 -0
  56. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/py.typed +0 -0
  57. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/test_tools/__init__.py +0 -0
  58. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/test_tools/plugin.py +0 -0
  59. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/test_tools/utils.py +0 -0
  60. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/common/types.py +0 -0
  61. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/flagsmith_schemas/__init__.py +0 -0
  62. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/flagsmith_schemas/api.py +0 -0
  63. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/flagsmith_schemas/constants.py +0 -0
  64. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/flagsmith_schemas/dynamodb.py +0 -0
  65. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/flagsmith_schemas/py.typed +0 -0
  66. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/flagsmith_schemas/pydantic_types.py +0 -0
  67. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/flagsmith_schemas/types.py +0 -0
  68. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/flagsmith_schemas/utils.py +0 -0
  69. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/flagsmith_schemas/validators.py +0 -0
  70. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/task_processor/__init__.py +0 -0
  71. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/task_processor/admin.py +0 -0
  72. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/task_processor/apps.py +0 -0
  73. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/task_processor/exceptions.py +0 -0
  74. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/task_processor/health.py +0 -0
  75. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/task_processor/managers.py +0 -0
  76. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/task_processor/metrics.py +0 -0
  77. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/task_processor/migrations/0001_initial.py +0 -0
  78. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/task_processor/migrations/0002_healthcheckmodel.py +0 -0
  79. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/task_processor/migrations/0003_add_completed_to_task.py +0 -0
  80. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/task_processor/migrations/0004_recreate_task_indexes.py +0 -0
  81. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/task_processor/migrations/0005_update_conditional_index_conditions.py +0 -0
  82. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/task_processor/migrations/0006_auto_20230221_0802.py +0 -0
  83. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/task_processor/migrations/0007_add_is_locked.py +0 -0
  84. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/task_processor/migrations/0008_add_get_task_to_process_function.py +0 -0
  85. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/task_processor/migrations/0009_add_recurring_task_run_first_run_at.py +0 -0
  86. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/task_processor/migrations/0010_task_priority.py +0 -0
  87. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/task_processor/migrations/0011_add_priority_to_get_tasks_to_process.py +0 -0
  88. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/task_processor/migrations/0012_add_locked_at_and_timeout.py +0 -0
  89. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/task_processor/migrations/0013_add_last_picked_at.py +0 -0
  90. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/task_processor/migrations/__init__.py +0 -0
  91. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/task_processor/migrations/sql/0008_get_recurring_tasks_to_process.sql +0 -0
  92. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/task_processor/migrations/sql/0008_get_tasks_to_process.sql +0 -0
  93. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/task_processor/migrations/sql/0011_get_tasks_to_process.sql +0 -0
  94. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/task_processor/migrations/sql/0012_get_recurringtasks_to_process.sql +0 -0
  95. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/task_processor/migrations/sql/0013_get_recurringtasks_to_process.sql +0 -0
  96. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/task_processor/migrations/sql/__init__.py +0 -0
  97. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/task_processor/monitoring.py +0 -0
  98. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/task_processor/py.typed +0 -0
  99. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/task_processor/routers.py +0 -0
  100. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/task_processor/serializers.py +0 -0
  101. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/task_processor/task_registry.py +0 -0
  102. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/task_processor/task_run_method.py +0 -0
  103. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/task_processor/tasks.py +0 -0
  104. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/task_processor/threads.py +0 -0
  105. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/task_processor/urls.py +0 -0
  106. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/task_processor/utils.py +0 -0
  107. {flagsmith_common-3.6.0 → flagsmith_common-3.7.0}/src/task_processor/views.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flagsmith-common
3
- Version: 3.6.0
3
+ Version: 3.7.0
4
4
  Summary: Flagsmith's common library
5
5
  Author: Matthew Elwell, Gagan Trivedi, Kim Gustyr, Zach Aysan, Francesco Lo Franco, Rodrigo López Dato, Evandro Myller, Wadii Zaim
6
6
  License-Expression: BSD-3-Clause
@@ -38,6 +38,7 @@ Requires-Dist: flagsmith-flag-engine>6 ; extra == 'flagsmith-schemas'
38
38
  Requires-Dist: backoff>=2.2.1,<3.0.0 ; extra == 'task-processor'
39
39
  Requires-Dist: django>4,<6 ; extra == 'task-processor'
40
40
  Requires-Dist: django-health-check ; extra == 'task-processor'
41
+ Requires-Dist: opentelemetry-api>=1.25,<2 ; extra == 'task-processor'
41
42
  Requires-Dist: prometheus-client>=0.0.16 ; extra == 'task-processor'
42
43
  Requires-Dist: pyfakefs>=5,<6 ; extra == 'test-tools'
43
44
  Requires-Dist: pytest-django>=4,<5 ; extra == 'test-tools'
@@ -164,7 +165,7 @@ OTel instrumentation is opt-in, controlled by environment variables:
164
165
  | Variable | Description | Default |
165
166
  | --------------------------------- | --------------------------------------------------------------------------------------------------------------------- | --------------- |
166
167
  | `OTEL_EXPORTER_OTLP_ENDPOINT` | Base OTLP endpoint (e.g. `http://collector:4318`). If unset, no OTel setup occurs. | _(disabled)_ |
167
- | `OTEL_SERVICE_NAME` | The `service.name` resource attribute. | `flagsmith-api` |
168
+ | `OTEL_SERVICE_NAME` | The `service.name` resource attribute. Defaults to `flagsmith-task-processor` when running the task processor. | `flagsmith-api` |
168
169
  | `OTEL_TRACING_EXCLUDED_URL_PATHS` | Comma-separated URL paths to exclude from tracing (e.g. `health/liveness,health/readiness`). | _(none)_ |
169
170
 
170
171
  Standard `OTEL_*` env vars (e.g. `OTEL_RESOURCE_ATTRIBUTES`, `OTEL_EXPORTER_OTLP_HEADERS`) are also respected by the OTel SDK.
@@ -178,6 +179,7 @@ When `OTEL_EXPORTER_OTLP_ENDPOINT` is set, `ensure_cli_env()` sets up:
178
179
  - **psycopg2** (`Psycopg2Instrumentor`): creates child spans for each SQL query with `db.system`, `db.statement`, and `db.name` attributes. SQL commenter is enabled, adding trace context as SQL comments for database-side correlation.
179
180
  - **Redis** (`RedisInstrumentor`): creates child spans for each Redis command with `db.system` and `db.statement` attributes.
180
181
  - **Structured log export**: A structlog processor that emits each log event as both an OTLP log record and a span event (when an active span exists).
182
+ - **Task processor trace propagation**: When a task is enqueued via `TaskHandler.delay()`, the current W3C trace context (including baggage) is serialized into the task's `trace_context` field. When the task processor executes the task, the context is extracted and a child span is created, linking the task execution back to the originating request trace. Span attributes (`task_identifier`, `task_type`, `result`) match the Prometheus metric labels for cross-signal correlation.
181
183
 
182
184
  #### Emitting OTel log events via structlog
183
185
 
@@ -107,7 +107,7 @@ OTel instrumentation is opt-in, controlled by environment variables:
107
107
  | Variable | Description | Default |
108
108
  | --------------------------------- | --------------------------------------------------------------------------------------------------------------------- | --------------- |
109
109
  | `OTEL_EXPORTER_OTLP_ENDPOINT` | Base OTLP endpoint (e.g. `http://collector:4318`). If unset, no OTel setup occurs. | _(disabled)_ |
110
- | `OTEL_SERVICE_NAME` | The `service.name` resource attribute. | `flagsmith-api` |
110
+ | `OTEL_SERVICE_NAME` | The `service.name` resource attribute. Defaults to `flagsmith-task-processor` when running the task processor. | `flagsmith-api` |
111
111
  | `OTEL_TRACING_EXCLUDED_URL_PATHS` | Comma-separated URL paths to exclude from tracing (e.g. `health/liveness,health/readiness`). | _(none)_ |
112
112
 
113
113
  Standard `OTEL_*` env vars (e.g. `OTEL_RESOURCE_ATTRIBUTES`, `OTEL_EXPORTER_OTLP_HEADERS`) are also respected by the OTel SDK.
@@ -121,6 +121,7 @@ When `OTEL_EXPORTER_OTLP_ENDPOINT` is set, `ensure_cli_env()` sets up:
121
121
  - **psycopg2** (`Psycopg2Instrumentor`): creates child spans for each SQL query with `db.system`, `db.statement`, and `db.name` attributes. SQL commenter is enabled, adding trace context as SQL comments for database-side correlation.
122
122
  - **Redis** (`RedisInstrumentor`): creates child spans for each Redis command with `db.system` and `db.statement` attributes.
123
123
  - **Structured log export**: A structlog processor that emits each log event as both an OTLP log record and a span event (when an active span exists).
124
+ - **Task processor trace propagation**: When a task is enqueued via `TaskHandler.delay()`, the current W3C trace context (including baggage) is serialized into the task's `trace_context` field. When the task processor executes the task, the context is extracted and a child span is created, linking the task execution back to the originating request trace. Span attributes (`task_identifier`, `task_type`, `result`) match the Prometheus metric labels for cross-signal correlation.
124
125
 
125
126
  #### Emitting OTel log events via structlog
126
127
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "flagsmith-common"
3
- version = "3.6.0"
3
+ version = "3.7.0"
4
4
  description = "Flagsmith's common library"
5
5
  requires-python = ">=3.11,<4.0"
6
6
  dependencies = []
@@ -35,6 +35,7 @@ optional-dependencies = { test-tools = [
35
35
  "backoff (>=2.2.1,<3.0.0)",
36
36
  "django (>4,<6)",
37
37
  "django-health-check",
38
+ "opentelemetry-api (>=1.25,<2)",
38
39
  "prometheus-client (>=0.0.16)",
39
40
  ], flagsmith-schemas = [
40
41
  "simplejson",
@@ -37,7 +37,24 @@ def ensure_cli_env() -> typing.Generator[None, None, None]:
37
37
  """
38
38
  ctx = contextlib.ExitStack()
39
39
 
40
+ # Currently we don't install Flagsmith modules as a package, so we need to add
41
+ # $CWD to the Python path to be able to import them
42
+ sys.path.append(os.getcwd())
43
+
44
+ # TODO @khvn26 We should find a better way to pre-set the Django settings module
45
+ # without resorting to it being set outside of the application
46
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings.dev")
47
+
48
+ if "docgen" in sys.argv:
49
+ os.environ["DOCGEN_MODE"] = "true"
50
+
51
+ if "task-processor" in sys.argv:
52
+ # A hacky way to signal we're not running the API
53
+ os.environ["RUN_BY_PROCESSOR"] = "true"
54
+
40
55
  # Set up OTel instrumentation (opt-in via OTEL_EXPORTER_OTLP_ENDPOINT).
56
+ # Must come after sys.path / DJANGO_SETTINGS_MODULE setup because
57
+ # DjangoInstrumentor accesses settings.MIDDLEWARE.
41
58
  otel_processors = None
42
59
  otel_endpoint = env.str("OTEL_EXPORTER_OTLP_ENDPOINT", None)
43
60
  if otel_endpoint:
@@ -49,7 +66,12 @@ def ensure_cli_env() -> typing.Generator[None, None, None]:
49
66
  setup_tracing,
50
67
  )
51
68
 
52
- service_name = env.str("OTEL_SERVICE_NAME", "flagsmith-api")
69
+ default_service_name = (
70
+ "flagsmith-task-processor"
71
+ if "task-processor" in sys.argv
72
+ else "flagsmith-api"
73
+ )
74
+ service_name = env.str("OTEL_SERVICE_NAME", default_service_name)
53
75
  log_provider = build_otel_log_provider(
54
76
  endpoint=f"{otel_endpoint}/v1/logs",
55
77
  service_name=service_name,
@@ -84,21 +106,6 @@ def ensure_cli_env() -> typing.Generator[None, None, None]:
84
106
  if not os.environ.get("PROMETHEUS_MULTIPROC_DIR"):
85
107
  os.environ["PROMETHEUS_MULTIPROC_DIR"] = mkdtemp(prefix="flagsmith-prometheus-")
86
108
 
87
- # Currently we don't install Flagsmith modules as a package, so we need to add
88
- # $CWD to the Python path to be able to import them
89
- sys.path.append(os.getcwd())
90
-
91
- # TODO @khvn26 We should find a better way to pre-set the Django settings module
92
- # without resorting to it being set outside of the application
93
- os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings.dev")
94
-
95
- if "docgen" in sys.argv:
96
- os.environ["DOCGEN_MODE"] = "true"
97
-
98
- if "task-processor" in sys.argv:
99
- # A hacky way to signal we're not running the API
100
- os.environ["RUN_BY_PROCESSOR"] = "true"
101
-
102
109
  with ctx:
103
110
  yield
104
111
 
@@ -20,7 +20,7 @@ class AssertMetricFixture(Protocol):
20
20
  class RunTasksFixture(Protocol):
21
21
  def __call__(
22
22
  self,
23
- num_tasks: int,
23
+ num_tasks: int = 1,
24
24
  ) -> "list[TaskRun]": ...
25
25
 
26
26
 
@@ -6,12 +6,13 @@ from threading import Thread
6
6
  from django.conf import settings
7
7
  from django.db.transaction import on_commit
8
8
  from django.utils import timezone
9
+ from opentelemetry import propagate
9
10
 
10
11
  from task_processor import metrics, task_registry
11
12
  from task_processor.exceptions import InvalidArgumentsError, TaskQueueFullError
12
13
  from task_processor.models import RecurringTask, Task, TaskPriority
13
14
  from task_processor.task_run_method import TaskRunMethod
14
- from task_processor.types import TaskCallable, TaskParameters
15
+ from task_processor.types import TaskCallable, TaskParameters, TraceContext
15
16
  from task_processor.utils import get_task_identifier_from_function
16
17
 
17
18
  logger = logging.getLogger(__name__)
@@ -92,6 +93,8 @@ class TaskHandler(typing.Generic[TaskParameters]):
92
93
  task_identifier=task_identifier
93
94
  ).inc()
94
95
  try:
96
+ carrier: TraceContext = {}
97
+ propagate.inject(carrier)
95
98
  task = Task.create(
96
99
  task_identifier=task_identifier,
97
100
  scheduled_for=delay_until or timezone.now(),
@@ -100,6 +103,7 @@ class TaskHandler(typing.Generic[TaskParameters]):
100
103
  timeout=self.timeout,
101
104
  args=args,
102
105
  kwargs=kwargs,
106
+ trace_context=carrier or None,
103
107
  )
104
108
  except TaskQueueFullError as e:
105
109
  logger.warning(e)
@@ -0,0 +1,23 @@
1
+ # Generated by Django 5.2.12 on 2026-04-10 14:29
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ("task_processor", "0013_add_last_picked_at"),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name="recurringtask",
15
+ name="trace_context",
16
+ field=models.JSONField(blank=True, null=True),
17
+ ),
18
+ migrations.AddField(
19
+ model_name="task",
20
+ name="trace_context",
21
+ field=models.JSONField(blank=True, null=True),
22
+ ),
23
+ ]
@@ -10,7 +10,7 @@ from django.utils import timezone
10
10
  from task_processor.exceptions import TaskQueueFullError
11
11
  from task_processor.managers import RecurringTaskManager, TaskManager
12
12
  from task_processor.task_registry import get_task, registered_tasks
13
- from task_processor.types import TaskCallable
13
+ from task_processor.types import TaskCallable, TraceContext
14
14
 
15
15
  _django_json_encoder_default = DjangoJSONEncoder().default
16
16
 
@@ -31,6 +31,7 @@ class AbstractBaseTask(models.Model):
31
31
  serialized_kwargs = models.TextField(blank=True, null=True)
32
32
  is_locked = models.BooleanField(default=False)
33
33
  timeout = models.DurationField(blank=True, null=True)
34
+ trace_context = models.JSONField(null=True, blank=True)
34
35
 
35
36
  class Meta:
36
37
  abstract = True
@@ -112,6 +113,7 @@ class Task(AbstractBaseTask):
112
113
  args: typing.Tuple[typing.Any, ...] | None = None,
113
114
  kwargs: typing.Dict[str, typing.Any] | None = None,
114
115
  timeout: timedelta | None = timedelta(seconds=60),
116
+ trace_context: TraceContext | None = None,
115
117
  ) -> "Task":
116
118
  if queue_size and cls._is_queue_full(task_identifier, queue_size):
117
119
  raise TaskQueueFullError(
@@ -125,6 +127,7 @@ class Task(AbstractBaseTask):
125
127
  serialized_args=cls.serialize_data(args or tuple()),
126
128
  serialized_kwargs=cls.serialize_data(kwargs or dict()),
127
129
  timeout=timeout,
130
+ trace_context=trace_context,
128
131
  )
129
132
 
130
133
  @classmethod
@@ -4,9 +4,12 @@ import typing
4
4
  from concurrent.futures import ThreadPoolExecutor
5
5
  from contextlib import ExitStack
6
6
  from datetime import timedelta
7
+ from importlib.metadata import version
7
8
 
8
9
  from django.conf import settings
9
10
  from django.utils import timezone
11
+ from opentelemetry import context as otel_context
12
+ from opentelemetry import propagate, trace
10
13
 
11
14
  from task_processor import metrics
12
15
  from task_processor.exceptions import TaskBackoffError
@@ -130,6 +133,11 @@ def _run_task(
130
133
  result: str
131
134
  executor = None
132
135
 
136
+ extracted_ctx = propagate.extract(task.trace_context or {})
137
+ tracer = trace.get_tracer("task_processor", version("flagsmith-common"))
138
+ span = tracer.start_span(task_identifier, context=extracted_ctx)
139
+ otel_token = otel_context.attach(trace.set_span_in_context(span, extracted_ctx))
140
+
133
141
  try:
134
142
  # Use explicit executor management to avoid blocking on shutdown
135
143
  # when tasks timeout but continue running in worker threads.
@@ -151,6 +159,9 @@ def _run_task(
151
159
  # fall back to using repr.
152
160
  err_msg = str(e) or repr(e)
153
161
 
162
+ span.set_status(trace.StatusCode.ERROR, err_msg)
163
+ span.record_exception(e)
164
+
154
165
  task.mark_failure()
155
166
 
156
167
  task_run.result = result = TaskResult.FAILURE.value
@@ -199,4 +210,8 @@ def _run_task(
199
210
 
200
211
  metrics.flagsmith_task_processor_finished_tasks_total.labels(**labels).inc()
201
212
 
213
+ span.set_attributes(labels)
214
+ span.end()
215
+ otel_context.detach(otel_token)
216
+
202
217
  return task, task_run
@@ -5,6 +5,8 @@ TaskParameters = ParamSpec("TaskParameters")
5
5
 
6
6
  TaskCallable: TypeAlias = Callable[TaskParameters, None]
7
7
 
8
+ TraceContext: TypeAlias = dict[str, str]
9
+
8
10
 
9
11
  @dataclass
10
12
  class TaskProcessorConfig: