rebrandly-otel 0.1.20__py3-none-any.whl → 0.1.22__py3-none-any.whl

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.

@@ -2,9 +2,11 @@
2
2
  from .rebrandly_otel import *
3
3
  from .flask_support import setup_flask
4
4
  from .fastapi_support import setup_fastapi
5
+ from .pymysql_instrumentation import instrument_pymysql
5
6
 
6
7
  # Explicitly define what's available
7
8
  __all__ = [
9
+ 'otel',
8
10
  'lambda_handler',
9
11
  'span',
10
12
  'aws_message_span',
@@ -16,5 +18,6 @@ __all__ = [
16
18
  'aws_message_handler',
17
19
  'shutdown',
18
20
  'setup_flask',
19
- 'setup_fastapi'
21
+ 'setup_fastapi',
22
+ 'instrument_pymysql'
20
23
  ]
rebrandly_otel/metrics.py CHANGED
@@ -111,7 +111,7 @@ class RebrandlyMeter:
111
111
  histogram_view = View(
112
112
  instrument_type=Histogram,
113
113
  instrument_name="*",
114
- aggregation=ExplicitBucketHistogramAggregation((0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10)) # todo <-- define buckets
114
+ aggregation=ExplicitBucketHistogramAggregation((0.001, 0.004, 0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5)) # todo <-- define buckets
115
115
  )
116
116
  views.append(histogram_view)
117
117
 
@@ -22,7 +22,7 @@ def create_resource(name: str = None, version: str = None) -> Resource:
22
22
  service_attributes.SERVICE_NAME: name,
23
23
  service_attributes.SERVICE_VERSION: version,
24
24
  process_attributes.PROCESS_RUNTIME_VERSION: f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
25
- deployment_attributes.DEPLOYMENT_ENVIRONMENT: os.environ.get('ENV', os.environ.get('ENVIRONMENT', os.environ.get('NODE_ENV', 'local')))
25
+ deployment_attributes.DEPLOYMENT_ENVIRONMENT_NAME: os.environ.get('ENV', os.environ.get('ENVIRONMENT', os.environ.get('NODE_ENV', 'local')))
26
26
  }
27
27
  )
28
28
  return resource
