azpaddypy 0.2.7__py3-none-any.whl → 0.2.9__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.
@@ -4,6 +4,10 @@ AzPaddyPy - A standardized Python package for Azure cloud services integration.
4
4
 
5
5
  __version__ = "0.1.0"
6
6
 
7
- from azpaddypy.mgmt import logging
7
+ from azpaddypy.mgmt.logging import AzureLogger
8
+ from azpaddypy.mgmt.identity import AzureIdentity
8
9
 
9
- __all__ = ["AzureLogger"]
10
+ __all__ = [
11
+ "AzureLogger",
12
+ "AzureIdentity",
13
+ ]
@@ -0,0 +1,335 @@
1
+ from typing import Optional, Dict, Any, Union
2
+ from azure.identity import (
3
+ TokenCachePersistenceOptions,
4
+ get_bearer_token_provider,
5
+ DefaultAzureCredential
6
+ )
7
+ from azure.core.credentials import AccessToken, TokenCredential
8
+ from .logging import AzureLogger
9
+
10
+
11
+ class AzureIdentity:
12
+ """Azure identity management with token caching and distributed tracing.
13
+
14
+ Provides standardized Azure authentication using DefaultAzureCredential
15
+ with integrated logging, caching, and OpenTelemetry tracing support.
16
+ Prioritizes Managed Identity, then Environment variables per Azure SDK
17
+ best practices.
18
+
19
+ Attributes:
20
+ service_name: Service identifier for logging and tracing
21
+ service_version: Service version for context
22
+ enable_token_cache: Whether token caching is enabled
23
+ allow_unencrypted_storage: Whether to allow unencrypted token storage
24
+ logger: AzureLogger instance for structured logging
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ service_name: str = "azure_identity",
30
+ service_version: str = "1.0.0",
31
+ enable_token_cache: bool = True,
32
+ allow_unencrypted_storage: bool = True,
33
+ custom_credential_options: Optional[Dict[str, Any]] = None,
34
+ logger: Optional[AzureLogger] = None,
35
+ connection_string: Optional[str] = None,
36
+ ):
37
+ """Initialize Azure Identity with comprehensive configuration.
38
+
39
+ Args:
40
+ service_name: Service name for tracing context
41
+ service_version: Service version for metadata
42
+ enable_token_cache: Enable in-memory token persistence
43
+ allow_unencrypted_storage: Allow unencrypted token storage
44
+ custom_credential_options: Additional DefaultAzureCredential options
45
+ logger: Optional AzureLogger instance
46
+ connection_string: Application Insights connection string
47
+ """
48
+ self.service_name = service_name
49
+ self.service_version = service_version
50
+ self.enable_token_cache = enable_token_cache
51
+ self.allow_unencrypted_storage = allow_unencrypted_storage
52
+
53
+ # Initialize logger - use provided instance or create new one
54
+ if logger is not None:
55
+ self.logger = logger
56
+ else:
57
+ self.logger = AzureLogger(
58
+ service_name=service_name,
59
+ service_version=service_version,
60
+ connection_string=connection_string,
61
+ enable_console_logging=True,
62
+ )
63
+
64
+ self._credential = None
65
+ self._setup_credential(custom_credential_options)
66
+
67
+ self.logger.info(
68
+ f"Azure Identity initialized for service '{service_name}' v{service_version}"
69
+ )
70
+
71
+ def _setup_credential(self, custom_options: Optional[Dict[str, Any]] = None):
72
+ """Configure DefaultAzureCredential with appropriate settings.
73
+
74
+ Args:
75
+ custom_options: Additional options for DefaultAzureCredential
76
+
77
+ Raises:
78
+ Exception: If credential initialization fails
79
+ """
80
+ try:
81
+ credential_options = {}
82
+
83
+ # Add token cache configuration if enabled
84
+ if self.enable_token_cache:
85
+ token_cache_options = TokenCachePersistenceOptions(
86
+ allow_unencrypted_storage=self.allow_unencrypted_storage
87
+ )
88
+ credential_options["token_cache_persistence_options"] = token_cache_options
89
+
90
+ # Merge custom options
91
+ if custom_options:
92
+ credential_options.update(custom_options)
93
+
94
+ self._credential = DefaultAzureCredential(**credential_options)
95
+
96
+ self.logger.debug(
97
+ "DefaultAzureCredential configured successfully",
98
+ extra={
99
+ "token_cache_enabled": self.enable_token_cache,
100
+ "unencrypted_storage": self.allow_unencrypted_storage,
101
+ }
102
+ )
103
+
104
+ except Exception as e:
105
+ self.logger.error(
106
+ f"Failed to initialize DefaultAzureCredential: {e}",
107
+ exc_info=True
108
+ )
109
+ raise
110
+
111
+ def get_credential(self) -> TokenCredential:
112
+ """Get the configured DefaultAzureCredential instance.
113
+
114
+ Returns:
115
+ Configured TokenCredential instance
116
+
117
+ Raises:
118
+ RuntimeError: If credential is not initialized
119
+ """
120
+ with self.logger.create_span(
121
+ "AzureIdentity.get_credential",
122
+ attributes={
123
+ "service.name": self.service_name,
124
+ "operation.type": "credential_retrieval"
125
+ }
126
+ ):
127
+ if self._credential is None:
128
+ error_msg = "Credential not initialized"
129
+ self.logger.error(error_msg)
130
+ raise RuntimeError(error_msg)
131
+
132
+ self.logger.debug("Retrieving DefaultAzureCredential instance")
133
+ return self._credential
134
+
135
+ def get_token(
136
+ self,
137
+ scopes: Union[str, list],
138
+ **kwargs
139
+ ) -> AccessToken:
140
+ """Acquire an access token for specified scopes.
141
+
142
+ Args:
143
+ scopes: Target scope(s) for token request
144
+ **kwargs: Additional token request parameters
145
+
146
+ Returns:
147
+ AccessToken with token and expiration information
148
+
149
+ Raises:
150
+ RuntimeError: If credential is not initialized
151
+ Exception: If token acquisition fails
152
+ """
153
+ # Normalize to list format
154
+ if isinstance(scopes, str):
155
+ scopes = [scopes]
156
+
157
+ with self.logger.create_span(
158
+ "AzureIdentity.get_token",
159
+ attributes={
160
+ "service.name": self.service_name,
161
+ "operation.type": "token_acquisition",
162
+ "token.scopes": ", ".join(scopes),
163
+ "token.scope_count": len(scopes)
164
+ }
165
+ ):
166
+ if self._credential is None:
167
+ raise RuntimeError("Credential not initialized")
168
+
169
+ self.logger.debug(
170
+ "Acquiring access token",
171
+ extra={
172
+ "scopes": scopes,
173
+ "scope_count": len(scopes)
174
+ }
175
+ )
176
+
177
+ token = self._credential.get_token(*scopes, **kwargs)
178
+
179
+ self.logger.info(
180
+ "Access token acquired successfully",
181
+ extra={
182
+ "scopes": scopes,
183
+ "expires_on": token.expires_on if hasattr(token, 'expires_on') else None
184
+ }
185
+ )
186
+
187
+ return token
188
+
189
+ def get_token_provider(
190
+ self,
191
+ scopes: Union[str, list],
192
+ **kwargs
193
+ ) -> callable:
194
+ """Create a bearer token provider for specified scopes.
195
+
196
+ Useful for Azure SDK clients that accept token providers.
197
+
198
+ Args:
199
+ scopes: Target scope(s) for the token provider
200
+ **kwargs: Additional token provider parameters
201
+
202
+ Returns:
203
+ Callable that returns bearer tokens
204
+
205
+ Raises:
206
+ RuntimeError: If credential is not initialized
207
+ Exception: If token provider creation fails
208
+ """
209
+ # Normalize to list format
210
+ if isinstance(scopes, str):
211
+ scopes = [scopes]
212
+
213
+ with self.logger.create_span(
214
+ "AzureIdentity.get_token_provider",
215
+ attributes={
216
+ "service.name": self.service_name,
217
+ "operation.type": "token_provider_creation",
218
+ "token.scopes": ", ".join(scopes),
219
+ "token.scope_count": len(scopes)
220
+ }
221
+ ):
222
+ if self._credential is None:
223
+ raise RuntimeError("Credential not initialized")
224
+
225
+ self.logger.debug(
226
+ "Creating bearer token provider",
227
+ extra={
228
+ "scopes": scopes,
229
+ "scope_count": len(scopes)
230
+ }
231
+ )
232
+
233
+ provider = get_bearer_token_provider(self._credential, *scopes, **kwargs)
234
+
235
+ self.logger.info(
236
+ "Bearer token provider created successfully",
237
+ extra={
238
+ "scopes": scopes
239
+ }
240
+ )
241
+
242
+ return provider
243
+
244
+ def test_credential(self, test_scopes: Optional[Union[str, list]] = None) -> bool:
245
+ """Test credential by attempting token acquisition.
246
+
247
+ Args:
248
+ test_scopes: Scopes to test with (defaults to Azure Management API)
249
+
250
+ Returns:
251
+ True if credential works, False otherwise
252
+ """
253
+ if test_scopes is None:
254
+ test_scopes = ["https://management.azure.com/.default"]
255
+ elif isinstance(test_scopes, str):
256
+ test_scopes = [test_scopes]
257
+
258
+ with self.logger.create_span(
259
+ "AzureIdentity.test_credential",
260
+ attributes={
261
+ "service.name": self.service_name,
262
+ "operation.type": "credential_test",
263
+ "test.scopes": ", ".join(test_scopes)
264
+ }
265
+ ):
266
+ try:
267
+ self.logger.info("Testing credential authentication")
268
+ token = self.get_token(test_scopes)
269
+
270
+ if token and hasattr(token, 'token') and token.token:
271
+ self.logger.info("Credential test successful")
272
+ return True
273
+ else:
274
+ self.logger.warning("Credential test returned empty token")
275
+ return False
276
+
277
+ except Exception as e:
278
+ self.logger.warning(
279
+ f"Credential test failed: {e}",
280
+ extra={"test_scopes": test_scopes}
281
+ )
282
+ return False
283
+
284
+ def set_correlation_id(self, correlation_id: str):
285
+ """Set correlation ID for tracking identity operations.
286
+
287
+ Args:
288
+ correlation_id: Unique identifier for transaction tracking
289
+ """
290
+ self.logger.set_correlation_id(correlation_id)
291
+
292
+ def get_correlation_id(self) -> Optional[str]:
293
+ """Get the current correlation ID.
294
+
295
+ Returns:
296
+ Current correlation ID if set, otherwise None
297
+ """
298
+ return self.logger.get_correlation_id()
299
+
300
+
301
+ def create_azure_identity(
302
+ service_name: str = "azure_identity",
303
+ service_version: str = "1.0.0",
304
+ enable_token_cache: bool = True,
305
+ allow_unencrypted_storage: bool = True,
306
+ custom_credential_options: Optional[Dict[str, Any]] = None,
307
+ logger: Optional[AzureLogger] = None,
308
+ connection_string: Optional[str] = None,
309
+ ) -> AzureIdentity:
310
+ """Create a configured AzureIdentity instance.
311
+
312
+ Factory function providing convenient AzureIdentity instantiation with
313
+ commonly used settings.
314
+
315
+ Args:
316
+ service_name: Service name for tracing context
317
+ service_version: Service version for metadata
318
+ enable_token_cache: Enable in-memory token persistence
319
+ allow_unencrypted_storage: Allow unencrypted token storage
320
+ custom_credential_options: Additional DefaultAzureCredential options
321
+ logger: Optional AzureLogger instance
322
+ connection_string: Application Insights connection string
323
+
324
+ Returns:
325
+ Configured AzureIdentity instance
326
+ """
327
+ return AzureIdentity(
328
+ service_name=service_name,
329
+ service_version=service_version,
330
+ enable_token_cache=enable_token_cache,
331
+ allow_unencrypted_storage=allow_unencrypted_storage,
332
+ custom_credential_options=custom_credential_options,
333
+ logger=logger,
334
+ connection_string=connection_string,
335
+ )
azpaddypy/mgmt/logging.py CHANGED
@@ -1,15 +1,11 @@
1
1
  import logging
2
2
  import os
3
- import json
4
3
  import functools
5
4
  import time
6
5
  import asyncio
7
6
  from typing import Optional, Dict, Any, Union, Callable
8
7
  from datetime import datetime
9
8
  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
13
9
  from opentelemetry import trace
14
10
  from opentelemetry.trace import Status, StatusCode, Span
15
11
  from opentelemetry import baggage
@@ -17,14 +13,18 @@ from opentelemetry.context import Context
17
13
 
18
14
 
19
15
  class AzureLogger:
20
- """
21
- A comprehensive logging class for Azure applications.
16
+ """Azure-integrated logger with OpenTelemetry distributed tracing.
22
17
 
23
- This logger integrates with Azure Monitor OpenTelemetry for distributed
24
- tracing and structured logging, providing advanced capabilities for
18
+ Provides comprehensive logging with Azure Monitor integration, correlation
19
+ tracking, baggage propagation, and automated function tracing for Azure
20
+ applications with seamless local development support.
25
21
 
26
- It supports correlation tracking, baggage propagation, and automated
27
- tracing for functions and dependencies.
22
+ Attributes:
23
+ service_name: Service identifier for telemetry
24
+ service_version: Service version for context
25
+ connection_string: Application Insights connection string
26
+ logger: Python logger instance
27
+ tracer: OpenTelemetry tracer for spans
28
28
  """
