azpaddypy 0.2.2__py3-none-any.whl → 0.2.4__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.
azpaddypy/mgmt/logging.py CHANGED
@@ -4,29 +4,28 @@ import json
4
4
  import functools
5
5
  import time
6
6
  import asyncio
7
- from typing import Optional, Dict, Any, Union, List
7
+ from typing import Optional, Dict, Any, Union, Callable
8
8
  from datetime import datetime
9
9
  from azure.monitor.opentelemetry import configure_azure_monitor
10
+ from opentelemetry.metrics import get_meter_provider
11
+ from opentelemetry.trace import get_tracer_provider
12
+ from opentelemetry._logs import get_logger_provider
10
13
  from opentelemetry import trace
11
14
  from opentelemetry.trace import Status, StatusCode, Span
12
- from opentelemetry.sdk.trace import TracerProvider
13
- from opentelemetry.sdk.resources import Resource
14
15
  from opentelemetry import baggage
15
16
  from opentelemetry.context import Context
16
17
 
17
18
 
18
19
  class AzureLogger:
19
- _instance = None
20
- _initialized = False
21
- """
22
- Comprehensive logging class for Azure Functions and Azure Web Apps
23
- using Azure Monitor OpenTelemetry integration with advanced tracing capabilities.
24
20
  """
21
+ A comprehensive logging class for Azure applications.
22
+
23
+ This logger integrates with Azure Monitor OpenTelemetry for distributed
24
+ tracing and structured logging, providing advanced capabilities for
25
25
 
26
- def __new__(cls, *args, **kwargs):
27
- if cls._instance is None:
28
- cls._instance = super(AzureLogger, cls).__new__(cls)
29
- return cls._instance
26
+ It supports correlation tracking, baggage propagation, and automated
27
+ tracing for functions and dependencies.
28
+ """
30
29
 
31
30
  def __init__(
32
31
  self,
@@ -37,9 +36,6 @@ class AzureLogger:
37
36
  enable_console_logging: bool = True,
38
37
  custom_resource_attributes: Optional[Dict[str, str]] = None,
39
38
  ):
40
- if self.__class__._initialized:
41
- return
42
- self.__class__._initialized = True
43
39
  """
44
40
  Initialize the Azure Logger with OpenTelemetry tracing
45
41
 
@@ -107,7 +103,12 @@ class AzureLogger:
107
103
  )
108
104
 
109
105
  def _setup_console_handler(self):
110
- """Setup console logging handler with structured format"""
106
+ """Sets up a console handler for logging to the standard output.
107
+
108
+ This is useful for local development and debugging purposes. The handler
109
+ is configured with a structured formatter to ensure log messages are
110
+ consistent and easy to read.
111
+ """
111
112
  console_handler = logging.StreamHandler()