@@ -0,0 +1,206 @@
1
+ """
2
+ PyMySQL instrumentation for Rebrandly OTEL SDK
3
+ Provides query tracing and slow query detection
4
+ """
5
+
6
+ import os
7
+ import time
8
+ import functools
9
+ from opentelemetry.trace import Status, StatusCode, SpanKind
10
+
11
+ # Environment configuration
12
+ SLOW_QUERY_THRESHOLD_MS = int(os.getenv('PYMYSQL_SLOW_QUERY_THRESHOLD_MS', '1500'))
13
+ MAX_QUERY_LENGTH = 2000 # Truncate long queries
14
+
15
+
16
+ def instrument_pymysql(otel_instance, connection, options=None):
17
+ """
18
+ Instrument a PyMySQL connection for OpenTelemetry tracing
19
+
20
+ Args:
21
+ otel_instance: The RebrandlyOTEL instance
22
+ connection: The PyMySQL connection to instrument
23
+ options: Configuration options dict with:
24
+ - slow_query_threshold_ms: Threshold for slow query detection (default: 1500ms)
25
+ - capture_bindings: Include query bindings in spans (default: False for security)
26
+
27
+ Returns:
28
+ The instrumented connection
29
+ """
30
+ if options is None:
31
+ options = {}
32
+
33
+ slow_query_threshold_ms = options.get('slow_query_threshold_ms', SLOW_QUERY_THRESHOLD_MS)
34
+ capture_bindings = options.get('capture_bindings', False)
35
+
36
+ if not connection:
37
+ print('[Rebrandly OTEL PyMySQL] No connection provided for instrumentation')
38
+ return connection
39
+
40
+ if not otel_instance or not hasattr(otel_instance, 'tracer'):
41
+ print('[Rebrandly OTEL PyMySQL] No valid OTEL instance provided for instrumentation')
42
+ return connection
43
+
44
+ # Get tracer from RebrandlyOTEL instance
45
+ tracer = otel_instance.tracer
46
+
47
+ # Wrap the cursor method to return instrumented cursors
48
+ original_cursor = connection.cursor
49
+
50
+ def instrumented_cursor(*args, **kwargs):
51
+ cursor = original_cursor(*args, **kwargs)
52
+ return _instrument_cursor(cursor, tracer, slow_query_threshold_ms, capture_bindings)
53
+
54
+ connection.cursor = instrumented_cursor
55
+
56
+ return connection
57
+
58
+
59
+ def _instrument_cursor(cursor, tracer, slow_query_threshold_ms, capture_bindings):
60
+ """
61
+ Instrument a cursor's execute methods
62
+ """
63
+ original_execute = cursor.execute
64
+ original_executemany = cursor.executemany
65
+
66
+ @functools.wraps(original_execute)
67
+ def instrumented_execute(query, args=None):
68
+ return _trace_query(
69
+ original_execute,
70
+ tracer,
71
+ slow_query_threshold_ms,
72
+ capture_bindings,
73
+ query,
74
+ args,
75
+ many=False
76
+ )
77
+
78
+ @functools.wraps(original_executemany)
79
+ def instrumented_executemany(query, args):
80
+ return _trace_query(
81
+ original_executemany,
82
+ tracer,
83
+ slow_query_threshold_ms,
84
+ capture_bindings,
85
+ query,
86
+ args,
87
+ many=True
88
+ )
89
+
90
+ cursor.execute = instrumented_execute
91
+ cursor.executemany = instrumented_executemany
92
+
93
+ return cursor
94
+
95
+
96
+ def _trace_query(func, tracer, slow_query_threshold_ms, capture_bindings, query, args, many=False):
97
+ """
98
+ Trace a query execution with OpenTelemetry
99
+ """
100
+ operation = _extract_operation(query)
101
+ truncated_query = _truncate_query(query)
102
+
103
+ # Start span
104
+ span_name = f"pymysql.{'executemany' if many else 'execute'}"
105
+
106
+ with tracer.start_span(
107
+ name=span_name,
108
+ kind=SpanKind.CLIENT
109
+ ) as span:
110
+ # Set database attributes
111
+ span.set_attribute('db.system', 'mysql')
112
+ span.set_attribute('db.operation.name', operation)
113
+ span.set_attribute('db.statement', truncated_query)
114
+
115
+ # Add bindings if enabled (be cautious with sensitive data)
116
+ if capture_bindings and args:
117
+ if many:
118
+ span.set_attribute('db.bindings_count', len(args))
119
+ else:
120
+ span.set_attribute('db.bindings', str(args))
121
+
122
+ start_time = time.time()
123
+
124
+ try:
125
+ # Execute the query
126
+ result = func(query, args)
127
+
128
+ # Calculate duration
129
+ duration_ms = (time.time() - start_time) * 1000
130
+ span.set_attribute('db.duration_ms', duration_ms)
131
+
132
+ # Check for slow query
133
+ if duration_ms >= slow_query_threshold_ms:
134
+ span.set_attribute('db.slow_query', True)
135
+ span.add_event('slow_query_detected', {
136
+ 'db.duration_ms': duration_ms,
137
+ 'db.threshold_ms': slow_query_threshold_ms
138
+ })
139
+
140
+ # Set success status
141
+ span.set_status(Status(StatusCode.OK))
142
+
143
+ return result
144
+
145
+ except Exception as error:
146
+ # Calculate duration even on error
147
+ duration_ms = (time.time() - start_time) * 1000
148
+ span.set_attribute('db.duration_ms', duration_ms)
149
+
150
+ # Record exception
151
+ span.record_exception(error)
152
+ span.set_status(Status(StatusCode.ERROR, str(error)))
153
+
154
+ raise
155
+
156
+
157
+ def _extract_operation(sql):
158
+ """
159
+ Extract operation type from SQL statement
160
+
161
+ Args:
162
+ sql: SQL query string
163
+
164
+ Returns:
165
+ Operation type (SELECT, INSERT, UPDATE, etc.)
166
+ """
167
+ if not sql:
168
+ return 'unknown'
169
+
170
+ normalized = sql.strip().upper()
171
+
172
+ if normalized.startswith('SELECT'):
173
+ return 'SELECT'
174
+ if normalized.startswith('INSERT'):
175
+ return 'INSERT'
176
+ if normalized.startswith('UPDATE'):
177
+ return 'UPDATE'
178
+ if normalized.startswith('DELETE'):
179
+ return 'DELETE'
180
+ if normalized.startswith('CREATE'):
181
+ return 'CREATE'
182
+ if normalized.startswith('DROP'):
183
+ return 'DROP'
184
+ if normalized.startswith('ALTER'):
185
+ return 'ALTER'
186
+ if normalized.startswith('TRUNCATE'):
187
+ return 'TRUNCATE'
188
+
189
+ return 'unknown'
190
+
191
+
192
+ def _truncate_query(sql):
193
+ """
194
+ Truncate long queries for span attributes
195
+
196
+ Args:
197
+ sql: SQL query string
198
+
199
+ Returns:
200
+ Truncated query
201
+ """
202
+ if not sql:
203
+ return ''
204
+ if len(sql) <= MAX_QUERY_LENGTH:
205
+ return sql
206
+ return sql[:MAX_QUERY_LENGTH] + '... [truncated]'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rebrandly_otel
3
- Version: 0.1.20
3
+ Version: 0.1.22
4
4
  Summary: Python OTEL wrapper by Rebrandly