29
29
 
30
30
  def __init__(
@@ -37,17 +37,16 @@ class AzureLogger:
37
37
  custom_resource_attributes: Optional[Dict[str, str]] = None,
38
38
  instrumentation_options: Optional[Dict[str, Any]] = None,
39
39
  ):
40
- """
41
- Initialize the Azure Logger with OpenTelemetry tracing
40
+ """Initialize Azure Logger with OpenTelemetry tracing.
42
41
 
43
42
  Args:
44
- service_name: Name of your service/application
45
- service_version: Version of your service
43
+ service_name: Service identifier for telemetry
44
+ service_version: Service version for metadata
46
45
  connection_string: Application Insights connection string
47
- log_level: Logging level (default: INFO)
46
+ log_level: Python logging level (default: INFO)
48
47
  enable_console_logging: Enable console output for local development
49
- custom_resource_attributes: Additional resource attributes
50
- instrumentation_options: Instrumentation options
48
+ custom_resource_attributes: Additional OpenTelemetry resource attributes
49
+ instrumentation_options: Azure Monitor instrumentation options
51
50
  """
52
51
  self.service_name = service_name
53
52
  self.service_version = service_version
@@ -55,7 +54,7 @@ class AzureLogger:
55
54
  "APPLICATIONINSIGHTS_CONNECTION_STRING"
56
55
  )
57
56
 
58
- # Prepare resource attributes
57
+ # Configure resource attributes
59
58
  resource_attributes = {
60
59
  "service.name": service_name,
61
60
  "service.version": service_version,
@@ -65,7 +64,7 @@ class AzureLogger:
65
64
  if custom_resource_attributes:
66
65
  resource_attributes.update(custom_resource_attributes)
67
66
 
68
- # Configure Azure Monitor if connection string is available
67
+ # Configure Azure Monitor if connection string available
69
68
  if self.connection_string:
70
69
  try:
71
70
  configure_azure_monitor(
@@ -84,21 +83,16 @@ class AzureLogger:
84
83
  "Warning: No Application Insights connection string found. Telemetry disabled."
85
84
  )
86
85
 
87
- # Set up logger without instrumentor
86
+ # Configure Python logger
88
87
  self.logger = logging.getLogger(service_name)
89
88
  self.logger.setLevel(log_level)
90
-
91
- # Clear existing handlers to avoid duplicates
92
89
  self.logger.handlers.clear()
93
90
 
94
- # Add console handler for local development
95
91
  if enable_console_logging:
96
92
  self._setup_console_handler()
97
93
 
98
- # Get tracer for manual span creation
94
+ # Initialize OpenTelemetry tracer and correlation context
99
95
  self.tracer = trace.get_tracer(__name__)
100
-
101
- # Initialize correlation context
102
96
  self._correlation_id = None
103
97
 
104
98
  self.info(
@@ -106,12 +100,7 @@ class AzureLogger:
106
100
  )
107
101
 
108
102
  def _setup_console_handler(self):
109
- """Sets up a console handler for logging to the standard output.
110
-
111
- This is useful for local development and debugging purposes. The handler
112
- is configured with a structured formatter to ensure log messages are
113
- consistent and easy to read.
114
- """
103
+ """Configure console handler for local development."""
115
104
  console_handler = logging.StreamHandler()
116
105
  formatter = logging.Formatter(
117
106
  "%(asctime)s - %(name)s - %(levelname)s - %(message)s - %(pathname)s:%(lineno)d"
@@ -120,70 +109,61 @@ class AzureLogger:
120
109
  self.logger.addHandler(console_handler)
121
110
 
122
111
  def set_correlation_id(self, correlation_id: str):
123
- """Sets a correlation ID for tracking a request or transaction.
124
-
125
- This ID is automatically included in all subsequent logs and traces,
126
- allowing for easy filtering and correlation of telemetry data.
112
+ """Set correlation ID for request/transaction tracking.
127
113
 
128
114
  Args:
129
- correlation_id: The unique identifier for the transaction.
115
+ correlation_id: Unique identifier for transaction correlation
130
116
  """
