azpaddypy 0.2.6__py3-none-any.whl → 0.2.8__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__(
@@ -35,17 +35,18 @@ class AzureLogger:
35
35
  log_level: int = logging.INFO,
36
36
  enable_console_logging: bool = True,
37
37
  custom_resource_attributes: Optional[Dict[str, str]] = None,
38
+ instrumentation_options: Optional[Dict[str, Any]] = None,
38
39
  ):
39
- """
40
- Initialize the Azure Logger with OpenTelemetry tracing
40
+ """Initialize Azure Logger with OpenTelemetry tracing.
41
41
 
42
42
  Args:
43
- service_name: Name of your service/application
44
- service_version: Version of your service
43
+ service_name: Service identifier for telemetry
44
+ service_version: Service version for metadata
45
45
  connection_string: Application Insights connection string
46
- log_level: Logging level (default: INFO)
46
+ log_level: Python logging level (default: INFO)
47
47
  enable_console_logging: Enable console output for local development
48
- custom_resource_attributes: Additional resource attributes
48
+ custom_resource_attributes: Additional OpenTelemetry resource attributes
49
+ instrumentation_options: Azure Monitor instrumentation options
49
50
  """
50
51
  self.service_name = service_name
51
52
  self.service_version = service_version
@@ -53,7 +54,7 @@ class AzureLogger:
53
54
  "APPLICATIONINSIGHTS_CONNECTION_STRING"
54
55
  )
55
56
 
56
- # Prepare resource attributes
57
+ # Configure resource attributes
57
58
  resource_attributes = {
58
59
  "service.name": service_name,
59
60
  "service.version": service_version,
@@ -63,13 +64,14 @@ class AzureLogger:
63
64
  if custom_resource_attributes:
64
65
  resource_attributes.update(custom_resource_attributes)
65
66
 
66
- # Configure Azure Monitor if connection string is available
67
+ # Configure Azure Monitor if connection string available
67
68
  if self.connection_string:
68
69
  try:
69
70
  configure_azure_monitor(
70
71
  connection_string=self.connection_string,
71
72
  resource_attributes=resource_attributes,
72
73
  enable_live_metrics=True,
74
+ instrumentation_options=instrumentation_options,
73
75
  )
74
76
  self._telemetry_enabled = True
75
77
  except Exception as e:
@@ -81,21 +83,16 @@ class AzureLogger:
81
83
  "Warning: No Application Insights connection string found. Telemetry disabled."
82
84
  )
83
85
 
84
- # Set up logger without instrumentor
86
+ # Configure Python logger
85
87
  self.logger = logging.getLogger(service_name)
86
88
  self.logger.setLevel(log_level)
87
-
88
- # Clear existing handlers to avoid duplicates
89
89
  self.logger.handlers.clear()
90
90
 
91
- # Add console handler for local development
92
91
  if enable_console_logging:
93
92
  self._setup_console_handler()
94
93
 
95
- # Get tracer for manual span creation
94
+ # Initialize OpenTelemetry tracer and correlation context
96
95
  self.tracer = trace.get_tracer(__name__)
97
-
98
- # Initialize correlation context
99
96
  self._correlation_id = None
100
97
 
101
98
  self.info(
@@ -103,12 +100,7 @@ class AzureLogger:
103
100
  )
104
101
 
105
102
  def _setup_console_handler(self):
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
- """
103
+ """Configure console handler for local development."""
112
104
  console_handler = logging.StreamHandler()
