rebrandly-otel 0.2.16__tar.gz → 0.2.19__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.

Potentially problematic release.


This version of rebrandly-otel might be problematic. Click here for more details.

Files changed (28) hide show
  1. {rebrandly_otel-0.2.16 → rebrandly_otel-0.2.19}/PKG-INFO +74 -9
  2. {rebrandly_otel-0.2.16 → rebrandly_otel-0.2.19}/README.md +73 -8
  3. {rebrandly_otel-0.2.16 → rebrandly_otel-0.2.19}/rebrandly_otel.egg-info/PKG-INFO +74 -9
  4. {rebrandly_otel-0.2.16 → rebrandly_otel-0.2.19}/rebrandly_otel.egg-info/SOURCES.txt +2 -0
  5. {rebrandly_otel-0.2.16 → rebrandly_otel-0.2.19}/setup.py +1 -1
  6. rebrandly_otel-0.2.19/src/span_attributes_processor.py +106 -0
  7. {rebrandly_otel-0.2.16 → rebrandly_otel-0.2.19}/src/traces.py +4 -0
  8. rebrandly_otel-0.2.19/tests/test_span_attributes_processor.py +409 -0
  9. {rebrandly_otel-0.2.16 → rebrandly_otel-0.2.19}/LICENSE +0 -0
  10. {rebrandly_otel-0.2.16 → rebrandly_otel-0.2.19}/rebrandly_otel.egg-info/dependency_links.txt +0 -0
  11. {rebrandly_otel-0.2.16 → rebrandly_otel-0.2.19}/rebrandly_otel.egg-info/requires.txt +0 -0
  12. {rebrandly_otel-0.2.16 → rebrandly_otel-0.2.19}/rebrandly_otel.egg-info/top_level.txt +0 -0
  13. {rebrandly_otel-0.2.16 → rebrandly_otel-0.2.19}/setup.cfg +0 -0
  14. {rebrandly_otel-0.2.16 → rebrandly_otel-0.2.19}/src/__init__.py +0 -0
  15. {rebrandly_otel-0.2.16 → rebrandly_otel-0.2.19}/src/fastapi_support.py +0 -0
  16. {rebrandly_otel-0.2.16 → rebrandly_otel-0.2.19}/src/flask_support.py +0 -0
  17. {rebrandly_otel-0.2.16 → rebrandly_otel-0.2.19}/src/http_utils.py +0 -0
  18. {rebrandly_otel-0.2.16 → rebrandly_otel-0.2.19}/src/logs.py +0 -0
  19. {rebrandly_otel-0.2.16 → rebrandly_otel-0.2.19}/src/metrics.py +0 -0
  20. {rebrandly_otel-0.2.16 → rebrandly_otel-0.2.19}/src/otel_utils.py +0 -0
  21. {rebrandly_otel-0.2.16 → rebrandly_otel-0.2.19}/src/pymysql_instrumentation.py +0 -0
  22. {rebrandly_otel-0.2.16 → rebrandly_otel-0.2.19}/src/rebrandly_otel.py +0 -0
  23. {rebrandly_otel-0.2.16 → rebrandly_otel-0.2.19}/tests/test_decorators.py +0 -0
  24. {rebrandly_otel-0.2.16 → rebrandly_otel-0.2.19}/tests/test_fastapi_support.py +0 -0
  25. {rebrandly_otel-0.2.16 → rebrandly_otel-0.2.19}/tests/test_flask_support.py +0 -0
  26. {rebrandly_otel-0.2.16 → rebrandly_otel-0.2.19}/tests/test_metrics_and_logs.py +0 -0
  27. {rebrandly_otel-0.2.16 → rebrandly_otel-0.2.19}/tests/test_pymysql_instrumentation.py +0 -0
  28. {rebrandly_otel-0.2.16 → rebrandly_otel-0.2.19}/tests/test_usage.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rebrandly_otel
3
- Version: 0.2.16
3
+ Version: 0.2.19
4
4
  Summary: Python OTEL wrapper by Rebrandly
5
5
  Home-page: https://github.com/rebrandly/rebrandly-otel-python
6
6
  Author: Antonio Romano
@@ -51,14 +51,16 @@ pip install rebrandly-otel
51
51
 
52
52
  The SDK is configured through environment variables:
53
53
 
54
- | Variable | Description | Default |
55
- |------------------------------------|-------------|---------|
56
- | `OTEL_SERVICE_NAME` | Service identifier | `default-service-python` |
57
- | `OTEL_SERVICE_VERSION` | Service version | `1.0.0` |
58
- | `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP collector endpoint | `None` |
59
- | `OTEL_DEBUG` | Enable console debugging | `false` |
60
- | `BATCH_EXPORT_TIME_MILLIS` | Batch export interval | `100` |
61
- | `ENV` or `ENVIRONMENT` or `NODE_ENV` | Deployment environment | `local` |
54
+ | Variable | Description | Default |
55
+ |------------------------------------|-------------|---------------------------------|
56
+ | `OTEL_SERVICE_NAME` | Service identifier | `default-service-python` |
57
+ | `OTEL_SERVICE_VERSION` | Service version | `1.0.0` |
58
+ | `OTEL_SERVICE_APPLICATION` | Application namespace (groups multiple services under one application) | Fallback to `OTEL_SERVICE_NAME` |
59
+ | `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP collector endpoint | `None` |
60
+ | `OTEL_DEBUG` | Enable console debugging | `false` |
61
+ | `OTEL_SPAN_ATTRIBUTES` | Attributes automatically added to all spans (format: `key1=value1,key2=value2`) | `None` |
62
+ | `BATCH_EXPORT_TIME_MILLIS` | Batch export interval | `100` |
63
+ | `ENV` or `ENVIRONMENT` or `NODE_ENV` | Deployment environment | `local` |
62
64
 
