azpaddypy 0.1.0__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.
@@ -0,0 +1,9 @@
1
+ """
2
+ AzPaddyPy - A standardized Python package for Azure cloud services integration.
3
+ """
4
+
5
+ __version__ = "0.1.0"
6
+
7
+ from azpaddypy.mgmt import logging
8
+
9
+ __all__ = ["AzureLogger"]
@@ -0,0 +1,534 @@
1
+ import logging
2
+ import os
3
+ import json
4
+ import functools
5
+ import time
6
+ from typing import Optional, Dict, Any, Union, List
7
+ from datetime import datetime
8
+ from azure.monitor.opentelemetry import configure_azure_monitor
9
+ from opentelemetry import trace
10
+ from opentelemetry.trace import Status, StatusCode, Span
11
+ from opentelemetry.sdk.trace import TracerProvider
12
+ from opentelemetry.sdk.resources import Resource
13
+
14
+
15
+ class AzureLogger:
16
+ """
17
+ Comprehensive logging class for Azure Functions and Azure Web Apps
18
+ using Azure Monitor OpenTelemetry integration with advanced tracing capabilities.
19
+ """
20
+
21
+ def __init__(
22
+ self,
23
+ service_name: str,
24
+ service_version: str = "1.0.0",
25
+ connection_string: Optional[str] = None,
26
+ log_level: int = logging.INFO,
27
+ enable_console_logging: bool = True,
28
+ custom_resource_attributes: Optional[Dict[str, str]] = None,
29
+ ):
30
+ """
31
+ Initialize the Azure Logger with OpenTelemetry tracing
32
+
33
+ Args:
34
+ service_name: Name of your service/application
35
+ service_version: Version of your service
36
+ connection_string: Application Insights connection string
37
+ log_level: Logging level (default: INFO)
38
+ enable_console_logging: Enable console output for local development
39
+ custom_resource_attributes: Additional resource attributes
40
+ """
41
+ self.service_name = service_name
42
+ self.service_version = service_version
43
+ self.connection_string = connection_string or os.getenv(
44
+ "APPLICATIONINSIGHTS_CONNECTION_STRING"
45
+ )
46
+
47
+ # Prepare resource attributes
48
+ resource_attributes = {
49
+ "service.name": service_name,
50
+ "service.version": service_version,
51
+ "service.instance.id": os.getenv("WEBSITE_INSTANCE_ID", "local"),
52
+ }
53
+
54
+ if custom_resource_attributes:
55
+ resource_attributes.update(custom_resource_attributes)
56
+
57
+ # Configure Azure Monitor if connection string is available
58
+ if self.connection_string:
59
+ try:
60
+ configure_azure_monitor(
61
+ connection_string=self.connection_string,
62
+ resource_attributes=resource_attributes,
63
+ enable_live_metrics=True,
64
+ )
65
+ self._telemetry_enabled = True
66
+ except Exception as e:
67
+ print(f"Warning: Failed to configure Azure Monitor: {e}")
68
+ self._telemetry_enabled = False
69
+ else:
70
+ self._telemetry_enabled = False
71
+ print(
72
+ "Warning: No Application Insights connection string found. Telemetry disabled."
73
+ )
74
+
75
+ # Set up logger without instrumentor
76
+ self.logger = logging.getLogger(service_name)
77
+ self.logger.setLevel(log_level)
78
+
79
+ # Clear existing handlers to avoid duplicates
80
+ self.logger.handlers.clear()
81
+
82
+ # Add console handler for local development
83
+ if enable_console_logging:
84
+ self._setup_console_handler()
85
+
86
+ # Get tracer for manual span creation
87
+ self.tracer = trace.get_tracer(__name__)
88
+
89
+ # Initialize correlation context
90
+ self._correlation_id = None
91
+
92
+ self.info(
93
+ f"Azure Logger initialized for service '{service_name}' v{service_version}"
94
+ )
95
+
96
+ def _setup_console_handler(self):
97
+ """Setup console logging handler with structured format"""
98
+ console_handler = logging.StreamHandler()
99
+ formatter = logging.Formatter(
100
+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s - %(pathname)s:%(lineno)d"
101
+ )
102
+ console_handler.setFormatter(formatter)
103
+ self.logger.addHandler(console_handler)
104
+
105
+ def set_correlation_id(self, correlation_id: str):
106
+ """Set correlation ID for request tracking"""
107
+ self._correlation_id = correlation_id
108
+
109
+ def get_correlation_id(self) -> Optional[str]:
110
+ """Get current correlation ID"""
111
+ return self._correlation_id
112
+
113
+ def _enhance_extra(self, extra: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
114
+ """Enhance log extra data with correlation and service info"""
115
+ enhanced_extra = {
116
+ "service_name": self.service_name,
117
+ "service_version": self.service_version,
118
+ "timestamp": datetime.utcnow().isoformat(),
119
+ }
120
+
121
+ if self._correlation_id:
122
+ enhanced_extra["correlation_id"] = self._correlation_id
123
+
124
+ # Add span context if available
125
+ current_span = trace.get_current_span()
126
+ if current_span and current_span.is_recording():
127
+ span_context = current_span.get_span_context()
128
+ enhanced_extra["trace_id"] = format(span_context.trace_id, "032x")
129
+ enhanced_extra["span_id"] = format(span_context.span_id, "016x")
130
+
131
+ if extra:
132
+ enhanced_extra.update(extra)
133
+
134
+ return enhanced_extra
135
+
136
+ def debug(self, message: str, extra: Optional[Dict[str, Any]] = None):
137
+ """Log debug message with enhanced context"""
138
+ self.logger.debug(message, extra=self._enhance_extra(extra))
139
+
140
+ def info(self, message: str, extra: Optional[Dict[str, Any]] = None):
141
+ """Log info message with enhanced context"""
142
+ self.logger.info(message, extra=self._enhance_extra(extra))
143
+
144
+ def warning(self, message: str, extra: Optional[Dict[str, Any]] = None):
145
+ """Log warning message with enhanced context"""
146
+ self.logger.warning(message, extra=self._enhance_extra(extra))
147
+
148
+ def error(
149
+ self,
150
+ message: str,
151
+ extra: Optional[Dict[str, Any]] = None,
152
+ exc_info: bool = True,
153
+ ):
154
+ """Log error message with enhanced context and exception info"""
155
+ self.logger.error(message, extra=self._enhance_extra(extra), exc_info=exc_info)
156
+
157
+ def critical(self, message: str, extra: Optional[Dict[str, Any]] = None):
158
+ """Log critical message with enhanced context"""
159
+ self.logger.critical(message, extra=self._enhance_extra(extra))
160
+
161
+ def log_function_execution(
162
+ self,
163
+ function_name: str,
164
+ duration_ms: float,
165
+ success: bool = True,
166
+ extra: Optional[Dict[str, Any]] = None,
167
+ ):
168
+ """
169
+ Log function execution metrics with performance data
170
+
171
+ Args:
172
+ function_name: Name of the executed function
173
+ duration_ms: Execution duration in milliseconds
174
+ success: Whether the function executed successfully
175
+ extra: Additional custom properties
176
+ """
177
+ log_data = {
178
+ "function_name": function_name,
179
+ "duration_ms": duration_ms,
180
+ "success": success,
181
+ "performance_category": "function_execution",
182
+ }
183
+
184
+ if extra:
185
+ log_data.update(extra)
186
+
187
+ log_level = logging.INFO if success else logging.ERROR
188
+ message = f"Function '{function_name}' executed in {duration_ms:.2f}ms - {'SUCCESS' if success else 'FAILED'}"
189
+
190
+ if success:
191
+ self.info(message, extra=log_data)
192
+ else:
193
+ self.error(message, extra=log_data, exc_info=False)
194
+
195
+ def log_request(
196
+ self,
197
+ method: str,
198
+ url: str,
199
+ status_code: int,
200
+ duration_ms: float,
201
+ extra: Optional[Dict[str, Any]] = None,
202
+ ):
203
+ """
204
+ Log HTTP request with comprehensive details
205
+
206
+ Args:
207
+ method: HTTP method
208
+ url: Request URL
209
+ status_code: HTTP status code
210
+ duration_ms: Request duration in milliseconds
211
+ extra: Additional custom properties
212
+ """
213
+ log_data = {
214
+ "http_method": method,
215
+ "http_url": str(url),
216
+ "http_status_code": status_code,
217
+ "duration_ms": duration_ms,
218
+ "request_category": "http_request",
219
+ }
220
+
221
+ if extra:
222
+ log_data.update(extra)
223
+
224
+ # Determine log level based on status code
225
+ if status_code < 400:
226
+ log_level = logging.INFO
227
+ status_text = "SUCCESS"
228
+ elif status_code < 500:
229
+ log_level = logging.WARNING
230
+ status_text = "CLIENT_ERROR"
231
+ else:
232
+ log_level = logging.ERROR
233
+ status_text = "SERVER_ERROR"
234
+
235
+ message = (
236
+ f"{method} {url} - {status_code} - {duration_ms:.2f}ms - {status_text}"
237
+ )
238
+ self.logger.log(log_level, message, extra=self._enhance_extra(log_data))
239
+
240
+ def create_span(
241
+ self,
242
+ span_name: str,
243
+ attributes: Optional[Dict[str, Union[str, int, float, bool]]] = None,
244
+ ) -> Span:
245
+ """
246
+ Create a new span for distributed tracing
247
+
248
+ Args:
249
+ span_name: Name of the span
250
+ attributes: Initial attributes for the span
251
+
252
+ Returns:
253
+ OpenTelemetry span context manager
254
+ """
255
+ span = self.tracer.start_span(span_name)
256
+
257
+ # Add default attributes
258
+ span.set_attribute("service.name", self.service_name)
259
+ span.set_attribute("service.version", self.service_version)
260
+
261
+ if self._correlation_id:
262
+ span.set_attribute("correlation.id", self._correlation_id)
263
+
264
+ # Add custom attributes
265
+ if attributes:
266
+ for key, value in attributes.items():
267
+ span.set_attribute(key, value)
268
+
269
+ return span
270
+
271
+ def trace_function(
272
+ self,
273
+ function_name: Optional[str] = None,
274
+ log_args: bool = False,
275
+ log_result: bool = False,
276
+ log_execution: bool = True,
277
+ ):
278
+ """
279
+ Decorator to automatically trace function execution with comprehensive logging
280
+
281
+ Args:
282
+ function_name: Custom name for the span (defaults to function name)
283
+ log_args: Whether to log function arguments
284
+ log_result: Whether to log function result
285
+ log_execution: Whether to log execution metrics
286
+ """
287
+
288
+ def decorator(func):
289
+ @functools.wraps(func)
290
+ def wrapper(*args, **kwargs):
291
+ span_name = function_name or f"{func.__module__}.{func.__name__}"
292
+
293
+ with self.tracer.start_as_current_span(span_name) as span:
294
+ # Add function metadata
295
+ span.set_attribute("function.name", func.__name__)
296
+ span.set_attribute("function.module", func.__module__)
297
+ span.set_attribute("service.name", self.service_name)
298
+
299
+ if self._correlation_id:
300
+ span.set_attribute("correlation.id", self._correlation_id)
301
+
302
+ if log_args and args:
303
+ span.set_attribute("function.args_count", len(args))
304
+ if log_args and kwargs:
305
+ span.set_attribute("function.kwargs_count", len(kwargs))
306
+
307
+ start_time = time.time()
308
+
309
+ try:
310
+ self.debug(f"Starting function execution: {func.__name__}")
311
+
312
+ result = func(*args, **kwargs)
313
+ duration_ms = (time.time() - start_time) * 1000
314
+
315
+ # Mark span as successful
316
+ span.set_attribute("function.duration_ms", duration_ms)
317
+ span.set_attribute("function.success", True)
318
+ span.set_status(Status(StatusCode.OK))
319
+
320
+ if log_result and result is not None:
321
+ span.set_attribute("function.has_result", True)
322
+ span.set_attribute(
323
+ "function.result_type", type(result).__name__
324
+ )
325
+
326
+ if log_execution:
327
+ self.log_function_execution(
328
+ func.__name__,
329
+ duration_ms,
330
+ True,
331
+ {
332
+ "args_count": len(args) if args else 0,
333
+ "kwargs_count": len(kwargs) if kwargs else 0,
334
+ },
335
+ )
336
+
337
+ self.debug(f"Function execution completed: {func.__name__}")
338
+ return result
339
+
340
+ except Exception as e:
341
+ duration_ms = (time.time() - start_time) * 1000
342
+
343
+ # Mark span as failed
344
+ span.set_status(Status(StatusCode.ERROR, str(e)))
345
+ span.record_exception(e)
346
+ span.set_attribute("function.duration_ms", duration_ms)
347
+ span.set_attribute("function.success", False)
348
+ span.set_attribute("error.type", type(e).__name__)
349
+ span.set_attribute("error.message", str(e))
350
+
351
+ if log_execution:
352
+ self.log_function_execution(
353
+ func.__name__,
354
+ duration_ms,
355
+ False,
356
+ {
357
+ "error_type": type(e).__name__,
358
+ "error_message": str(e),
359
+ },
360
+ )
361
+
362
+ self.error(
363
+ f"Function execution failed: {func.__name__} - {str(e)}"
364
+ )
365
+ raise
366
+
367
+ return wrapper
368
+
369
+ return decorator
370
+
371
+ def add_span_attributes(self, attributes: Dict[str, Union[str, int, float, bool]]):
372
+ """Add attributes to the current active span"""
373
+ current_span = trace.get_current_span()
374
+ if current_span and current_span.is_recording():
375
+ for key, value in attributes.items():
376
+ current_span.set_attribute(key, value)
377
+
378
+ def add_span_event(self, name: str, attributes: Optional[Dict[str, Any]] = None):
379
+ """Add an event to the current active span"""
380
+ current_span = trace.get_current_span()
381
+ if current_span and current_span.is_recording():
382
+ event_attributes = attributes or {}
383
+ if self._correlation_id:
384
+ event_attributes["correlation_id"] = self._correlation_id
385
+ current_span.add_event(name, event_attributes)
386
+
387
+ def set_span_status(
388
+ self, status_code: StatusCode, description: Optional[str] = None
389
+ ):
390
+ """Set the status of the current active span"""
391
+ current_span = trace.get_current_span()
392
+ if current_span and current_span.is_recording():
393
+ current_span.set_status(Status(status_code, description))
394
+
395
+ def log_with_span(
396
+ self,
397
+ span_name: str,
398
+ message: str,
399
+ level: int = logging.INFO,
400
+ extra: Optional[Dict[str, Any]] = None,
401
+ span_attributes: Optional[Dict[str, Union[str, int, float, bool]]] = None,
402
+ ):
403
+ """
404
+ Log a message within a span context
405
+
406
+ Args:
407
+ span_name: Name of the span
408
+ message: Log message
409
+ level: Log level
410
+ extra: Additional log properties
411
+ span_attributes: Attributes to add to the span
412
+ """
413
+ with self.tracer.start_as_current_span(span_name) as span:
414
+ if span_attributes:
415
+ for key, value in span_attributes.items():
416
+ span.set_attribute(key, value)
417
+
418
+ self.logger.log(level, message, extra=self._enhance_extra(extra))
419
+
420
+ def log_dependency(
421
+ self,
422
+ dependency_type: str,
423
+ name: str,
424
+ command: str,
425
+ success: bool,
426
+ duration_ms: float,
427
+ extra: Optional[Dict[str, Any]] = None,
428
+ ):
429
+ """
430
+ Log external dependency calls (Database, HTTP, etc.)
431
+
432
+ Args:
433
+ dependency_type: Type of dependency (SQL, HTTP, etc.)
434
+ name: Name/identifier of the dependency
435
+ command: Command/query executed
436
+ success: Whether the call was successful
437
+ duration_ms: Call duration in milliseconds
438
+ extra: Additional properties
439
+ """
440
+ log_data = {
441
+ "dependency_type": dependency_type,
442
+ "dependency_name": name,
443
+ "dependency_command": command,
444
+ "dependency_success": success,
445
+ "duration_ms": duration_ms,
446
+ "category": "dependency_call",
447
+ }
448
+
449
+ if extra:
450
+ log_data.update(extra)
451
+
452
+ log_level = logging.INFO if success else logging.ERROR
453
+ status = "SUCCESS" if success else "FAILED"
454
+ message = f"Dependency call: {dependency_type}:{name} - {duration_ms:.2f}ms - {status}"
455
+
456
+ self.logger.log(log_level, message, extra=self._enhance_extra(log_data))
457
+
458
+ def flush(self):
459
+ """Flush all pending telemetry data"""
460
+ if self._telemetry_enabled:
461
+ try:
462
+ # Force flush any pending telemetry
463
+ from opentelemetry.sdk.trace import TracerProvider
464
+
465
+ tracer_provider = trace.get_tracer_provider()
466
+ if hasattr(tracer_provider, "force_flush"):
467
+ tracer_provider.force_flush(timeout_millis=5000)
468
+ except Exception as e:
469
+ self.warning(f"Failed to flush telemetry: {e}")
470
+
471
+
472
+ # Factory functions for easy instantiation
473
+ def create_app_logger(
474
+ service_name: str,
475
+ service_version: str = "1.0.0",
476
+ connection_string: Optional[str] = None,
477
+ log_level: int = logging.INFO,
478
+ enable_console_logging: bool = True,
479
+ custom_resource_attributes: Optional[Dict[str, str]] = None,
480
+ ) -> AzureLogger:
481
+ """
482
+ Factory function to create an AzureLogger instance
483
+
484
+ Args:
485
+ service_name: Name of your service/application
486
+ service_version: Version of your service
487
+ connection_string: Application Insights connection string
488
+ log_level: Logging level
489
+ enable_console_logging: Enable console output
490
+ custom_resource_attributes: Additional resource attributes
491
+
492
+ Returns:
493
+ Configured AzureLogger instance
494
+ """
495
+ return AzureLogger(
496
+ service_name=service_name,
497
+ service_version=service_version,
498
+ connection_string=connection_string,
499
+ log_level=log_level,
500
+ enable_console_logging=enable_console_logging,
501
+ custom_resource_attributes=custom_resource_attributes,
502
+ )
503
+
504
+
505
+ def create_function_logger(
506
+ function_app_name: str,
507
+ function_name: str,
508
+ service_version: str = "1.0.0",
509
+ connection_string: Optional[str] = None,
510
+ ) -> AzureLogger:
511
+ """
512
+ Factory function specifically for Azure Functions
513
+
514
+ Args:
515
+ function_app_name: Name of the Function App
516
+ function_name: Name of the specific function
517
+ service_version: Version of the service
518
+ connection_string: Application Insights connection string
519
+
520
+ Returns:
521
+ Configured AzureLogger instance for Azure Functions
522
+ """
523
+ custom_attributes = {
524
+ "azure.function.app": function_app_name,
525
+ "azure.function.name": function_name,
526
+ "azure.resource.type": "function",
527
+ }
528
+
529
+ return create_app_logger(
530
+ service_name=f"{function_app_name}.{function_name}",
531
+ service_version=service_version,
532
+ connection_string=connection_string,
533
+ custom_resource_attributes=custom_attributes,
534
+ )
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: azpaddypy
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE
8
+ Requires-Dist: azure-monitor-opentelemetry==1.6.10
9
+ Dynamic: license-file
@@ -0,0 +1,7 @@
1
+ azpaddypy/mgmt/__init__.py,sha256=5-0eZuJMZlCONZNJ5hEvWXfvLIM36mg7FsEVs32bY_A,183
2
+ azpaddypy/mgmt/logging.py,sha256=bpxGz674_1_Re5LzC1b6vslImeUQXSPTdBfHbt81jI0,19942
3
+ azpaddypy-0.1.0.dist-info/licenses/LICENSE,sha256=hQ6t0g2QaewGCQICHqTckBFbMVakGmoyTAzDpmEYV4c,1089
4
+ azpaddypy-0.1.0.dist-info/METADATA,sha256=IYtfH4emxwpYEuSY1vHij7D4ZeLpYlNPvzaOiS6hcaQ,256
5
+ azpaddypy-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
6
+ azpaddypy-0.1.0.dist-info/top_level.txt,sha256=hsDuboDhT61320ML8X479ezSTwT3rrlDWz1_Z45B2cs,10
7
+ azpaddypy-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Patrik Hartl
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ azpaddypy