131
117
  self._correlation_id = correlation_id
132
118
 
133
119
  def get_correlation_id(self) -> Optional[str]:
134
- """Retrieves the current correlation ID.
120
+ """Get current correlation ID.
135
121
 
136
122
  Returns:
137
- The current correlation ID if it has been set, otherwise None.
123
+ Current correlation ID if set, otherwise None
138
124
  """
139
125
  return self._correlation_id
140
126
 
141
127
  def set_baggage(self, key: str, value: str) -> Context:
142
- """
143
- Set a baggage item in the current context
128
+ """Set baggage item in OpenTelemetry context.
144
129
 
145
130
  Args:
146
131
  key: Baggage key
147
132
  value: Baggage value
148
133
 
149
134
  Returns:
150
- Updated context with the new baggage item
135
+ Updated context with baggage item
151
136
  """
152
137
  return baggage.set_baggage(key, value)
153
138
 
154
139
  def get_baggage(self, key: str) -> Optional[str]:
155
- """
156
- Retrieves a baggage item from the current OpenTelemetry context.
140
+ """Get baggage item from current context.
157
141
 
158
142
  Args:
159
- key: The baggage key.
143
+ key: Baggage key
160
144
 
161
145
  Returns:
162
- The baggage value if it exists, otherwise None.
146
+ Baggage value if exists, otherwise None
163
147
  """
164
148
  return baggage.get_baggage(key)
165
149
 
166
150
  def get_all_baggage(self) -> Dict[str, str]:
167
- """
168
- Retrieves all baggage items from the current OpenTelemetry context.
151
+ """Get all baggage items from current context.
169
152
 
170
153
  Returns:
171
- A dictionary containing all baggage items.
154
+ Dictionary of all baggage items
172
155
  """