63
65
  ## Core Components
64
66
 
@@ -146,6 +148,67 @@ Lambda spans automatically include:
146
148
  - `cloud.provider`: Always "aws" for Lambda
147
149
  - `cloud.platform`: Always "aws_lambda" for Lambda
148
150
 
151
+ ## Automatic Span Attributes
152
+
153
+ The SDK supports automatically adding custom attributes to all spans via the `OTEL_SPAN_ATTRIBUTES` environment variable. This is useful for adding metadata that applies to all telemetry in a service, such as team ownership, deployment environment, or version information.
154
+
155
+ ### Configuration
156
+
157
+ Set the `OTEL_SPAN_ATTRIBUTES` environment variable with a comma-separated list of key-value pairs:
158
+
159
+ ```bash
160
+ export OTEL_SPAN_ATTRIBUTES="team=backend,environment=production,version=1.2.3"
161
+ ```
162
+
163
+ ### Behavior
164
+
165
+ - **Universal Application**: Attributes are added to ALL spans, including:
166
+ - Manually created spans (`tracer.start_span()`, `tracer.start_as_current_span()`)
167
+ - Lambda handler spans (`@lambda_handler`)
168
+ - AWS message handler spans (`@aws_message_handler`)
169
+ - Flask/FastAPI middleware spans
170
+ - Auto-instrumented spans (database queries, HTTP requests, etc.)
171
+
172
+ - **Format**: Same as `OTEL_RESOURCE_ATTRIBUTES` - comma-separated `key=value` pairs
173
+ - **Value Handling**: Supports values containing `=` characters (e.g., URLs)
174
+ - **Whitespace**: Leading/trailing whitespace is automatically trimmed
175
+
176
+ ### Example
177
+
178
+ ```python
179
+ import os
180
+
181
+ # Set environment variable
182
+ os.environ['OTEL_SPAN_ATTRIBUTES'] = "team=backend,service.owner=platform-team,deployment.region=us-east-1"
183
+
184
+ # Initialize SDK
185
+ from rebrandly_otel import otel, logger
186
+
187
+ # Create any span - attributes are added automatically
188
+ with otel.span('my-operation'):
189
+ logger.info('Processing request')
190
+ # The span will include:
191
+ # - team: "backend"
192
+ # - service.owner: "platform-team"
193
+ # - deployment.region: "us-east-1"
194
+ # ... plus any other attributes you set manually
195
+ ```
196
+
197
+ ### Use Cases
198
+
199
+ - **Team/Ownership Tagging**: `team=backend,owner=john@example.com`
200
+ - **Environment Metadata**: `environment=production,region=us-east-1,availability_zone=us-east-1a`
201
+ - **Version Tracking**: `version=1.2.3,build=12345,commit=abc123def`
202
+ - **Cost Attribution**: `cost_center=engineering,project=customer-api`
203
+ - **Multi-Tenancy**: `tenant=acme-corp,customer_tier=enterprise`
204
+
205
+ ### Difference from OTEL_RESOURCE_ATTRIBUTES
206
+
207
+ - **OTEL_RESOURCE_ATTRIBUTES**: Service-level metadata (set once, applies to the entire service instance)
208
+ - **OTEL_SPAN_ATTRIBUTES**: Span-level metadata (added to each individual span at creation time)
209
+
210
+ Both use the same format but serve different purposes in the OpenTelemetry data model.
211
+
149
212
  ### Exception Handling
150
213
 
151
214
  Spans automatically capture exceptions with:
@@ -521,6 +584,7 @@ pytest tests/test_usage.py -v
521
584
  pytest tests/test_pymysql_instrumentation.py -v
522
585
  pytest tests/test_metrics_and_logs.py -v
523
586
  pytest tests/test_decorators.py -v
