rebrandly-otel 0.1.20__tar.gz → 0.1.22__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.

@@ -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
 
@@ -426,6 +426,52 @@ if __name__ == "__main__":
426
426
  uvicorn.run(app, host="0.0.0.0", port=8000)
427
427
  ```
428
428
 
429
+ ### PyMySQL Database Instrumentation
430
+
431
+ The SDK provides connection-level instrumentation for PyMySQL that automatically traces all queries without requiring you to instrument each query individually.
432
+
433
+ ```python
434
+ import pymysql
435
+ from rebrandly_otel import otel, logger, instrument_pymysql
436
+
437
+ # Initialize OTEL
438
+ otel.initialize()
439
+
440
+ # Create and instrument your connection
441
+ connection = pymysql.connect(
442
+ host='localhost',
443
+ user='your_user',
444
+ password='your_password',
445
+ database='your_database'
446
+ )
447
+
448
+ # Instrument the connection - all queries are now automatically traced
449
+ connection = instrument_pymysql(otel, connection, options={
450
+ 'slow_query_threshold_ms': 1000, # Queries over 1s flagged as slow
451
+ 'capture_bindings': False # Set True to capture query parameters
452
+ })
453
+
454
+ # Use normally - all queries automatically traced
455
+ with connection.cursor() as cursor:
456
+ cursor.execute("SELECT * FROM users WHERE id = %s", (123,))
457
+ result = cursor.fetchone()
458
+ logger.info(f"Found user: {result}")
459
+
460
+ connection.close()
461
+ otel.force_flush()
462
+ ```
463
+
464
+ Features:
465
+ - Automatic span creation for all queries
466
+ - Query operation detection (SELECT, INSERT, UPDATE, etc.)
467
+ - Slow query detection and flagging
468
+ - Duration tracking
469
+ - Error recording with exception details
470
+ - Optional query parameter capture (disabled by default for security)
471
+
472
+ Environment configuration:
473
+ - `PYMYSQL_SLOW_QUERY_THRESHOLD_MS`: Threshold for slow query detection (default: 1500ms)
474
+
429
475
  ### More examples
430
476
  You can find More examples [here](examples)
431
477
 
@@ -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
 
@@ -11,6 +11,7 @@ src/flask_support.py
11
11
  src/logs.py
12
12
  src/metrics.py
13
13
  src/otel_utils.py
14
+ src/pymysql_instrumentation.py
14
15
  src/rebrandly_otel.py
15
16
  src/traces.py
16
17
  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.1.20",
8
+ version="0.1.22",
9
9
  author="Antonio Romano",
10
10
  author_email="antonio@rebrandly.com",
11
11
  description="Python OTEL wrapper by Rebrandly",
@@ -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
  ]
@@ -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]'
File without changes