rebrandly-otel 0.1.20__py3-none-any.whl → 0.1.23__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.23
4
4
  Summary: Python OTEL wrapper by Rebrandly
5
5
  Home-page: https://github.com/rebrandly/rebrandly-otel-python
6
6
  Author: Antonio Romano
@@ -10,6 +10,10 @@ Classifier: License :: OSI Approved :: MIT License
10
10
  Classifier: Operating System :: OS Independent
11
11
  Description-Content-Type: text/markdown
12
12
  License-File: LICENSE
13
+ Requires-Dist: opentelemetry-api>=1.34.0
14
+ Requires-Dist: opentelemetry-sdk>=1.34.0
15
+ Requires-Dist: opentelemetry-exporter-otlp>=1.34.0
16
+ Requires-Dist: psutil>=5.0.0
13
17
  Dynamic: author
14
18
  Dynamic: author-email
15
19
  Dynamic: classifier
@@ -17,6 +21,7 @@ Dynamic: description
17
21
  Dynamic: description-content-type
18
22
  Dynamic: home-page
19
23
  Dynamic: license-file
24
+ Dynamic: requires-dist
20
25
  Dynamic: summary
21
26
 
22
27
  # Rebrandly OpenTelemetry SDK for Python
@@ -447,6 +452,52 @@ if __name__ == "__main__":
447
452
  uvicorn.run(app, host="0.0.0.0", port=8000)
448
453
  ```
449
454
 
455
+ ### PyMySQL Database Instrumentation
456
+
457
+ The SDK provides connection-level instrumentation for PyMySQL that automatically traces all queries without requiring you to instrument each query individually.
458
+
459
+ ```python
460
+ import pymysql
461
+ from rebrandly_otel import otel, logger, instrument_pymysql
462
+
463
+ # Initialize OTEL
464
+ otel.initialize()
465
+
466
+ # Create and instrument your connection
467
+ connection = pymysql.connect(
468
+ host='localhost',
469
+ user='your_user',
470
+ password='your_password',
471
+ database='your_database'
472
+ )
473
+
474
+ # Instrument the connection - all queries are now automatically traced
475
+ connection = instrument_pymysql(otel, connection, options={
476
+ 'slow_query_threshold_ms': 1000, # Queries over 1s flagged as slow
477
+ 'capture_bindings': False # Set True to capture query parameters
478
+ })
479
+
480
+ # Use normally - all queries automatically traced
481
+ with connection.cursor() as cursor:
482
+ cursor.execute("SELECT * FROM users WHERE id = %s", (123,))
483
+ result = cursor.fetchone()
484
+ logger.info(f"Found user: {result}")
485
+
486
+ connection.close()
487
+ otel.force_flush()
488
+ ```
489
+
490
+ Features:
491
+ - Automatic span creation for all queries
492
+ - Query operation detection (SELECT, INSERT, UPDATE, etc.)
493
+ - Slow query detection and flagging
494
+ - Duration tracking
495
+ - Error recording with exception details
496
+ - Optional query parameter capture (disabled by default for security)
497
+
498
+ Environment configuration:
499
+ - `PYMYSQL_SLOW_QUERY_THRESHOLD_MS`: Threshold for slow query detection (default: 1500ms)
500
+
450
501
  ### More examples
451
502
  You can find More examples [here](examples)
452
503
 
@@ -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.23.dist-info/licenses/LICENSE,sha256=KMXHvTwP62S2q-SG7CFfMU_09rUwxiqlM0izaYGdcgY,1103
11
+ rebrandly_otel-0.1.23.dist-info/METADATA,sha256=0aW8pxbWwRsR3-QN1kYCsiiC4BsYwJ-uf_iO--tC9mc,14478
12
+ rebrandly_otel-0.1.23.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
13
+ rebrandly_otel-0.1.23.dist-info/top_level.txt,sha256=26PSC1gjVUl8tTH5QfKbFevjVV4E2yojoukEfiTScvM,15
14
+ rebrandly_otel-0.1.23.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,,