173
156
  return dict(baggage.get_all())
174
157
 
175
158
  def _enhance_extra(self, extra: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
176
- """
177
- Enriches log records with contextual information.
178
-
179
- This method adds service details, correlation IDs, trace context,
180
- and baggage items to the log's `extra` dictionary.
159
+ """Enrich log records with contextual information.
181
160
 
182
161
  Args:
183
- extra: An optional dictionary of custom data to include.
162
+ extra: Optional custom data dictionary
184
163
 
185
164
  Returns:
186
- The enhanced dictionary with added contextual data.
165
+ Enhanced dictionary with service context, correlation ID, trace
166
+ context, and baggage items
187
167
  """
188
168
  enhanced_extra = {
189
169
  "service_name": self.service_name,
@@ -212,15 +192,15 @@ class AzureLogger:
212
192
  return enhanced_extra
213
193
 
214
194
  def debug(self, message: str, extra: Optional[Dict[str, Any]] = None):
215
- """Logs a debug message with enhanced context."""
195
+ """Log debug message with enhanced context."""
216
196
  self.logger.debug(message, extra=self._enhance_extra(extra))
217
197
 
218
198
  def info(self, message: str, extra: Optional[Dict[str, Any]] = None):
219
- """Logs an info message with enhanced context."""
199
+ """Log info message with enhanced context."""
220
200
  self.logger.info(message, extra=self._enhance_extra(extra))
221
201
 
222
202
  def warning(self, message: str, extra: Optional[Dict[str, Any]] = None):
223
- """Logs a warning message with enhanced context."""
203
+ """Log warning message with enhanced context."""
224
204
  self.logger.warning(message, extra=self._enhance_extra(extra))
225
205
 
226
206
  def error(
@@ -229,11 +209,11 @@ class AzureLogger:
229
209
  extra: Optional[Dict[str, Any]] = None,
230
210
  exc_info: bool = True,
231
211
  ):
232
- """Logs an error message with enhanced context and exception info."""
212
+ """Log error message with enhanced context and exception info."""
233
213
  self.logger.error(message, extra=self._enhance_extra(extra), exc_info=exc_info)
234
214
 
235
215
  def critical(self, message: str, extra: Optional[Dict[str, Any]] = None):
236
- """Logs a critical message with enhanced context."""
216
+ """Log critical message with enhanced context."""
237
217
  self.logger.critical(message, extra=self._enhance_extra(extra))
238
218
 
239
219
  def log_function_execution(
@@ -243,17 +223,13 @@ class AzureLogger:
243
223
  success: bool = True,
244
224
  extra: Optional[Dict[str, Any]] = None,
245
225
  ):
246
- """
247
- Logs a custom event for a function's execution metrics.
248
-
249
- This captures performance data such as duration and success status,
250
- which can be queried in Azure Monitor.
226
+ """Log function execution metrics for performance monitoring.
251
227
 
252
228
  Args:
253
- function_name: The name of the executed function.
254
- duration_ms: The execution duration in milliseconds.
255
- success: A flag indicating whether the function executed successfully.
256
- extra: Additional custom properties to include in the log.
229
+ function_name: Name of executed function
230
+ duration_ms: Execution duration in milliseconds
231
+ success: Whether function executed successfully
232
+ extra: Additional custom properties
257
233
  """
258
234
  log_data = {
259
235
  "function_name": function_name,
@@ -265,7 +241,6 @@ class AzureLogger:
265
241
  if extra:
266
242
  log_data.update(extra)
267
243
 
268
- log_level = logging.INFO if success else logging.ERROR
269
244
  message = f"Function '{function_name}' executed in {duration_ms:.2f}ms - {'SUCCESS' if success else 'FAILED'}"
270
245
 
271
246
  if success:
@@ -281,13 +256,12 @@ class AzureLogger:
281
256
  duration_ms: float,
282
257
  extra: Optional[Dict[str, Any]] = None,
283
258
  ):
284
- """
285
- Log HTTP request with comprehensive details
259
+ """Log HTTP request with comprehensive details.
286
260
 
287
261
  Args:
288
- method: HTTP method
262
+ method: HTTP method (GET, POST, etc.)
289
263
  url: Request URL
290
- status_code: HTTP status code
264
+ status_code: HTTP response status code
291
265
  duration_ms: Request duration in milliseconds
292
266
  extra: Additional custom properties
293
267
  """
@@ -302,7 +276,7 @@ class AzureLogger:
302
276
  if extra:
303
277
  log_data.update(extra)
304
278
 
305
- # Determine log level based on status code
279
+ # Determine log level and status based on status code
306
280
  if status_code < 400:
307
281
  log_level = logging.INFO
308
282
  status_text = "SUCCESS"
@@ -323,19 +297,18 @@ class AzureLogger:
323
297
  span_name: str,
324
298
  attributes: Optional[Dict[str, Union[str, int, float, bool]]] = None,
325
299
  ) -> Span:
326
- """
327
- Create a new span for distributed tracing
300
+ """Create OpenTelemetry span for distributed tracing.
328
301
 
329
302
  Args:
330
- span_name: Name of the span
331
- attributes: Initial attributes for the span
303
+ span_name: Name for the span
304
+ attributes: Initial span attributes
332
305
 
333
306
  Returns:
334
307
  OpenTelemetry span context manager
335
308
  """
336
309
  span = self.tracer.start_span(span_name)
337
310
 
338
- # Add default attributes
311
+ # Add default service attributes
339
312
  span.set_attribute("service.name", self.service_name)
340
313
  span.set_attribute("service.version", self.service_version)
341
314
 
@@ -357,21 +330,65 @@ class AzureLogger:
357
330
  log_args: bool,
358
331
  args: tuple,
359
332
  kwargs: dict,
333
+ log_result: bool,
334
+ log_execution: bool,
360
335
  ):