587
+ pytest tests/test_span_attributes_processor.py -v
524
588
  ```
525
589
 
526
590
  Run with coverage:
@@ -537,6 +601,7 @@ The test suite includes:
537
601
  - **PyMySQL instrumentation tests** (`test_pymysql_instrumentation.py`): Database connection instrumentation, query tracing, helper functions
538
602
  - **Metrics and logs tests** (`test_metrics_and_logs.py`): Custom metrics creation (counter, histogram, gauge), logging levels (info, warning, debug, error)
539
603
  - **Decorators tests** (`test_decorators.py`): Lambda handler decorator, AWS message handler decorator, traces decorator, aws_message_span context manager
604
+ - **Span attributes processor tests** (`test_span_attributes_processor.py`): Automatic span attributes from OTEL_SPAN_ATTRIBUTES (31 tests)
540
605
 
541
606
  ## License
542
607
 
@@ -24,14 +24,16 @@ pip install rebrandly-otel
24
24
 
25
25
  The SDK is configured through environment variables:
26
26
 
27
- | Variable | Description | Default |
28
- |------------------------------------|-------------|---------|
29
- | `OTEL_SERVICE_NAME` | Service identifier | `default-service-python` |
30
- | `OTEL_SERVICE_VERSION` | Service version | `1.0.0` |
31
- | `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP collector endpoint | `None` |
32
- | `OTEL_DEBUG` | Enable console debugging | `false` |
33
- | `BATCH_EXPORT_TIME_MILLIS` | Batch export interval | `100` |
34
- | `ENV` or `ENVIRONMENT` or `NODE_ENV` | Deployment environment | `local` |
27
+ | Variable | Description | Default |
28
+ |------------------------------------|-------------|---------------------------------|
29
+ | `OTEL_SERVICE_NAME` | Service identifier | `default-service-python` |
30
+ | `OTEL_SERVICE_VERSION` | Service version | `1.0.0` |
31
+ | `OTEL_SERVICE_APPLICATION` | Application namespace (groups multiple services under one application) | Fallback to `OTEL_SERVICE_NAME` |
32
+ | `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP collector endpoint | `None` |
33
+ | `OTEL_DEBUG` | Enable console debugging | `false` |
34
+ | `OTEL_SPAN_ATTRIBUTES` | Attributes automatically added to all spans (format: `key1=value1,key2=value2`) | `None` |
35
+ | `BATCH_EXPORT_TIME_MILLIS` | Batch export interval | `100` |
36
+ | `ENV` or `ENVIRONMENT` or `NODE_ENV` | Deployment environment | `local` |
35
37
 
36
38
  ## Core Components
37
39
 
@@ -119,6 +121,67 @@ Lambda spans automatically include:
119
121
  - `cloud.provider`: Always "aws" for Lambda
120
122
  - `cloud.platform`: Always "aws_lambda" for Lambda
121
123
 
124
+ ## Automatic Span Attributes
125
+
126
+ The SDK supports automatically adding custom attributes to all spans via the `OTEL_SPAN_ATTRIBUTES` environment variable. This is useful for adding metadata that applies to all telemetry in a service, such as team ownership, deployment environment, or version information.
127
+
128
+ ### Configuration
129
+
130
+ Set the `OTEL_SPAN_ATTRIBUTES` environment variable with a comma-separated list of key-value pairs:
131
+
132
+ ```bash
133
+ export OTEL_SPAN_ATTRIBUTES="team=backend,environment=production,version=1.2.3"
134
+ ```
135
+
136
+ ### Behavior
137
+
138
+ - **Universal Application**: Attributes are added to ALL spans, including:
139
+ - Manually created spans (`tracer.start_span()`, `tracer.start_as_current_span()`)
140
+ - Lambda handler spans (`@lambda_handler`)
141
+ - AWS message handler spans (`@aws_message_handler`)
142
+ - Flask/FastAPI middleware spans
143
+ - Auto-instrumented spans (database queries, HTTP requests, etc.)
144
+
145
+ - **Format**: Same as `OTEL_RESOURCE_ATTRIBUTES` - comma-separated `key=value` pairs
146
+ - **Value Handling**: Supports values containing `=` characters (e.g., URLs)
147
+ - **Whitespace**: Leading/trailing whitespace is automatically trimmed
148
+
149
+ ### Example
150
+
151
+ ```python
152
+ import os
153
+
154
+ # Set environment variable
155
+ os.environ['OTEL_SPAN_ATTRIBUTES'] = "team=backend,service.owner=platform-team,deployment.region=us-east-1"
156
+
157
+ # Initialize SDK
158
+ from rebrandly_otel import otel, logger
159
+
160
+ # Create any span - attributes are added automatically
161
+ with otel.span('my-operation'):
162
+ logger.info('Processing request')
163
+ # The span will include:
164
+ # - team: "backend"
165
+ # - service.owner: "platform-team"
166
+ # - deployment.region: "us-east-1"
167
+ # ... plus any other attributes you set manually
168
+ ```
169
+
170
+ ### Use Cases
171
+
172
+ - **Team/Ownership Tagging**: `team=backend,owner=john@example.com`
173
+ - **Environment Metadata**: `environment=production,region=us-east-1,availability_zone=us-east-1a`
174
+ - **Version Tracking**: `version=1.2.3,build=12345,commit=abc123def`
175
+ - **Cost Attribution**: `cost_center=engineering,project=customer-api`
176
+ - **Multi-Tenancy**: `tenant=acme-corp,customer_tier=enterprise`
177
+
178
+ ### Difference from OTEL_RESOURCE_ATTRIBUTES
179
+
180
+ - **OTEL_RESOURCE_ATTRIBUTES**: Service-level metadata (set once, applies to the entire service instance)
181
+ - **OTEL_SPAN_ATTRIBUTES**: Span-level metadata (added to each individual span at creation time)
182
+
183
+ Both use the same format but serve different purposes in the OpenTelemetry data model.
184
+
122
185
  ### Exception Handling
123
186
 
124
187
  Spans automatically capture exceptions with:
@@ -494,6 +557,7 @@ pytest tests/test_usage.py -v
494
557
  pytest tests/test_pymysql_instrumentation.py -v
495
558
  pytest tests/test_metrics_and_logs.py -v
496
559
  pytest tests/test_decorators.py -v
560
+ pytest tests/test_span_attributes_processor.py -v
497
561
  ```
498
562
 
499
563
  Run with coverage:
@@ -510,6 +574,7 @@ The test suite includes:
510
574
  - **PyMySQL instrumentation tests** (`test_pymysql_instrumentation.py`): Database connection instrumentation, query tracing, helper functions
511
575
  - **Metrics and logs tests** (`test_metrics_and_logs.py`): Custom metrics creation (counter, histogram, gauge), logging levels (info, warning, debug, error)
512
576
  - **Decorators tests** (`test_decorators.py`): Lambda handler decorator, AWS message handler decorator, traces decorator, aws_message_span context manager
577
+ - **Span attributes processor tests** (`test_span_attributes_processor.py`): Automatic span attributes from OTEL_SPAN_ATTRIBUTES (31 tests)
513
578
 
514
579
  ## License
515
580
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rebrandly_otel
3
- Version: 0.2.16
3
+ Version: 0.2.19
4
4
  Summary: Python OTEL wrapper by Rebrandly
5
5
  Home-page: https://github.com/rebrandly/rebrandly-otel-python
6
6
  Author: Antonio Romano
@@ -51,14 +51,16 @@ pip install rebrandly-otel
51
51
 
52
52
  The SDK is configured through environment variables:
53
53
 