112
113
  formatter = logging.Formatter(
113
114
  "%(asctime)s - %(name)s - %(levelname)s - %(message)s - %(pathname)s:%(lineno)d"
@@ -116,11 +117,22 @@ class AzureLogger:
116
117
  self.logger.addHandler(console_handler)
117
118
 
118
119
  def set_correlation_id(self, correlation_id: str):
119
- """Set correlation ID for request tracking"""
120
+ """Sets a correlation ID for tracking a request or transaction.
121
+
122
+ This ID is automatically included in all subsequent logs and traces,
123
+ allowing for easy filtering and correlation of telemetry data.
124
+
125
+ Args:
126
+ correlation_id: The unique identifier for the transaction.
127
+ """
120
128
  self._correlation_id = correlation_id
121
129
 
122
130
  def get_correlation_id(self) -> Optional[str]:
123
- """Get current correlation ID"""
131
+ """Retrieves the current correlation ID.
132
+
133
+ Returns:
134
+ The current correlation ID if it has been set, otherwise None.
135
+ """
124
136
  return self._correlation_id
125
137
 
126
138
  def set_baggage(self, key: str, value: str) -> Context:
@@ -138,27 +150,38 @@ class AzureLogger:
138
150
 
139
151
  def get_baggage(self, key: str) -> Optional[str]:
140
152
  """
141
- Get a baggage item from the current context
153
+ Retrieves a baggage item from the current OpenTelemetry context.
142
154
 
143
155
  Args:
144
- key: Baggage key
156
+ key: The baggage key.
145
157
 
146
158
  Returns:
147
- Baggage value if exists, None otherwise
159
+ The baggage value if it exists, otherwise None.
148
160
  """
149
161
  return baggage.get_baggage(key)
150
162
 
151
163
  def get_all_baggage(self) -> Dict[str, str]:
152
164
  """
153
- Get all baggage items from the current context
165
+ Retrieves all baggage items from the current OpenTelemetry context.
154
166
 
155
167
  Returns:
156
- Dictionary of all baggage items
168
+ A dictionary containing all baggage items.
157
169
  """
158
170
  return dict(baggage.get_all())
159
171
 
160
172
  def _enhance_extra(self, extra: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
161
- """Enhance log extra data with correlation and service info"""
173
+ """
174
+ Enriches log records with contextual information.
175
+
176
+ This method adds service details, correlation IDs, trace context,
177
+ and baggage items to the log's `extra` dictionary.
178
+
179
+ Args:
180
+ extra: An optional dictionary of custom data to include.
181
+
182
+ Returns:
183
+ The enhanced dictionary with added contextual data.
184
+ """
162
185
  enhanced_extra = {
163
186
  "service_name": self.service_name,
164
187
  "service_version": self.service_version,
@@ -186,15 +209,15 @@ class AzureLogger:
186
209
  return enhanced_extra
187
210
 
188
211
  def debug(self, message: str, extra: Optional[Dict[str, Any]] = None):
189
- """Log debug message with enhanced context"""
212
+ """Logs a debug message with enhanced context."""
190
213
  self.logger.debug(message, extra=self._enhance_extra(extra))
191
214
 
192
215
  def info(self, message: str, extra: Optional[Dict[str, Any]] = None):
193
- """Log info message with enhanced context"""
216
+ """Logs an info message with enhanced context."""
194
217
  self.logger.info(message, extra=self._enhance_extra(extra))
195
218
 
196
219
  def warning(self, message: str, extra: Optional[Dict[str, Any]] = None):
197
- """Log warning message with enhanced context"""
220
+ """Logs a warning message with enhanced context."""
198
221
  self.logger.warning(message, extra=self._enhance_extra(extra))
199
222
 
200
223
  def error(
@@ -203,11 +226,11 @@ class AzureLogger:
203
226
  extra: Optional[Dict[str, Any]] = None,
204
227
  exc_info: bool = True,
205
228
  ):
206
- """Log error message with enhanced context and exception info"""
229
+ """Logs an error message with enhanced context and exception info."""
207
230
  self.logger.error(message, extra=self._enhance_extra(extra), exc_info=exc_info)
208
231
 
209
232
  def critical(self, message: str, extra: Optional[Dict[str, Any]] = None):
210
- """Log critical message with enhanced context"""
233
+ """Logs a critical message with enhanced context."""
211
234
  self.logger.critical(message, extra=self._enhance_extra(extra))
212
235
 
213
236
  def log_function_execution(
@@ -218,13 +241,16 @@ class AzureLogger:
218
241
  extra: Optional[Dict[str, Any]] = None,
219
242
  ):
220
243
  """
221
- Log function execution metrics with performance data
244
+ Logs a custom event for a function's execution metrics.
245
+
246
+ This captures performance data such as duration and success status,
247
+ which can be queried in Azure Monitor.
222
248
 
223
249
  Args:
224
- function_name: Name of the executed function
225
- duration_ms: Execution duration in milliseconds
226
- success: Whether the function executed successfully
227
- extra: Additional custom properties
250
+ function_name: The name of the executed function.
251
+ duration_ms: The execution duration in milliseconds.
252
+ success: A flag indicating whether the function executed successfully.
253
+ extra: Additional custom properties to include in the log.
228
254
  """
229
255
  log_data = {
230
256
  "function_name": function_name,
@@ -320,13 +346,105 @@ class AzureLogger:
320
346
 
321
347
  return span
322
348
 
349
+ def _setup_span_for_function_trace(
350
+ self,
351
+ span: Span,
352
+ func: Callable,
353
+ is_async: bool,
354
+ log_args: bool,
355
+ args: tuple,
356
+ kwargs: dict,
357
+ ):
358
+ """Helper to set up span attributes for function tracing."""
359
+ span.set_attribute("function.name", func.__name__)
360
+ span.set_attribute("function.module", func.__module__)
361
+ span.set_attribute("service.name", self.service_name)
362
+ span.set_attribute("function.is_async", is_async)
363
+
364
+ if self._correlation_id:
365
+ span.set_attribute("correlation.id", self._correlation_id)
366
+
367
+ if log_args:
368
+ if args:
369
+ span.set_attribute("function.args_count", len(args))
370
+ if kwargs:
371
+ span.set_attribute("function.kwargs_count", len(kwargs))
372
+
373
+ def _handle_function_success(
374
+ self,
375
+ span: Span,
376
+ func: Callable,
377
+ duration_ms: float,
378
+ result: Any,
379
+ log_result: bool,
380
+ log_execution: bool,
381
+ is_async: bool,
382
+ args: tuple,
383
+ kwargs: dict,
384
+ ):
385
+ """Helper to handle successful function execution tracing."""
386
+ span.set_attribute("function.duration_ms", duration_ms)
387
+ span.set_attribute("function.success", True)
388
+ span.set_status(Status(StatusCode.OK))
389
+
390
+ if log_result and result is not None:
391
+ span.set_attribute("function.has_result", True)
392
+ span.set_attribute("function.result_type", type(result).__name__)
393
+
394
+ if log_execution:
395
+ self.log_function_execution(
396
+ func.__name__,
397
+ duration_ms,
398
+ True,
399
+ {
400
+ "args_count": len(args) if args else 0,
401
+ "kwargs_count": len(kwargs) if kwargs else 0,
402
+ "is_async": is_async,
403
+ },
404
+ )
405
+
406
+ log_prefix = "Async function" if is_async else "Function"
407
+ self.debug(f"{log_prefix} execution completed: {func.__name__}")
408
+
409
+ def _handle_function_exception(
410
+ self,
411
+ span: Span,
412
+ func: Callable,
413
+ duration_ms: float,
414
+ e: Exception,
415
+ log_execution: bool,
416
+ is_async: bool,
417
+ ):
418
+ """Helper to handle failed function execution tracing."""
419
+ span.set_status(Status(StatusCode.ERROR, str(e)))
420
+ span.record_exception(e)
421
+ span.set_attribute("function.duration_ms", duration_ms)
422
+ span.set_attribute("function.success", False)
423
+ span.set_attribute("error.type", type(e).__name__)
424
+ span.set_attribute("error.message", str(e))
425
+
426
+ if log_execution:
427
+ self.log_function_execution(
428
+ func.__name__,
429
+ duration_ms,
430
+ False,
431
+ {
432
+ "error_type": type(e).__name__,
433
+ "error_message": str(e),
434
+ "is_async": is_async,
435
+ },
436
+ )
437
+
438
+ log_prefix = "Async function" if is_async else "Function"
439
+ self.error(f"{log_prefix} execution failed: {func.__name__} - {str(e)}")
440
+
323
441
  def trace_function(
324
442
  self,
325
443
  function_name: Optional[str] = None,
326
444
  log_args: bool = False,
327
445
  log_result: bool = False,
328
446
  log_execution: bool = True,
329
- ):
447
+ ) -> Callable:
330
448
  """
331
449
  Decorator to automatically trace function execution with comprehensive logging.
332
450
  Supports both synchronous and asynchronous functions.
@@ -342,166 +460,64 @@ class AzureLogger:
342
460
  @functools.wraps(func)
343
461
  async def async_wrapper(*args, **kwargs):
344
462
  span_name = function_name or f"{func.__module__}.{func.__name__}"
345
-
346
463
  with self.tracer.start_as_current_span(span_name) as span:
347
- # Add function metadata
348
- span.set_attribute("function.name", func.__name__)
349
- span.set_attribute("function.module", func.__module__)
350
- span.set_attribute("service.name", self.service_name)
351
- span.set_attribute("function.is_async", True)
352
-
353
- if self._correlation_id:
354
- span.set_attribute("correlation.id", self._correlation_id)
355
-
356
- if log_args and args:
357
- span.set_attribute("function.args_count", len(args))
358
- if log_args and kwargs:
359
- span.set_attribute("function.kwargs_count", len(kwargs))
360
-
464
+ self._setup_span_for_function_trace(
465
+ span, func, True, log_args, args, kwargs
466
+ )
361
467
  start_time = time.time()
362
-
363
468
  try:
364
469
  self.debug(
365
470
  f"Starting async function execution: {func.__name__}"
366
471
  )
367
-
368
472
  result = await func(*args, **kwargs)
369
473
  duration_ms = (time.time() - start_time) * 1000
370
-
371
- # Mark span as successful
372
- span.set_attribute("function.duration_ms", duration_ms)
373
- span.set_attribute("function.success", True)
374
- span.set_status(Status(StatusCode.OK))
375
-
376
- if log_result and result is not None:
377
- span.set_attribute("function.has_result", True)
378
- span.set_attribute(
379
- "function.result_type", type(result).__name__
380
- )
381
-
382
- if log_execution:
383
- self.log_function_execution(
384
- func.__name__,
385
- duration_ms,
386
- True,
387
- {
388
- "args_count": len(args) if args else 0,
389
- "kwargs_count": len(kwargs) if kwargs else 0,
390
- "is_async": True,
391
- },
392
- )
393
-
394
- self.debug(
395
- f"Async function execution completed: {func.__name__}"
474
+ self._handle_function_success(
475
+ span,
476
+ func,
477
+ duration_ms,
478
+ result,
479
+ log_result,
480
+ log_execution,
481
+ True,
482
+ args,
483
+ kwargs,
396
484
  )
397
485
  return result
398
-
399
486
  except Exception as e:
400
487
  duration_ms = (time.time() - start_time) * 1000
401
-
402
- # Mark span as failed
403
- span.set_status(Status(StatusCode.ERROR, str(e)))
404
- span.record_exception(e)
405
- span.set_attribute("function.duration_ms", duration_ms)
406
- span.set_attribute("function.success", False)
407
- span.set_attribute("error.type", type(e).__name__)
408
- span.set_attribute("error.message", str(e))
409
-
410
- if log_execution:
411
- self.log_function_execution(
412
- func.__name__,
413
- duration_ms,
414
- False,
415
- {
416
- "error_type": type(e).__name__,
417
- "error_message": str(e),
418
- "is_async": True,
419
- },
420
- )
421
-
422
- self.error(
423
- f"Async function execution failed: {func.__name__} - {str(e)}"
488
+ self._handle_function_exception(
489
+ span, func, duration_ms, e, log_execution, True
424
490
  )
425
491
  raise
426
492
 
427
493
  @functools.wraps(func)
428
494
  def sync_wrapper(*args, **kwargs):
429
495
  span_name = function_name or f"{func.__module__}.{func.__name__}"
430
-
431
496
  with self.tracer.start_as_current_span(span_name) as span:
432
- # Add function metadata
433
- span.set_attribute("function.name", func.__name__)
434
- span.set_attribute("function.module", func.__module__)
435
- span.set_attribute("service.name", self.service_name)
436
- span.set_attribute("function.is_async", False)
437
-
438
- if self._correlation_id:
439
- span.set_attribute("correlation.id", self._correlation_id)
440
-
441
- if log_args and args:
442
- span.set_attribute("function.args_count", len(args))
443
- if log_args and kwargs:
444
- span.set_attribute("function.kwargs_count", len(kwargs))
445
-
497
+ self._setup_span_for_function_trace(
498
+ span, func, False, log_args, args, kwargs
499
+ )
446
500
  start_time = time.time()
447
-
448
501
  try:
449
502
  self.debug(f"Starting function execution: {func.__name__}")
450
-
451
503
  result = func(*args, **kwargs)
452
504
  duration_ms = (time.time() - start_time) * 1000
453
-
454
- # Mark span as successful
455
- span.set_attribute("function.duration_ms", duration_ms)
456
- span.set_attribute("function.success", True)
457
- span.set_status(Status(StatusCode.OK))
458
-
459
- if log_result and result is not None:
460
- span.set_attribute("function.has_result", True)
461
- span.set_attribute(
462
- "function.result_type", type(result).__name__
463
- )
464
-
465
- if log_execution:
466
- self.log_function_execution(
467
- func.__name__,
468
- duration_ms,
469
- True,
470
- {
471
- "args_count": len(args) if args else 0,
472
- "kwargs_count": len(kwargs) if kwargs else 0,
473
- "is_async": False,
474
- },
475
- )
476
-
477
- self.debug(f"Function execution completed: {func.__name__}")
505
+ self._handle_function_success(
506
+ span,
507
+ func,
508
+ duration_ms,
509
+ result,
510
+ log_result,
511
+ log_execution,
512
+ False,
513
+ args,
514
+ kwargs,
515
+ )
478
516
  return result
479
-
480
517
  except Exception as e:
481
518
  duration_ms = (time.time() - start_time) * 1000
482
-
483
- # Mark span as failed
484
- span.set_status(Status(StatusCode.ERROR, str(e)))
485
- span.record_exception(e)
486
- span.set_attribute("function.duration_ms", duration_ms)
487
- span.set_attribute("function.success", False)
488
- span.set_attribute("error.type", type(e).__name__)
489
- span.set_attribute("error.message", str(e))
490
-
491
- if log_execution:
492
- self.log_function_execution(
493
- func.__name__,
494
- duration_ms,
495
- False,
496
- {
497
- "error_type": type(e).__name__,
498
- "error_message": str(e),
499
- "is_async": False,
500
- },
501
- )
502
-
503
- self.error(
504
- f"Function execution failed: {func.__name__} - {str(e)}"
519
+ self._handle_function_exception(
520
+ span, func, duration_ms, e, log_execution, False
505
521
  )
506
522
  raise
507
523
 
@@ -614,6 +630,9 @@ class AzureLogger:
614
630
 
615
631
 
616
632
  # Factory functions for easy instantiation
633
+ _loggers: Dict[Any, "AzureLogger"] = {}
634
+
635
+
617
636
  def create_app_logger(
618
637
  service_name: str,
619
638
  service_version: str = "1.0.0",
@@ -623,7 +642,8 @@ def create_app_logger(
623
642
  custom_resource_attributes: Optional[Dict[str, str]] = None,
624
643
  ) -> AzureLogger:
625
644
  """
626
- Factory function to create an AzureLogger instance
645
+ Factory function to create an AzureLogger instance. If a logger with the
646
+ same configuration has already been created, it will be returned.
627
647
 
628
648
  Args:
629
649
  service_name: Name of your service/application
@@ -636,7 +656,29 @@ def create_app_logger(
636
656
  Returns:
637
657
  Configured AzureLogger instance
638
658
  """
639
- return AzureLogger(
659
+ resolved_connection_string = connection_string or os.getenv(
660
+ "APPLICATIONINSIGHTS_CONNECTION_STRING"
661
+ )
662
+
663
+ attr_items = (
664
+ tuple(sorted(custom_resource_attributes.items()))
665
+ if custom_resource_attributes
666
+ else None
667
+ )
668
+
669
+ params_key = (
670
+ service_name,
671
+ service_version,
672
+ resolved_connection_string,
673
+ log_level,
674
+ enable_console_logging,
675
+ attr_items,
676
+ )
677
+
678
+ if params_key in _loggers:
679
+ return _loggers[params_key]
680
+
681
+ logger = AzureLogger(
640
682
  service_name=service_name,
641
683
  service_version=service_version,
642
684
  connection_string=connection_string,
@@ -644,6 +686,8 @@ def create_app_logger(
644
686
  enable_console_logging=enable_console_logging,
645
687
  custom_resource_attributes=custom_resource_attributes,
646
688
  )
689
+ _loggers[params_key] = logger
690
+ return logger
647
691
 
648
692
 
649
693
  def create_function_logger(
@@ -0,0 +1,19 @@
1
+ Metadata-Version: 2.4
2
+ Name: azpaddypy
3
+ Version: 0.2.4
4
+ Summary: Comprehensive Python logger for Azure, integrating OpenTelemetry for advanced, structured, and distributed tracing.
5
+ Classifier: Programming Language :: Python :: 3
6
+ Classifier: License :: OSI Approved :: MIT License
7
+ Classifier: Operating System :: OS Independent
8
+ Requires-Python: >=3.11
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: azure-monitor-opentelemetry==1.6.10
12
+ Provides-Extra: dev
13
+ Requires-Dist: trio>=0.22.2; extra == "dev"
14
+ Requires-Dist: azure-functions>=1.17.0; extra == "dev"
15
+ Requires-Dist: pytest>=7.4.0; extra == "dev"
16
+ Requires-Dist: pytest-asyncio>=0.21.1; extra == "dev"
17
+ Requires-Dist: anyio>=3.7.1; extra == "dev"
18
+ Requires-Dist: pip; extra == "dev"
19
+ Dynamic: license-file
@@ -0,0 +1,9 @@
1
+ azpaddypy/mgmt/__init__.py,sha256=5-0eZuJMZlCONZNJ5hEvWXfvLIM36mg7FsEVs32bY_A,183
2
+ azpaddypy/mgmt/logging.py,sha256=P37ofxuuQOiZsMV7ImwSRvebu9OQHLQ6Qn4mebujdgM,26042
3
+ azpaddypy/test_function/__init__.py,sha256=0NjUl36wvUWV79GpRwBFkgkBaC6uDZsTdaSVOIHMFEU,3481
4
+ azpaddypy/test_function/function_app.py,sha256=6nX54-iq0L1l_hZpD6E744-j79oLxdaldFyWDCpwH7c,3867
5
+ azpaddypy-0.2.4.dist-info/licenses/LICENSE,sha256=hQ6t0g2QaewGCQICHqTckBFbMVakGmoyTAzDpmEYV4c,1089
6
+ azpaddypy-0.2.4.dist-info/METADATA,sha256=oHiAcLAt_h7WUfXZSgP2Mk_38tQE09mSqYO7AVkvKB0,799
7
+ azpaddypy-0.2.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
+ azpaddypy-0.2.4.dist-info/top_level.txt,sha256=hsDuboDhT61320ML8X479ezSTwT3rrlDWz1_Z45B2cs,10
9
+ azpaddypy-0.2.4.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: azpaddypy
3
- Version: 0.2.2
4
- Summary: Add your description here
5
- Classifier: Operating System :: OS Independent
6
- Requires-Python: >=3.11
7
- Description-Content-Type: text/markdown
8
- License-File: LICENSE
9
- Requires-Dist: azure-monitor-opentelemetry==1.6.10
10
- Dynamic: license-file
@@ -1,9 +0,0 @@
1
- azpaddypy/mgmt/__init__.py,sha256=5-0eZuJMZlCONZNJ5hEvWXfvLIM36mg7FsEVs32bY_A,183
2
- azpaddypy/mgmt/logging.py,sha256=A-56GNbSWAFnKNPcoa-NfTINbBWvJf-p4ZGxUz7M4uQ,25777
3
- azpaddypy/test_function/__init__.py,sha256=0NjUl36wvUWV79GpRwBFkgkBaC6uDZsTdaSVOIHMFEU,3481
4
- azpaddypy/test_function/function_app.py,sha256=6nX54-iq0L1l_hZpD6E744-j79oLxdaldFyWDCpwH7c,3867
5
- azpaddypy-0.2.2.dist-info/licenses/LICENSE,sha256=hQ6t0g2QaewGCQICHqTckBFbMVakGmoyTAzDpmEYV4c,1089
6
- azpaddypy-0.2.2.dist-info/METADATA,sha256=AM8Zw5IFOQVOQcd5zB2J473u1jSy9oDqOtoLpU-mhjc,304
7
- azpaddypy-0.2.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
- azpaddypy-0.2.2.dist-info/top_level.txt,sha256=hsDuboDhT61320ML8X479ezSTwT3rrlDWz1_Z45B2cs,10
9
- azpaddypy-0.2.2.dist-info/RECORD,,