361
- """Helper to set up span attributes for function tracing."""
336
+ """Configure span attributes for function tracing."""
362
337
  span.set_attribute("function.name", func.__name__)
363
338
  span.set_attribute("function.module", func.__module__)
364
339
  span.set_attribute("service.name", self.service_name)
365
340
  span.set_attribute("function.is_async", is_async)
366
341
 
342
+ # Add decorator parameters as span attributes
343
+ span.set_attribute("function.decorator.log_args", log_args)
344
+ span.set_attribute("function.decorator.log_result", log_result)
345
+ span.set_attribute("function.decorator.log_execution", log_execution)
346
+
367
347
  if self._correlation_id:
368
348
  span.set_attribute("correlation.id", self._correlation_id)
369
349
 
370
350
  if log_args:
371
351
  if args:
372
352
  span.set_attribute("function.args_count", len(args))
353
+ # Add positional arguments as span attributes
354
+ import inspect
355
+ try:
356
+ sig = inspect.signature(func)
357
+ param_names = list(sig.parameters.keys())
358
+ for i, arg_value in enumerate(args):
359
+ param_name = param_names[i] if i < len(param_names) else f"arg_{i}"
360
+ try:
361
+ # Convert to string for safe serialization
362
+ attr_value = str(arg_value)
363
+ # Truncate if too long to avoid excessive data
364
+ if len(attr_value) > 1000:
365
+ attr_value = attr_value[:1000] + "..."
366
+ span.set_attribute(f"function.arg.{param_name}", attr_value)
367
+ except Exception:
368
+ span.set_attribute(f"function.arg.{param_name}", "<non-serializable>")
369
+ except Exception:
370
+ # Fallback if signature inspection fails
371
+ for i, arg_value in enumerate(args):
372
+ try:
373
+ attr_value = str(arg_value)
374
+ if len(attr_value) > 1000:
375
+ attr_value = attr_value[:1000] + "..."
376
+ span.set_attribute(f"function.arg.{i}", attr_value)
377
+ except Exception:
378
+ span.set_attribute(f"function.arg.{i}", "<non-serializable>")
379
+
373
380
  if kwargs:
374
381
  span.set_attribute("function.kwargs_count", len(kwargs))
382
+ # Add keyword arguments as span attributes
383
+ for key, value in kwargs.items():
384
+ try:
385
+ attr_value = str(value)
386
+ # Truncate if too long to avoid excessive data
387
+ if len(attr_value) > 1000:
388
+ attr_value = attr_value[:1000] + "..."
389
+ span.set_attribute(f"function.kwarg.{key}", attr_value)
390
+ except Exception:
391
+ span.set_attribute(f"function.kwarg.{key}", "<non-serializable>")
375
392
 
376
393
  def _handle_function_success(
377
394
  self,
@@ -385,7 +402,7 @@ class AzureLogger:
385
402
  args: tuple,
386
403
  kwargs: dict,
387
404
  ):
388
- """Helper to handle successful function execution tracing."""
405
+ """Handle successful function execution in tracing."""
389
406
  span.set_attribute("function.duration_ms", duration_ms)
390
407
  span.set_attribute("function.success", True)
391
408
  span.set_status(Status(StatusCode.OK))
@@ -418,7 +435,7 @@ class AzureLogger:
418
435
  log_execution: bool,
419
436
  is_async: bool,
420
437
  ):
421
- """Helper to handle failed function execution tracing."""
438
+ """Handle failed function execution in tracing."""
422
439
  span.set_status(Status(StatusCode.ERROR, str(e)))
423
440
  span.record_exception(e)
424
441
  span.set_attribute("function.duration_ms", duration_ms)
@@ -444,19 +461,20 @@ class AzureLogger:
444
461
  def trace_function(
445
462
  self,
446
463
  function_name: Optional[str] = None,
447
- log_args: bool = False,
448
- log_result: bool = False,
449
464
  log_execution: bool = True,
465
+ log_args: bool = True,
466
+ log_result: bool = False
450
467
  ) -> Callable:
451
- """
452
- Decorator to automatically trace function execution with comprehensive logging.
453
- Supports both synchronous and asynchronous functions.
468
+ """Decorator for automatic function execution tracing.
469
+
470
+ Supports both synchronous and asynchronous functions with comprehensive
471
+ logging and OpenTelemetry span creation.
454
472
 
455
473
  Args:
456
- function_name: Custom name for the span (defaults to function name)
474
+ function_name: Custom span name (defaults to function name)
475
+ log_execution: Whether to log execution metrics
457
476
  log_args: Whether to log function arguments
458
477
  log_result: Whether to log function result