54
- | Variable | Description | Default |
55
- |------------------------------------|-------------|---------|
56
- | `OTEL_SERVICE_NAME` | Service identifier | `default-service-python` |
57
- | `OTEL_SERVICE_VERSION` | Service version | `1.0.0` |
58
- | `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP collector endpoint | `None` |
59
- | `OTEL_DEBUG` | Enable console debugging | `false` |
60
- | `BATCH_EXPORT_TIME_MILLIS` | Batch export interval | `100` |
61
- | `ENV` or `ENVIRONMENT` or `NODE_ENV` | Deployment environment | `local` |
54
+ | Variable | Description | Default |
55
+ |------------------------------------|-------------|---------------------------------|
56
+ | `OTEL_SERVICE_NAME` | Service identifier | `default-service-python` |
57
+ | `OTEL_SERVICE_VERSION` | Service version | `1.0.0` |
58
+ | `OTEL_SERVICE_APPLICATION` | Application namespace (groups multiple services under one application) | Fallback to `OTEL_SERVICE_NAME` |
59
+ | `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP collector endpoint | `None` |
60
+ | `OTEL_DEBUG` | Enable console debugging | `false` |
61
+ | `OTEL_SPAN_ATTRIBUTES` | Attributes automatically added to all spans (format: `key1=value1,key2=value2`) | `None` |
62
+ | `BATCH_EXPORT_TIME_MILLIS` | Batch export interval | `100` |
63
+ | `ENV` or `ENVIRONMENT` or `NODE_ENV` | Deployment environment | `local` |
62
64
 
63
65
  ## Core Components
64
66
 
@@ -146,6 +148,67 @@ Lambda spans automatically include:
146
148
  - `cloud.provider`: Always "aws" for Lambda
147
149
  - `cloud.platform`: Always "aws_lambda" for Lambda
148
150
 
151
+ ## Automatic Span Attributes
152
+
153
+ The SDK supports automatically adding custom attributes to all spans via the `OTEL_SPAN_ATTRIBUTES` environment variable. This is useful for adding metadata that applies to all telemetry in a service, such as team ownership, deployment environment, or version information.
154
+
155
+ ### Configuration
156
+
157
+ Set the `OTEL_SPAN_ATTRIBUTES` environment variable with a comma-separated list of key-value pairs:
158
+
159
+ ```bash
160
+ export OTEL_SPAN_ATTRIBUTES="team=backend,environment=production,version=1.2.3"
161
+ ```
162
+
163
+ ### Behavior
164
+
165
+ - **Universal Application**: Attributes are added to ALL spans, including:
166
+ - Manually created spans (`tracer.start_span()`, `tracer.start_as_current_span()`)
167
+ - Lambda handler spans (`@lambda_handler`)
168
+ - AWS message handler spans (`@aws_message_handler`)
169
+ - Flask/FastAPI middleware spans
170
+ - Auto-instrumented spans (database queries, HTTP requests, etc.)
171
+
172
+ - **Format**: Same as `OTEL_RESOURCE_ATTRIBUTES` - comma-separated `key=value` pairs
173
+ - **Value Handling**: Supports values containing `=` characters (e.g., URLs)
174
+ - **Whitespace**: Leading/trailing whitespace is automatically trimmed
175
+
176
+ ### Example
177
+
178
+ ```python
179
+ import os
180
+
181
+ # Set environment variable
182
+ os.environ['OTEL_SPAN_ATTRIBUTES'] = "team=backend,service.owner=platform-team,deployment.region=us-east-1"
183
+
184
+ # Initialize SDK
185
+ from rebrandly_otel import otel, logger
186
+
187
+ # Create any span - attributes are added automatically
188
+ with otel.span('my-operation'):
189
+ logger.info('Processing request')
190
+ # The span will include:
191
+ # - team: "backend"
192
+ # - service.owner: "platform-team"
193
+ # - deployment.region: "us-east-1"
194
+ # ... plus any other attributes you set manually
195
+ ```
196
+
197
+ ### Use Cases
198
+
199
+ - **Team/Ownership Tagging**: `team=backend,owner=john@example.com`
200
+ - **Environment Metadata**: `environment=production,region=us-east-1,availability_zone=us-east-1a`
201
+ - **Version Tracking**: `version=1.2.3,build=12345,commit=abc123def`
202
+ - **Cost Attribution**: `cost_center=engineering,project=customer-api`
203
+ - **Multi-Tenancy**: `tenant=acme-corp,customer_tier=enterprise`
204
+
205
+ ### Difference from OTEL_RESOURCE_ATTRIBUTES
206
+
207
+ - **OTEL_RESOURCE_ATTRIBUTES**: Service-level metadata (set once, applies to the entire service instance)
208
+ - **OTEL_SPAN_ATTRIBUTES**: Span-level metadata (added to each individual span at creation time)
209
+
210
+ Both use the same format but serve different purposes in the OpenTelemetry data model.
211
+
149
212
  ### Exception Handling
150
213
 
151
214
  Spans automatically capture exceptions with:
@@ -521,6 +584,7 @@ pytest tests/test_usage.py -v
521
584
  pytest tests/test_pymysql_instrumentation.py -v
522
585
  pytest tests/test_metrics_and_logs.py -v
523
586
  pytest tests/test_decorators.py -v