5
5
  Home-page: https://github.com/rebrandly/rebrandly-otel-python
6
6
  Author: Antonio Romano
@@ -447,6 +447,52 @@ if __name__ == "__main__":
447
447
  uvicorn.run(app, host="0.0.0.0", port=8000)
448
448
  ```
449
449
 
450
+ ### PyMySQL Database Instrumentation
451
+
452
+ The SDK provides connection-level instrumentation for PyMySQL that automatically traces all queries without requiring you to instrument each query individually.
453
+
454
+ ```python
455
+ import pymysql
456
+ from rebrandly_otel import otel, logger, instrument_pymysql
457
+
458
+ # Initialize OTEL
459
+ otel.initialize()
460
+
461
+ # Create and instrument your connection
462
+ connection = pymysql.connect(
463
+ host='localhost',
464
+ user='your_user',
465
+ password='your_password',
466
+ database='your_database'
467
+ )
468
+
469
+ # Instrument the connection - all queries are now automatically traced
470
+ connection = instrument_pymysql(otel, connection, options={
471
+ 'slow_query_threshold_ms': 1000, # Queries over 1s flagged as slow
472
+ 'capture_bindings': False # Set True to capture query parameters
473
+ })
474
+
475
+ # Use normally - all queries automatically traced
476
+ with connection.cursor() as cursor:
477
+ cursor.execute("SELECT * FROM users WHERE id = %s", (123,))
478
+ result = cursor.fetchone()
479
+ logger.info(f"Found user: {result}")
480
+
481
+ connection.close()
482
+ otel.force_flush()
483
+ ```
484
+
485
+ Features:
486
+ - Automatic span creation for all queries
487
+ - Query operation detection (SELECT, INSERT, UPDATE, etc.)
488
+ - Slow query detection and flagging
489
+ - Duration tracking
490
+ - Error recording with exception details
491
+ - Optional query parameter capture (disabled by default for security)
492
+
493
+ Environment configuration:
494
+ - `PYMYSQL_SLOW_QUERY_THRESHOLD_MS`: Threshold for slow query detection (default: 1500ms)
495
+
450
496
  ### More examples
451
497
  You can find More examples [here](examples)
452
498
 
@@ -0,0 +1,14 @@
1
+ rebrandly_otel/__init__.py,sha256=dEcTvHbhNNp0XD_7jojxeHXNa1QDCI9HxayeICaK_WY,491
2
+ rebrandly_otel/fastapi_support.py,sha256=RuBBZJuzr3osBDrkHZ0oQPV70pmvnqxTfBBDVFBFQlo,8019
3
+ rebrandly_otel/flask_support.py,sha256=cUVMGTjN6N8xZD4Zyng2LRWhNj62C5nmTH91SnYBp2A,6072
4
+ rebrandly_otel/logs.py,sha256=5byeN-cDmBRpeZDw9IBz_vuiJm3wsGEbcAk5pwYHNAU,3791
5
+ rebrandly_otel/metrics.py,sha256=Tf21BwSVK6eLPG2hz55gNpwdPq85uqAx4bttLO6rnq8,7542
6
+ rebrandly_otel/otel_utils.py,sha256=bP6Xhel4g7gbQWzCUlABWJfBSJaveKU1fhSOngcfzc4,3577
7
+ rebrandly_otel/pymysql_instrumentation.py,sha256=4MNIfqNQlMtOcmYLy5bq8FP0KoEw72bcZZqBdG6xupM,5955
8
+ rebrandly_otel/rebrandly_otel.py,sha256=wT1GiOQQMZl2sVG8MGJ9qM9gx_zRwS7U5oAe77RbJdk,21618
9
+ rebrandly_otel/traces.py,sha256=JY_3RWbzpUxzEx3GqTVgggsyA2DB4oR-zDftIFFJha4,7174
10
+ rebrandly_otel-0.1.22.dist-info/licenses/LICENSE,sha256=KMXHvTwP62S2q-SG7CFfMU_09rUwxiqlM0izaYGdcgY,1103
11
+ rebrandly_otel-0.1.22.dist-info/METADATA,sha256=FNsgak3kyt88nzZOtte1RdFNpBDbScDaQcIC6a3DSBw,14293
12
+ rebrandly_otel-0.1.22.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
13
+ rebrandly_otel-0.1.22.dist-info/top_level.txt,sha256=26PSC1gjVUl8tTH5QfKbFevjVV4E2yojoukEfiTScvM,15
14
+ rebrandly_otel-0.1.22.dist-info/RECORD,,
@@ -1,13 +0,0 @@
1
- rebrandly_otel/__init__.py,sha256=tkZQJo5hR4FJ4dIRc-3b_YGxGo-uq-DsiSz8shdac-k,397
2
- rebrandly_otel/fastapi_support.py,sha256=RuBBZJuzr3osBDrkHZ0oQPV70pmvnqxTfBBDVFBFQlo,8019
3
- rebrandly_otel/flask_support.py,sha256=cUVMGTjN6N8xZD4Zyng2LRWhNj62C5nmTH91SnYBp2A,6072
4
- rebrandly_otel/logs.py,sha256=5byeN-cDmBRpeZDw9IBz_vuiJm3wsGEbcAk5pwYHNAU,3791
5
- rebrandly_otel/metrics.py,sha256=8aAqdr3SAcX_rVivTl_aHeD_BRByt-Qnfij_51Y7Fn0,7561
6
- rebrandly_otel/otel_utils.py,sha256=DodoBBqzDlo3pC5TLmWT2eAAE8NvW6MxxAoBrfVaxc4,3572
7
- rebrandly_otel/rebrandly_otel.py,sha256=wT1GiOQQMZl2sVG8MGJ9qM9gx_zRwS7U5oAe77RbJdk,21618
8
- rebrandly_otel/traces.py,sha256=JY_3RWbzpUxzEx3GqTVgggsyA2DB4oR-zDftIFFJha4,7174
9
- rebrandly_otel-0.1.20.dist-info/licenses/LICENSE,sha256=KMXHvTwP62S2q-SG7CFfMU_09rUwxiqlM0izaYGdcgY,1103
10
- rebrandly_otel-0.1.20.dist-info/METADATA,sha256=II9R2GM1UWTYg-JmUbQewcXR9J_jww3eiKVOApklmkM,12854
11
- rebrandly_otel-0.1.20.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
12
- rebrandly_otel-0.1.20.dist-info/top_level.txt,sha256=26PSC1gjVUl8tTH5QfKbFevjVV4E2yojoukEfiTScvM,15
13
- rebrandly_otel-0.1.20.dist-info/RECORD,,