459
- log_execution: Whether to log execution metrics
460
478
  """
461
479
 
462
480
  def decorator(func):
@@ -465,7 +483,7 @@ class AzureLogger:
465
483
  span_name = function_name or f"{func.__module__}.{func.__name__}"
466
484
  with self.tracer.start_as_current_span(span_name) as span:
467
485
  self._setup_span_for_function_trace(
468
- span, func, True, log_args, args, kwargs
486
+ span, func, True, log_args, args, kwargs, log_result, log_execution
469
487
  )
470
488
  start_time = time.time()
471
489
  try:
@@ -498,7 +516,7 @@ class AzureLogger:
498
516
  span_name = function_name or f"{func.__module__}.{func.__name__}"
499
517
  with self.tracer.start_as_current_span(span_name) as span:
500
518
  self._setup_span_for_function_trace(
501
- span, func, False, log_args, args, kwargs
519
+ span, func, False, log_args, args, kwargs, log_result, log_execution
502
520
  )
503
521
  start_time = time.time()
504
522
  try:
@@ -524,7 +542,7 @@ class AzureLogger:
524
542
  )
525
543
  raise
526
544
 
527
- # Return the appropriate wrapper based on whether the function is async
545
+ # Return appropriate wrapper based on function type
528
546
  if asyncio.iscoroutinefunction(func):
529
547
  return async_wrapper
530
548
  return sync_wrapper
@@ -532,14 +550,14 @@ class AzureLogger:
532
550
  return decorator
533
551
 
534
552
  def add_span_attributes(self, attributes: Dict[str, Union[str, int, float, bool]]):
535
- """Add attributes to the current active span"""
553
+ """Add attributes to current active span."""
536
554
  current_span = trace.get_current_span()
537
555
  if current_span and current_span.is_recording():
538
556
  for key, value in attributes.items():
539
557
  current_span.set_attribute(key, value)
540
558
 
541
559
  def add_span_event(self, name: str, attributes: Optional[Dict[str, Any]] = None):
542
- """Add an event to the current active span"""
560
+ """Add event to current active span."""
543
561
  current_span = trace.get_current_span()
544
562
  if current_span and current_span.is_recording():
545
563
  event_attributes = attributes or {}
@@ -550,7 +568,7 @@ class AzureLogger:
550
568
  def set_span_status(
551
569
  self, status_code: StatusCode, description: Optional[str] = None
552
570
  ):
553
- """Set the status of the current active span"""
571
+ """Set status of current active span."""
554
572
  current_span = trace.get_current_span()
555
573
  if current_span and current_span.is_recording():
556
574
  current_span.set_status(Status(status_code, description))
@@ -563,15 +581,14 @@ class AzureLogger:
563
581
  extra: Optional[Dict[str, Any]] = None,
564
582
  span_attributes: Optional[Dict[str, Union[str, int, float, bool]]] = None,
565
583
  ):
566
- """
567
- Log a message within a span context
584
+ """Log message within a span context.
568
585
 
569
586
  Args:
570
- span_name: Name of the span
587
+ span_name: Name for the span
571
588
  message: Log message
572
- level: Log level
589
+ level: Python logging level
573
590
  extra: Additional log properties
574
- span_attributes: Attributes to add to the span
591
+ span_attributes: Attributes to add to span
575
592
  """
576
593
  with self.tracer.start_as_current_span(span_name) as span:
577
594
  if span_attributes:
@@ -589,14 +606,13 @@ class AzureLogger:
589
606
  duration_ms: float,
590
607
  extra: Optional[Dict[str, Any]] = None,
591
608
  ):
592
- """
593
- Log external dependency calls (Database, HTTP, etc.)
609
+ """Log external dependency calls for monitoring.
594
610
 
595
611
  Args:
596
612
  dependency_type: Type of dependency (SQL, HTTP, etc.)
597
- name: Name/identifier of the dependency
613
+ name: Dependency identifier
598
614
  command: Command/query executed
599
- success: Whether the call was successful
615
+ success: Whether call was successful
600
616
  duration_ms: Call duration in milliseconds
601
617
  extra: Additional properties
602
618
  """
@@ -619,10 +635,9 @@ class AzureLogger:
619
635
  self.logger.log(log_level, message, extra=self._enhance_extra(log_data))
620
636
 
621
637
  def flush(self):
622
- """Flush all pending telemetry data"""
638
+ """Flush pending telemetry data."""
623
639
  if self._telemetry_enabled:
624
640
  try:
625
- # Force flush any pending telemetry
626
641
  from opentelemetry.sdk.trace import TracerProvider
627
642
 
628
643
  tracer_provider = trace.get_tracer_provider()
@@ -632,7 +647,7 @@ class AzureLogger:
632
647
  self.warning(f"Failed to flush telemetry: {e}")
633
648
 
634
649
 
635
- # Factory functions for easy instantiation
650
+ # Factory functions with logger caching
636
651
  _loggers: Dict[Any, "AzureLogger"] = {}
637
652
 
638
653
 
@@ -645,18 +660,18 @@ def create_app_logger(
645
660
  custom_resource_attributes: Optional[Dict[str, str]] = None,
646
661
  instrumentation_options: Optional[Dict[str, Any]] = None,
647
662
  ) -> AzureLogger:
648
- """
649
- Factory function to create an AzureLogger instance. If a logger with the
650
- same configuration has already been created, it will be returned.
663
+ """Create cached AzureLogger instance for applications.
664
+
665
+ Returns existing logger if one with same configuration exists.
651
666
 
652
667
  Args:
653
- service_name: Name of your service/application
654
- service_version: Version of your service
668
+ service_name: Service identifier for telemetry
669
+ service_version: Service version for metadata
655
670
  connection_string: Application Insights connection string
656
- log_level: Logging level
671
+ log_level: Python logging level
657
672
  enable_console_logging: Enable console output
658
- custom_resource_attributes: Additional resource attributes
659
- instrumentation_options: Instrumentation options
673
+ custom_resource_attributes: Additional OpenTelemetry resource attributes
674
+ instrumentation_options: Azure Monitor instrumentation options
660
675
 
661
676
  Returns:
662
677
  Configured AzureLogger instance
@@ -702,18 +717,17 @@ def create_function_logger(
702
717
  connection_string: Optional[str] = None,
703
718
  instrumentation_options: Optional[Dict[str, Any]] = None,
704
719
  ) -> AzureLogger:
705
- """
706
- Factory function specifically for Azure Functions
720
+ """Create AzureLogger optimized for Azure Functions.
707
721
 
708
722
  Args:
709
- function_app_name: Name of the Function App
710
- function_name: Name of the specific function
711
- service_version: Version of the service
723
+ function_app_name: Azure Function App name
724
+ function_name: Specific function name
725
+ service_version: Service version for metadata
712
726
  connection_string: Application Insights connection string
713
- instrumentation_options: Instrumentation options
727
+ instrumentation_options: Azure Monitor instrumentation options
714
728
 
715
729
  Returns:
716
- Configured AzureLogger instance for Azure Functions
730
+ Configured AzureLogger with Azure Functions context
717
731
  """