587
+ pytest tests/test_span_attributes_processor.py -v
524
588
  ```
525
589
 
526
590
  Run with coverage:
@@ -537,6 +601,7 @@ The test suite includes:
537
601
  - **PyMySQL instrumentation tests** (`test_pymysql_instrumentation.py`): Database connection instrumentation, query tracing, helper functions
538
602
  - **Metrics and logs tests** (`test_metrics_and_logs.py`): Custom metrics creation (counter, histogram, gauge), logging levels (info, warning, debug, error)
539
603
  - **Decorators tests** (`test_decorators.py`): Lambda handler decorator, AWS message handler decorator, traces decorator, aws_message_span context manager
604
+ - **Span attributes processor tests** (`test_span_attributes_processor.py`): Automatic span attributes from OTEL_SPAN_ATTRIBUTES (31 tests)
540
605
 
541
606
  ## License
542
607
 
@@ -15,10 +15,12 @@ src/metrics.py
15
15
  src/otel_utils.py
16
16
  src/pymysql_instrumentation.py
17
17
  src/rebrandly_otel.py
18
+ src/span_attributes_processor.py
18
19
  src/traces.py
19
20
  tests/test_decorators.py
20
21
  tests/test_fastapi_support.py
21
22
  tests/test_flask_support.py
22
23
  tests/test_metrics_and_logs.py
23
24
  tests/test_pymysql_instrumentation.py
25
+ tests/test_span_attributes_processor.py
24
26
  tests/test_usage.py
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
5
5
 
6
6
  setuptools.setup(
7
7
  name="rebrandly_otel",
8
- version="0.2.16",
8
+ version="0.2.19",
9
9
  author="Antonio Romano",
10
10
  author_email="antonio@rebrandly.com",
11
11
  description="Python OTEL wrapper by Rebrandly",
@@ -0,0 +1,106 @@
1
+ """
2
+ Span Attributes Processor for Rebrandly OTEL SDK
3
+ Automatically adds attributes from OTEL_SPAN_ATTRIBUTES environment variable to all spans
4
+ """
5
+
6
+ import os
7
+ from typing import Optional
8
+ from opentelemetry.context import Context
9
+ from opentelemetry.sdk.trace import ReadableSpan, Span
10
+ from opentelemetry.sdk.trace.export import SpanProcessor
11
+
12
+
13
+ class SpanAttributesProcessor(SpanProcessor):
14
+ """
15
+ Span processor that automatically adds attributes from OTEL_SPAN_ATTRIBUTES
16
+ environment variable to all spans at creation time.
17
+ """
18
+
19
+ def __init__(self):
20
+ """Initialize the processor and parse OTEL_SPAN_ATTRIBUTES."""
21
+ self.name = 'SpanAttributesProcessor'
22
+ self.span_attributes = self._parse_span_attributes()
23
+
24
+ # Log parsed attributes in debug mode
25
+ if os.environ.get('OTEL_DEBUG', 'false').lower() == 'true' and self.span_attributes:
26
+ print(f'[SpanAttributesProcessor] Parsed OTEL_SPAN_ATTRIBUTES: {self.span_attributes}')
27
+
28
+ def _parse_span_attributes(self) -> dict:
29
+ """
30
+ Parse OTEL_SPAN_ATTRIBUTES environment variable.
31
+ Format: key1=value1,key2=value2
32
+
33
+ Returns:
34
+ Dictionary of parsed attributes as key-value pairs
35
+ """
36
+ attributes = {}
37
+ otel_span_attrs = os.environ.get('OTEL_SPAN_ATTRIBUTES', None)
38
+
39
+ if not otel_span_attrs or otel_span_attrs.strip() == '':
40
+ return attributes
41
+
42
+ try:
43
+ pairs = otel_span_attrs.split(',')
44
+ for attr in pairs:
45
+ trimmed_attr = attr.strip()
46
+ if trimmed_attr and '=' in trimmed_attr:
47
+ # Split on first '=' only, in case value contains '='
48
+ key, value = trimmed_attr.split('=', 1)
49
+ key = key.strip()
50
+ value = value.strip()
51
+ if key:
52
+ attributes[key] = value
53
+ except Exception as e:
54
+ print(f'[SpanAttributesProcessor] Warning: Invalid OTEL_SPAN_ATTRIBUTES value: {e}')
55
+
56
+ return attributes
57
+
58
+ def on_start(self, span: Span, parent_context: Optional[Context] = None) -> None:
59
+ """
60
+ Called when a span is started.
61
+ Adds configured attributes to the span.
62
+
63
+ Args:
64
+ span: The span that was just started
65
+ parent_context: The parent context (optional)
66
+ """
67
+ try:
68
+ # Add all parsed attributes to the span
69
+ if self.span_attributes:
70
+ for key, value in self.span_attributes.items():
71
+ span.set_attribute(key, value)
72
+ except Exception as e:
73
+ # Fail silently to avoid breaking the entire tracing pipeline
74
+ # Log only in debug mode to avoid noise
75
+ if os.environ.get('OTEL_DEBUG', 'false').lower() == 'true':
76
+ print(f'[SpanAttributesProcessor] Error processing span: {e}')
77
+
78
+ def on_end(self, span: ReadableSpan) -> None:
79
+ """
80
+ Called when a span is ended.
81
+ No-op for this processor.
82
+
83
+ Args:
84
+ span: The span that was just ended
85
+ """
86
+ pass
87
+
88
+ def shutdown(self) -> None:
89
+ """
90
+ Shutdown the processor.
91
+ No-op for this processor.
92
+ """
93
+ pass
94
+
95
+ def force_flush(self, timeout_millis: int = 30000) -> bool:
96
+ """
97
+ Force flush the processor.
98
+ No-op for this processor.
99
+
100
+ Args:
101
+ timeout_millis: Maximum time to wait for flush in milliseconds
102
+
103
+ Returns:
104
+ Always returns True as there's nothing to flush
105
+ """
106
+ return True
@@ -12,6 +12,7 @@ from opentelemetry.sdk.trace.export import (
12
12
  )
13
13
 
14
14
  from .otel_utils import *
15
+ from .span_attributes_processor import SpanAttributesProcessor
15
16
 
16
17
  class RebrandlyTracer:
17
18
  """Wrapper for OpenTelemetry tracing with Rebrandly-specific features."""
@@ -26,6 +27,9 @@ class RebrandlyTracer:
26
27
  # Create provider with resource
27
28
  self._provider = TracerProvider(resource=create_resource())
28
29
 
30
+ # Add span attributes processor to automatically add OTEL_SPAN_ATTRIBUTES to all spans
31
+ self._provider.add_span_processor(SpanAttributesProcessor())
32
+
29
33
  # Add console exporter for local debugging
30
34
  if is_otel_debug():
31
35
  console_exporter = ConsoleSpanExporter()
@@ -0,0 +1,409 @@
1
+ """
2
+ Tests for SpanAttributesProcessor
3
+ Tests automatic addition of attributes from OTEL_SPAN_ATTRIBUTES to all spans
4
+ """
5
+
6
+ import pytest
7
+ import os
8
+ from unittest.mock import MagicMock, patch
9
+ from src.span_attributes_processor import SpanAttributesProcessor
10
+
11
+
12
+ class TestSpanAttributesProcessor:
13
+ """Test SpanAttributesProcessor functionality"""
14
+
15
+ def setup_method(self):
16
+ """Setup method to clear environment before each test"""
17
+ # Save original environment
18
+ self.original_env = os.environ.get('OTEL_SPAN_ATTRIBUTES')
19
+ self.original_debug = os.environ.get('OTEL_DEBUG')
20
+
21
+ def teardown_method(self):
22
+ """Teardown method to restore environment after each test"""
23
+ # Restore original environment
24
+ if self.original_env is not None:
25
+ os.environ['OTEL_SPAN_ATTRIBUTES'] = self.original_env
26
+ elif 'OTEL_SPAN_ATTRIBUTES' in os.environ:
27
+ del os.environ['OTEL_SPAN_ATTRIBUTES']
28
+
29
+ if self.original_debug is not None:
30
+ os.environ['OTEL_DEBUG'] = self.original_debug
31
+ elif 'OTEL_DEBUG' in os.environ:
32
+ del os.environ['OTEL_DEBUG']
33
+
34
+ # ===== Processor Initialization Tests =====
35
+
36
+ def test_initialize_without_otel_span_attributes(self):
37
+ """Test processor initialization without OTEL_SPAN_ATTRIBUTES"""
38
+ if 'OTEL_SPAN_ATTRIBUTES' in os.environ:
39
+ del os.environ['OTEL_SPAN_ATTRIBUTES']
40
+
41
+ processor = SpanAttributesProcessor()
42
+
43
+ assert processor is not None
44
+ assert processor.name == 'SpanAttributesProcessor'
45
+ assert processor.span_attributes == {}
46
+
47
+ def test_parse_single_attribute(self):
48
+ """Test parsing a single attribute"""
49
+ os.environ['OTEL_SPAN_ATTRIBUTES'] = 'team=backend'
50
+
51
+ processor = SpanAttributesProcessor()
52
+
53
+ assert processor.span_attributes == {'team': 'backend'}
54
+
55
+ def test_parse_multiple_attributes(self):
56
+ """Test parsing multiple attributes"""
57
+ os.environ['OTEL_SPAN_ATTRIBUTES'] = 'team=backend,environment=production,version=1.2.3'
58
+
59
+ processor = SpanAttributesProcessor()
60
+
61
+ assert processor.span_attributes == {
62
+ 'team': 'backend',
63
+ 'environment': 'production',
64
+ 'version': '1.2.3'
65
+ }
66
+
67
+ def test_handle_attributes_with_spaces(self):
68
+ """Test handling attributes with spaces"""
69
+ os.environ['OTEL_SPAN_ATTRIBUTES'] = 'team = backend , environment = production'
70
+
71
+ processor = SpanAttributesProcessor()
72
+
73
+ assert processor.span_attributes == {
74
+ 'team': 'backend',
75
+ 'environment': 'production'
76
+ }
77
+
78
+ def test_handle_attributes_with_equals_in_values(self):
79
+ """Test handling attributes with equals signs in values"""
80
+ os.environ['OTEL_SPAN_ATTRIBUTES'] = 'url=http://example.com?foo=bar,key=value'
81
+
82
+ processor = SpanAttributesProcessor()
83
+
84
+ assert processor.span_attributes == {
85
+ 'url': 'http://example.com?foo=bar',
86
+ 'key': 'value'
87
+ }
88
+
89
+ def test_ignore_empty_string(self):
90
+ """Test ignoring empty string"""
91
+ os.environ['OTEL_SPAN_ATTRIBUTES'] = ''
92
+
93
+ processor = SpanAttributesProcessor()
94
+
95
+ assert processor.span_attributes == {}
96
+
97
+ def test_ignore_whitespace_only_string(self):
98
+ """Test ignoring whitespace-only string"""
99
+ os.environ['OTEL_SPAN_ATTRIBUTES'] = ' '
100
+
101
+ processor = SpanAttributesProcessor()
102
+
103
+ assert processor.span_attributes == {}
104
+
105
+ def test_ignore_malformed_attributes_without_equals(self):
106
+ """Test ignoring malformed attributes without equals"""
107
+ os.environ['OTEL_SPAN_ATTRIBUTES'] = 'team=backend,invalid,version=1.0'
108
+
109
+ processor = SpanAttributesProcessor()
110
+
111
+ assert processor.span_attributes == {
112
+ 'team': 'backend',
113
+ 'version': '1.0'
114
+ }
115
+
116
+ def test_ignore_attributes_with_empty_keys(self):
117
+ """Test ignoring attributes with empty keys"""
118
+ os.environ['OTEL_SPAN_ATTRIBUTES'] = '=value,team=backend'
119
+
120
+ processor = SpanAttributesProcessor()
121
+
122
+ assert processor.span_attributes == {'team': 'backend'}
123
+
124
+ def test_allow_empty_values(self):
125
+ """Test allowing empty values"""
126
+ os.environ['OTEL_SPAN_ATTRIBUTES'] = 'team=,environment=production'
127
+
128
+ processor = SpanAttributesProcessor()
129
+
130
+ assert processor.span_attributes == {
131
+ 'team': '',
132
+ 'environment': 'production'
133
+ }
134
+
135
+ def test_handle_trailing_commas(self):
136
+ """Test handling trailing commas"""
137
+ os.environ['OTEL_SPAN_ATTRIBUTES'] = 'team=backend,environment=production,'
138
+
139
+ processor = SpanAttributesProcessor()
140
+
141
+ assert processor.span_attributes == {
142
+ 'team': 'backend',
143
+ 'environment': 'production'
144
+ }
145
+
146
+ # ===== Span Processing Tests =====
147
+
148
+ def test_add_attributes_to_span_on_start(self):
149
+ """Test adding attributes to span on start"""
150
+ os.environ['OTEL_SPAN_ATTRIBUTES'] = 'team=backend,environment=production,version=1.2.3'
151
+
152
+ processor = SpanAttributesProcessor()
153
+
154
+ # Create mock span
155
+ mock_span = MagicMock()
156
+
157
+ # Call on_start
158
+ processor.on_start(mock_span)
159
+
160
+ # Verify set_attribute was called for each attribute
161
+ assert mock_span.set_attribute.call_count == 3
162
+ mock_span.set_attribute.assert_any_call('team', 'backend')
163
+ mock_span.set_attribute.assert_any_call('environment', 'production')
164
+ mock_span.set_attribute.assert_any_call('version', '1.2.3')
165
+
166
+ def test_no_attributes_added_when_none_configured(self):
167
+ """Test that no attributes are added when none configured"""
168
+ if 'OTEL_SPAN_ATTRIBUTES' in os.environ:
169
+ del os.environ['OTEL_SPAN_ATTRIBUTES']
170
+
171
+ processor = SpanAttributesProcessor()
172
+
173
+ # Create mock span
174
+ mock_span = MagicMock()
175
+
176
+ # Call on_start
177
+ processor.on_start(mock_span)
178
+
179
+ # Verify set_attribute was not called
180
+ mock_span.set_attribute.assert_not_called()
181
+
182
+ def test_handle_set_attribute_errors_gracefully(self):
183
+ """Test handling set_attribute errors gracefully"""
184
+ os.environ['OTEL_SPAN_ATTRIBUTES'] = 'team=backend'
185
+
186
+ processor = SpanAttributesProcessor()
187
+
188
+ # Create mock span that raises error
189
+ mock_span = MagicMock()
190
+ mock_span.set_attribute.side_effect = Exception('Test error')
191
+
192
+ # Should not raise
193
+ processor.on_start(mock_span)
194
+
195
+ def test_on_end_does_nothing(self):
196
+ """Test that on_end does nothing"""
197
+ os.environ['OTEL_SPAN_ATTRIBUTES'] = 'team=backend'
198
+
199
+ processor = SpanAttributesProcessor()
200
+
201
+ # Create mock span
202
+ mock_span = MagicMock()
203
+
204
+ # Should not raise
205
+ processor.on_end(mock_span)
206
+
207
+ # ===== Lifecycle Methods Tests =====
208
+
209
+ def test_shutdown_method(self):
210
+ """Test shutdown method"""
211
+ os.environ['OTEL_SPAN_ATTRIBUTES'] = 'team=backend'
212
+
213
+ processor = SpanAttributesProcessor()
214
+
215
+ # Should not raise
216
+ processor.shutdown()
217
+
218
+ def test_force_flush_method(self):
219
+ """Test force_flush method"""
220
+ os.environ['OTEL_SPAN_ATTRIBUTES'] = 'team=backend'
221
+
222
+ processor = SpanAttributesProcessor()
223
+
224
+ # Should return True
225
+ result = processor.force_flush()
226
+ assert result is True
227
+
228
+ def test_force_flush_with_timeout(self):
229
+ """Test force_flush with custom timeout"""
230
+ os.environ['OTEL_SPAN_ATTRIBUTES'] = 'team=backend'
231
+
232
+ processor = SpanAttributesProcessor()
233
+
234
+ # Should return True regardless of timeout
235
+ result = processor.force_flush(timeout_millis=10000)
236
+ assert result is True
237
+
238
+ # ===== Debug Mode Tests =====
239
+
240
+ @patch('builtins.print')
241
+ def test_log_parsed_attributes_when_debug_true(self, mock_print):
242
+ """Test logging parsed attributes when OTEL_DEBUG is true"""
243
+ os.environ['OTEL_DEBUG'] = 'true'
244
+ os.environ['OTEL_SPAN_ATTRIBUTES'] = 'team=backend,env=prod'
245
+
246
+ processor = SpanAttributesProcessor()
247
+
248
+ # Verify print was called with correct message
249
+ mock_print.assert_called_with(
250
+ '[SpanAttributesProcessor] Parsed OTEL_SPAN_ATTRIBUTES: {\'team\': \'backend\', \'env\': \'prod\'}'
251
+ )
252
+
253
+ @patch('builtins.print')
254
+ def test_no_log_when_debug_false(self, mock_print):
255
+ """Test not logging when OTEL_DEBUG is false"""
256
+ os.environ['OTEL_DEBUG'] = 'false'
257
+ os.environ['OTEL_SPAN_ATTRIBUTES'] = 'team=backend'
258
+
259
+ processor = SpanAttributesProcessor()
260
+
261
+ # Verify print was not called
262
+ mock_print.assert_not_called()
263
+
264
+ @patch('builtins.print')
265
+ def test_no_log_when_attributes_empty(self, mock_print):
266
+ """Test not logging when attributes are empty"""
267
+ os.environ['OTEL_DEBUG'] = 'true'
268
+ if 'OTEL_SPAN_ATTRIBUTES' in os.environ:
269
+ del os.environ['OTEL_SPAN_ATTRIBUTES']
270
+
271
+ processor = SpanAttributesProcessor()
272
+
273
+ # Verify print was not called
274
+ mock_print.assert_not_called()
275
+
276
+ # ===== Error Handling Tests =====
277
+
278
+ def test_handle_none_span_gracefully(self):
279
+ """Test handling None span gracefully"""
280
+ os.environ['OTEL_SPAN_ATTRIBUTES'] = 'team=backend'
281
+
282
+ processor = SpanAttributesProcessor()
283
+
284
+ # Should not raise
285
+ processor.on_start(None)
286
+
287
+ @patch('builtins.print')
288
+ def test_log_errors_in_debug_mode(self, mock_print):
289
+ """Test logging errors in debug mode when set_attribute fails"""
290
+ os.environ['OTEL_DEBUG'] = 'true'
291
+ os.environ['OTEL_SPAN_ATTRIBUTES'] = 'team=backend'
292
+
293
+ processor = SpanAttributesProcessor()
294
+
295
+ # Create mock span that raises error
296
+ mock_span = MagicMock()
297
+ mock_span.set_attribute.side_effect = Exception('Test error')
298
+
299
+ # Call on_start
300
+ processor.on_start(mock_span)
301
+
302
+ # Verify error was logged
303
+ assert any('[SpanAttributesProcessor] Error processing span:' in str(call)
304
+ for call in mock_print.call_args_list)
305
+
306
+ # ===== Integration Tests =====
307
+
308
+ def test_processor_with_real_span(self):
309
+ """Test processor works with real span creation"""
310
+ from opentelemetry import trace
311
+ from opentelemetry.sdk.trace import TracerProvider
312
+ from opentelemetry.sdk.trace.export import SimpleSpanProcessor, ConsoleSpanExporter
313
+
314
+ os.environ['OTEL_SPAN_ATTRIBUTES'] = 'test=integration'
315
+
316
+ # Create provider and add processor
317
+ provider = TracerProvider()
318
+ provider.add_span_processor(SpanAttributesProcessor())
319
+
320
+ # Set as global
321
+ trace.set_tracer_provider(provider)
322
+
323
+ # Create tracer and span
324
+ tracer = trace.get_tracer(__name__)
325
+ with tracer.start_as_current_span('test-span') as span:
326
+ # Span should have the attribute
327
+ assert span is not None
328
+ # We can't directly verify attributes on the span object,
329
+ # but we can verify no errors occurred
330
+
331
+ def test_processor_name_property(self):
332
+ """Test processor has correct name property"""
333
+ processor = SpanAttributesProcessor()
334
+ assert processor.name == 'SpanAttributesProcessor'
335
+
336
+ def test_multiple_processors_can_coexist(self):
337
+ """Test multiple processors can coexist"""
338
+ from opentelemetry.sdk.trace import TracerProvider
339
+
340
+ os.environ['OTEL_SPAN_ATTRIBUTES'] = 'test=multi'
341
+
342
+ provider = TracerProvider()
343
+
344
+ # Add multiple span attributes processors
345
+ processor1 = SpanAttributesProcessor()
346
+ processor2 = SpanAttributesProcessor()
347
+
348
+ provider.add_span_processor(processor1)
349
+ provider.add_span_processor(processor2)
350
+
351
+ # Should not raise
352
+ assert True
353
+
354
+ # ===== Special Character Tests =====
355
+
356
+ def test_handle_special_characters_in_values(self):
357
+ """Test handling special characters in values"""
358
+ os.environ['OTEL_SPAN_ATTRIBUTES'] = 'msg=Hello, World!,path=/api/v1/users'
359
+
360
+ processor = SpanAttributesProcessor()
361
+
362
+ assert processor.span_attributes == {
363
+ 'msg': 'Hello',
364
+ 'path': '/api/v1/users'
365
+ }
366
+
367
+ def test_handle_unicode_characters(self):
368
+ """Test handling unicode characters"""
369
+ os.environ['OTEL_SPAN_ATTRIBUTES'] = 'emoji=🚀,name=José'
370
+
371
+ processor = SpanAttributesProcessor()
372
+
373
+ assert processor.span_attributes == {
374
+ 'emoji': '🚀',
375
+ 'name': 'José'
376
+ }
377
+
378
+ def test_handle_numeric_values_as_strings(self):
379
+ """Test that numeric values are treated as strings"""
380
+ os.environ['OTEL_SPAN_ATTRIBUTES'] = 'port=8080,version=1.0'
381
+
382
+ processor = SpanAttributesProcessor()
383
+
384
+ assert processor.span_attributes == {
385
+ 'port': '8080',
386
+ 'version': '1.0'
387
+ }
388
+
389
+ # ===== Edge Case Tests =====
390
+
391
+ def test_very_long_attribute_value(self):
392
+ """Test handling very long attribute values"""
393
+ long_value = 'a' * 1000
394
+ os.environ['OTEL_SPAN_ATTRIBUTES'] = f'long={long_value}'
395
+
396
+ processor = SpanAttributesProcessor()
397
+
398
+ assert processor.span_attributes == {'long': long_value}
399
+
400
+ def test_many_attributes(self):
401
+ """Test handling many attributes"""
402
+ attrs = ','.join([f'key{i}=value{i}' for i in range(50)])
403
+ os.environ['OTEL_SPAN_ATTRIBUTES'] = attrs
404
+
405
+ processor = SpanAttributesProcessor()
406
+
407
+ assert len(processor.span_attributes) == 50
408
+ assert processor.span_attributes['key0'] == 'value0'
409
+ assert processor.span_attributes['key49'] == 'value49'
File without changes