113
105
  formatter = logging.Formatter(
114
106
  "%(asctime)s - %(name)s - %(levelname)s - %(message)s - %(pathname)s:%(lineno)d"
@@ -117,70 +109,61 @@ class AzureLogger:
117
109
  self.logger.addHandler(console_handler)
118
110
 
119
111
  def set_correlation_id(self, correlation_id: str):
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.
112
+ """Set correlation ID for request/transaction tracking.
124
113
 
125
114
  Args:
126
- correlation_id: The unique identifier for the transaction.
115
+ correlation_id: Unique identifier for transaction correlation
127
116
  """
128
117
  self._correlation_id = correlation_id
129
118
 
130
119
  def get_correlation_id(self) -> Optional[str]:
131
- """Retrieves the current correlation ID.
120
+ """Get current correlation ID.
132
121
 
133
122
  Returns:
134
- The current correlation ID if it has been set, otherwise None.
123
+ Current correlation ID if set, otherwise None
135
124
  """
136
125
  return self._correlation_id
137
126
 
138
127
  def set_baggage(self, key: str, value: str) -> Context:
139
- """
140
- Set a baggage item in the current context
128
+ """Set baggage item in OpenTelemetry context.
141
129
 
142
130
  Args:
143
131
  key: Baggage key
144
132
  value: Baggage value
145
133
 
146
134
  Returns:
147
- Updated context with the new baggage item
135
+ Updated context with baggage item
148
136
  """
149
137
  return baggage.set_baggage(key, value)
150
138
 
151
139
  def get_baggage(self, key: str) -> Optional[str]:
152
- """
153
- Retrieves a baggage item from the current OpenTelemetry context.
140
+ """Get baggage item from current context.
154
141
 
155
142
  Args:
156
- key: The baggage key.
143
+ key: Baggage key
157
144
 
158
145
  Returns:
159
- The baggage value if it exists, otherwise None.
146
+ Baggage value if exists, otherwise None
160
147
  """
161
148
  return baggage.get_baggage(key)
162
149
 
163
150
  def get_all_baggage(self) -> Dict[str, str]:
164
- """
165
- Retrieves all baggage items from the current OpenTelemetry context.
151
+ """Get all baggage items from current context.
166
152
 
167
153
  Returns:
168
- A dictionary containing all baggage items.
154
+ Dictionary of all baggage items
169
155
  """
170
156
  return dict(baggage.get_all())
171
157
 
172
158
  def _enhance_extra(self, extra: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
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.
159
+ """Enrich log records with contextual information.
178
160
 
179
161
  Args:
180
- extra: An optional dictionary of custom data to include.
162
+ extra: Optional custom data dictionary
181
163
 
182
164
  Returns:
183
- The enhanced dictionary with added contextual data.
165
+ Enhanced dictionary with service context, correlation ID, trace
166
+ context, and baggage items
184
167
  """
185
168
  enhanced_extra = {
186
169
  "service_name": self.service_name,
@@ -209,15 +192,15 @@ class AzureLogger:
209
192
  return enhanced_extra
210
193
 
211
194
  def debug(self, message: str, extra: Optional[Dict[str, Any]] = None):
212
- """Logs a debug message with enhanced context."""
195
+ """Log debug message with enhanced context."""
213
196
  self.logger.debug(message, extra=self._enhance_extra(extra))
214
197
 
215
198
  def info(self, message: str, extra: Optional[Dict[str, Any]] = None):
216
- """Logs an info message with enhanced context."""
199
+ """Log info message with enhanced context."""
217
200
  self.logger.info(message, extra=self._enhance_extra(extra))
218
201
 
219
202
  def warning(self, message: str, extra: Optional[Dict[str, Any]] = None):
220
- """Logs a warning message with enhanced context."""
203
+ """Log warning message with enhanced context."""
221
204
  self.logger.warning(message, extra=self._enhance_extra(extra))
222
205
 
223
206
  def error(
@@ -226,11 +209,11 @@ class AzureLogger:
226
209
  extra: Optional[Dict[str, Any]] = None,
227
210
  exc_info: bool = True,
228
211
  ):
229
- """Logs an error message with enhanced context and exception info."""
212
+ """Log error message with enhanced context and exception info."""
230
213
  self.logger.error(message, extra=self._enhance_extra(extra), exc_info=exc_info)
231
214
 
232
215
  def critical(self, message: str, extra: Optional[Dict[str, Any]] = None):
233
- """Logs a critical message with enhanced context."""
216
+ """Log critical message with enhanced context."""
234
217
  self.logger.critical(message, extra=self._enhance_extra(extra))
235
218
 
236
219
  def log_function_execution(
@@ -240,17 +223,13 @@ class AzureLogger:
240
223
  success: bool = True,
241
224
  extra: Optional[Dict[str, Any]] = None,
242
225
  ):
243
- """
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.
226
+ """Log function execution metrics for performance monitoring.
248
227
 
249
228
  Args:
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.
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
254
233
  """
255
234
  log_data = {
256
235
  "function_name": function_name,
@@ -262,7 +241,6 @@ class AzureLogger:
262
241
  if extra:
263
242
  log_data.update(extra)
264
243
 
265
- log_level = logging.INFO if success else logging.ERROR
266
244
  message = f"Function '{function_name}' executed in {duration_ms:.2f}ms - {'SUCCESS' if success else 'FAILED'}"
267
245
 
268
246
  if success:
@@ -278,13 +256,12 @@ class AzureLogger:
278
256
  duration_ms: float,
279
257
  extra: Optional[Dict[str, Any]] = None,
280
258
  ):
281
- """
282
- Log HTTP request with comprehensive details
259
+ """Log HTTP request with comprehensive details.
283
260
 
284
261
  Args:
285
- method: HTTP method
262
+ method: HTTP method (GET, POST, etc.)
286
263
  url: Request URL
287
- status_code: HTTP status code
264
+ status_code: HTTP response status code
288
265
  duration_ms: Request duration in milliseconds
289
266
  extra: Additional custom properties
290
267
  """
@@ -299,7 +276,7 @@ class AzureLogger:
299
276
  if extra:
300
277
  log_data.update(extra)
301
278
 
302
- # Determine log level based on status code
279
+ # Determine log level and status based on status code
303
280
  if status_code < 400:
304
281
  log_level = logging.INFO
305
282
  status_text = "SUCCESS"
@@ -320,19 +297,18 @@ class AzureLogger:
320
297
  span_name: str,
321
298
  attributes: Optional[Dict[str, Union[str, int, float, bool]]] = None,
322
299
  ) -> Span:
323
- """
324
- Create a new span for distributed tracing
300
+ """Create OpenTelemetry span for distributed tracing.
325
301
 
326
302
  Args:
327
- span_name: Name of the span
328
- attributes: Initial attributes for the span
303
+ span_name: Name for the span
304
+ attributes: Initial span attributes
329
305
 
330
306
  Returns:
331
307
  OpenTelemetry span context manager
332
308
  """
333
309
  span = self.tracer.start_span(span_name)
334
310
 
335
- # Add default attributes
311
+ # Add default service attributes
336
312
  span.set_attribute("service.name", self.service_name)
337
313
  span.set_attribute("service.version", self.service_version)
338
314
 
@@ -354,21 +330,65 @@ class AzureLogger:
354
330
  log_args: bool,
355
331
  args: tuple,
356
332
  kwargs: dict,
333
+ log_result: bool,
334
+ log_execution: bool,
357
335
  ):
358
- """Helper to set up span attributes for function tracing."""
336
+ """Configure span attributes for function tracing."""
359
337
  span.set_attribute("function.name", func.__name__)
360
338
  span.set_attribute("function.module", func.__module__)
361
339
  span.set_attribute("service.name", self.service_name)
362
340
  span.set_attribute("function.is_async", is_async)
363
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
+
364
347
  if self._correlation_id:
365
348
  span.set_attribute("correlation.id", self._correlation_id)
366
349
 
367
350
  if log_args:
368
351
  if args:
369
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
+
370
380
  if kwargs:
371
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>")
372
392
 
373
393
  def _handle_function_success(
374
394
  self,
@@ -382,7 +402,7 @@ class AzureLogger:
382
402
  args: tuple,
383
403
  kwargs: dict,
384
404
  ):
385
- """Helper to handle successful function execution tracing."""
405
+ """Handle successful function execution in tracing."""
386
406
  span.set_attribute("function.duration_ms", duration_ms)
387
407
  span.set_attribute("function.success", True)
388
408
  span.set_status(Status(StatusCode.OK))
@@ -415,7 +435,7 @@ class AzureLogger:
415
435
  log_execution: bool,
416
436
  is_async: bool,
417
437
  ):
418
- """Helper to handle failed function execution tracing."""
438
+ """Handle failed function execution in tracing."""
419
439
  span.set_status(Status(StatusCode.ERROR, str(e)))
420
440
  span.record_exception(e)
421
441
  span.set_attribute("function.duration_ms", duration_ms)
@@ -441,19 +461,20 @@ class AzureLogger:
441
461
  def trace_function(
442
462
  self,
443
463
  function_name: Optional[str] = None,
444
- log_args: bool = False,
445
- log_result: bool = False,
446
464
  log_execution: bool = True,
465
+ log_args: bool = True,
466
+ log_result: bool = False
447
467
  ) -> Callable:
448
- """
449
- Decorator to automatically trace function execution with comprehensive logging.
450
- 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.
451
472
 
452
473
  Args:
453
- 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
454
476
  log_args: Whether to log function arguments
455
477
  log_result: Whether to log function result
456
- log_execution: Whether to log execution metrics
457
478
  """
458
479
 
459
480
  def decorator(func):
@@ -462,7 +483,7 @@ class AzureLogger:
462
483
  span_name = function_name or f"{func.__module__}.{func.__name__}"
463
484
  with self.tracer.start_as_current_span(span_name) as span:
464
485
  self._setup_span_for_function_trace(
465
- span, func, True, log_args, args, kwargs
486
+ span, func, True, log_args, args, kwargs, log_result, log_execution
466
487
  )
467
488
  start_time = time.time()
468
489
  try:
@@ -495,7 +516,7 @@ class AzureLogger:
495
516
  span_name = function_name or f"{func.__module__}.{func.__name__}"
496
517
  with self.tracer.start_as_current_span(span_name) as span:
497
518
  self._setup_span_for_function_trace(
498
- span, func, False, log_args, args, kwargs
519
+ span, func, False, log_args, args, kwargs, log_result, log_execution
499
520
  )
500
521
  start_time = time.time()
501
522
  try:
@@ -521,7 +542,7 @@ class AzureLogger:
521
542
  )
522
543
  raise
523
544
 
524
- # Return the appropriate wrapper based on whether the function is async
545
+ # Return appropriate wrapper based on function type
525
546
  if asyncio.iscoroutinefunction(func):
526
547
  return async_wrapper
527
548
  return sync_wrapper
@@ -529,14 +550,14 @@ class AzureLogger:
529
550
  return decorator
530
551
 
531
552
  def add_span_attributes(self, attributes: Dict[str, Union[str, int, float, bool]]):
532
- """Add attributes to the current active span"""
553
+ """Add attributes to current active span."""
533
554
  current_span = trace.get_current_span()
534
555
  if current_span and current_span.is_recording():
535
556
  for key, value in attributes.items():
536
557
  current_span.set_attribute(key, value)
537
558
 
538
559
  def add_span_event(self, name: str, attributes: Optional[Dict[str, Any]] = None):
539
- """Add an event to the current active span"""
560
+ """Add event to current active span."""
540
561
  current_span = trace.get_current_span()
541
562
  if current_span and current_span.is_recording():
542
563
  event_attributes = attributes or {}
@@ -547,7 +568,7 @@ class AzureLogger:
547
568
  def set_span_status(
548
569
  self, status_code: StatusCode, description: Optional[str] = None
549
570
  ):
550
- """Set the status of the current active span"""
571
+ """Set status of current active span."""
551
572
  current_span = trace.get_current_span()
552
573
  if current_span and current_span.is_recording():
553
574
  current_span.set_status(Status(status_code, description))
@@ -560,15 +581,14 @@ class AzureLogger:
560
581
  extra: Optional[Dict[str, Any]] = None,
561
582
  span_attributes: Optional[Dict[str, Union[str, int, float, bool]]] = None,
562
583
  ):
563
- """
564
- Log a message within a span context
584
+ """Log message within a span context.
565
585
 
566
586
  Args:
567
- span_name: Name of the span
587
+ span_name: Name for the span
568
588
  message: Log message
569
- level: Log level
589
+ level: Python logging level
570
590
  extra: Additional log properties
571
- span_attributes: Attributes to add to the span
591
+ span_attributes: Attributes to add to span
572
592
  """
573
593
  with self.tracer.start_as_current_span(span_name) as span:
574
594
  if span_attributes:
@@ -586,14 +606,13 @@ class AzureLogger:
586
606
  duration_ms: float,
587
607
  extra: Optional[Dict[str, Any]] = None,
588
608
  ):
589
- """
590
- Log external dependency calls (Database, HTTP, etc.)
609
+ """Log external dependency calls for monitoring.
591
610
 
592
611
  Args:
593
612
  dependency_type: Type of dependency (SQL, HTTP, etc.)
594
- name: Name/identifier of the dependency
613
+ name: Dependency identifier
595
614
  command: Command/query executed
596
- success: Whether the call was successful
615
+ success: Whether call was successful
597
616
  duration_ms: Call duration in milliseconds
598
617
  extra: Additional properties
599
618
  """
@@ -616,10 +635,9 @@ class AzureLogger:
616
635
  self.logger.log(log_level, message, extra=self._enhance_extra(log_data))
617
636
 
618
637
  def flush(self):
619
- """Flush all pending telemetry data"""
638
+ """Flush pending telemetry data."""
620
639
  if self._telemetry_enabled:
621
640
  try:
622
- # Force flush any pending telemetry
623
641
  from opentelemetry.sdk.trace import TracerProvider
624
642
 
625
643
  tracer_provider = trace.get_tracer_provider()
@@ -629,7 +647,7 @@ class AzureLogger:
629
647
  self.warning(f"Failed to flush telemetry: {e}")
630
648
 
631
649
 
632
- # Factory functions for easy instantiation
650
+ # Factory functions with logger caching
633
651
  _loggers: Dict[Any, "AzureLogger"] = {}
634
652
 
635
653
 
@@ -640,18 +658,20 @@ def create_app_logger(
640
658
  log_level: int = logging.INFO,
641
659
  enable_console_logging: bool = True,
642
660
  custom_resource_attributes: Optional[Dict[str, str]] = None,
661
+ instrumentation_options: Optional[Dict[str, Any]] = None,
643
662
  ) -> AzureLogger:
644
- """
645
- Factory function to create an AzureLogger instance. If a logger with the
646
- 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.
647
666
 
648
667
  Args:
649
- service_name: Name of your service/application
650
- service_version: Version of your service
668
+ service_name: Service identifier for telemetry
669
+ service_version: Service version for metadata
651
670
  connection_string: Application Insights connection string
652
- log_level: Logging level
671
+ log_level: Python logging level
653
672
  enable_console_logging: Enable console output
654
- custom_resource_attributes: Additional resource attributes
673
+ custom_resource_attributes: Additional OpenTelemetry resource attributes
674
+ instrumentation_options: Azure Monitor instrumentation options
655
675
 
656
676
  Returns:
657
677
  Configured AzureLogger instance
@@ -695,18 +715,19 @@ def create_function_logger(
695
715
  function_name: str,
696
716
  service_version: str = "1.0.0",
697
717
  connection_string: Optional[str] = None,
718
+ instrumentation_options: Optional[Dict[str, Any]] = None,
698
719
  ) -> AzureLogger:
699
- """
700
- Factory function specifically for Azure Functions
720
+ """Create AzureLogger optimized for Azure Functions.
701
721
 
702
722
  Args:
703
- function_app_name: Name of the Function App
704
- function_name: Name of the specific function
705
- 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
706
726
  connection_string: Application Insights connection string
727
+ instrumentation_options: Azure Monitor instrumentation options
707
728
 
708
729
  Returns:
709
- Configured AzureLogger instance for Azure Functions
730
+ Configured AzureLogger with Azure Functions context
710
731
  """
711
732
  custom_attributes = {
712
733
  "azure.function.app": function_app_name,
@@ -719,4 +740,5 @@ def create_function_logger(
719
740
  service_version=service_version,
720
741
  connection_string=connection_string,
721
742
  custom_resource_attributes=custom_attributes,
743
+ instrumentation_options=instrumentation_options,
722
744
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: azpaddypy
3
- Version: 0.2.6
3
+ Version: 0.2.8
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.8.dist-info/licenses/LICENSE,sha256=hQ6t0g2QaewGCQICHqTckBFbMVakGmoyTAzDpmEYV4c,1089
7
+ azpaddypy-0.2.8.dist-info/METADATA,sha256=7zvYg76eo4n_0g_GFCqTwmC0pK4cgyVBrPuK4R52e6c,705
8
+ azpaddypy-0.2.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
+ azpaddypy-0.2.8.dist-info/top_level.txt,sha256=hsDuboDhT61320ML8X479ezSTwT3rrlDWz1_Z45B2cs,10
10
+ azpaddypy-0.2.8.dist-info/RECORD,,
@@ -1,9 +0,0 @@
1
- azpaddypy/mgmt/__init__.py,sha256=5-0eZuJMZlCONZNJ5hEvWXfvLIM36mg7FsEVs32bY_A,183
2
- azpaddypy/mgmt/logging.py,sha256=7Ug99lg8gsvCGq0HukR0qWVl-_vacEq-poq2rUj24d4,26060
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.6.dist-info/licenses/LICENSE,sha256=hQ6t0g2QaewGCQICHqTckBFbMVakGmoyTAzDpmEYV4c,1089
6
- azpaddypy-0.2.6.dist-info/METADATA,sha256=jycbdYSHrsBp0riAHepPVTGZ-oQNMGWzngkhwC0A7vk,747
7
- azpaddypy-0.2.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
- azpaddypy-0.2.6.dist-info/top_level.txt,sha256=hsDuboDhT61320ML8X479ezSTwT3rrlDWz1_Z45B2cs,10
9
- azpaddypy-0.2.6.dist-info/RECORD,,