718
732
  custom_attributes = {
719
733
  "azure.function.app": function_app_name,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: azpaddypy
3
- Version: 0.2.7
3
+ Version: 0.2.9
4
4
  Summary: Comprehensive Python logger for Azure, integrating OpenTelemetry for advanced, structured, and distributed tracing.
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: Operating System :: OS Independent
@@ -8,11 +8,10 @@ Requires-Python: >=3.11
8
8
  Description-Content-Type: text/markdown
9
9
  License-File: LICENSE
10
10
  Requires-Dist: azure-monitor-opentelemetry==1.6.10
11
+ Requires-Dist: azure-functions==1.23.0
11
12
  Provides-Extra: dev
12
- Requires-Dist: trio>=0.22.2; extra == "dev"
13
- Requires-Dist: azure-functions>=1.17.0; extra == "dev"
14
- Requires-Dist: pytest>=7.4.0; extra == "dev"
15
- Requires-Dist: pytest-asyncio>=0.21.1; extra == "dev"
16
- Requires-Dist: anyio>=3.7.1; extra == "dev"
17
- Requires-Dist: pip; extra == "dev"
13
+ Requires-Dist: azure-functions==1.23.0; extra == "dev"
14
+ Requires-Dist: pytest==8.4.1; extra == "dev"
15
+ Requires-Dist: pytest-asyncio==1.0.0; extra == "dev"
16
+ Requires-Dist: anyio==4.9.0; extra == "dev"
18
17
  Dynamic: license-file
@@ -0,0 +1,10 @@
1
+ azpaddypy/mgmt/__init__.py,sha256=-jH8Ftx9C8qu4yF5dMVEapVZhNwG7m4QCUjyutesOoY,278
2
+ azpaddypy/mgmt/identity.py,sha256=mA_krQslMsK_sDob-z-QA0B9khK_JUO2way7xwPopR8,12001
3
+ azpaddypy/mgmt/logging.py,sha256=War3h9Td7KZyeOsvOz3t2_ESNQzJ3jeZcrFL5octoGo,28081
4
+ azpaddypy/test_function/__init__.py,sha256=0NjUl36wvUWV79GpRwBFkgkBaC6uDZsTdaSVOIHMFEU,3481
5
+ azpaddypy/test_function/function_app.py,sha256=6nX54-iq0L1l_hZpD6E744-j79oLxdaldFyWDCpwH7c,3867
6
+ azpaddypy-0.2.9.dist-info/licenses/LICENSE,sha256=hQ6t0g2QaewGCQICHqTckBFbMVakGmoyTAzDpmEYV4c,1089
7
+ azpaddypy-0.2.9.dist-info/METADATA,sha256=1qA7Y7s8jAjSkxl37SeQW8bCHfucxV8ct_mXcXcdBiE,705
8
+ azpaddypy-0.2.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
+ azpaddypy-0.2.9.dist-info/top_level.txt,sha256=hsDuboDhT61320ML8X479ezSTwT3rrlDWz1_Z45B2cs,10
10
+ azpaddypy-0.2.9.dist-info/RECORD,,
@@ -1,9 +0,0 @@
1
- azpaddypy/mgmt/__init__.py,sha256=5-0eZuJMZlCONZNJ5hEvWXfvLIM36mg7FsEVs32bY_A,183
2
- azpaddypy/mgmt/logging.py,sha256=-otodXu7LhOigQgoZiuGTy83i79NUsgF0AGcZWVHnBE,26559
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.7.dist-info/licenses/LICENSE,sha256=hQ6t0g2QaewGCQICHqTckBFbMVakGmoyTAzDpmEYV4c,1089
6
- azpaddypy-0.2.7.dist-info/METADATA,sha256=_RJdtcXLOeLiysiCLkDLwGFTv8EUhRHHfP11P54kDng,747
7
- azpaddypy-0.2.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
- azpaddypy-0.2.7.dist-info/top_level.txt,sha256=hsDuboDhT61320ML8X479ezSTwT3rrlDWz1_Z45B2cs,10
9
- azpaddypy-0.2.7.dist-info/RECORD,,