azpaddypy 0.2.7__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.
- azpaddypy/mgmt/__init__.py +6 -2
- azpaddypy/mgmt/identity.py +335 -0
- azpaddypy/mgmt/logging.py +138 -124
- {azpaddypy-0.2.7.dist-info → azpaddypy-0.2.8.dist-info}/METADATA +6 -7
- azpaddypy-0.2.8.dist-info/RECORD +10 -0
- azpaddypy-0.2.7.dist-info/RECORD +0 -9
- {azpaddypy-0.2.7.dist-info → azpaddypy-0.2.8.dist-info}/WHEEL +0 -0
- {azpaddypy-0.2.7.dist-info → azpaddypy-0.2.8.dist-info}/licenses/LICENSE +0 -0
- {azpaddypy-0.2.7.dist-info → azpaddypy-0.2.8.dist-info}/top_level.txt +0 -0
azpaddypy/mgmt/__init__.py
CHANGED
@@ -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
|
7
|
+
from azpaddypy.mgmt.logging import AzureLogger
|
8
|
+
from azpaddypy.mgmt.identity import AzureIdentity
|
8
9
|
|
9
|
-
__all__ = [
|
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
|
-
|
24
|
-
|
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
|
-
|
27
|
-
|
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:
|
45
|
-
service_version:
|
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:
|
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:
|
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
|
-
#
|
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
|
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
|
-
#
|
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
|
-
#
|
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
|
-
"""
|
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
|
-
"""
|
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:
|
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
|
-
"""
|
120
|
+
"""Get current correlation ID.
|
135
121
|
|
136
122
|
Returns:
|
137
|
-
|
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
|
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:
|
143
|
+
key: Baggage key
|
160
144
|
|
161
145
|
Returns:
|
162
|
-
|
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
|
-
|
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:
|
162
|
+
extra: Optional custom data dictionary
|
184
163
|
|
185
164
|
Returns:
|
186
|
-
|
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
|
-
"""
|
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
|
-
"""
|
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
|
-
"""
|
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
|
-
"""
|
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
|
-
"""
|
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:
|
254
|
-
duration_ms:
|
255
|
-
success:
|
256
|
-
extra: Additional custom properties
|
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
|
331
|
-
attributes: Initial attributes
|
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
|
-
"""
|
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
|
-
"""
|
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
|
-
"""
|
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
|
-
|
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
|
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
|
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
|
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
|
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
|
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
|
587
|
+
span_name: Name for the span
|
571
588
|
message: Log message
|
572
|
-
level:
|
589
|
+
level: Python logging level
|
573
590
|
extra: Additional log properties
|
574
|
-
span_attributes: Attributes to add to
|
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:
|
613
|
+
name: Dependency identifier
|
598
614
|
command: Command/query executed
|
599
|
-
success: Whether
|
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
|
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
|
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
|
-
|
650
|
-
|
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:
|
654
|
-
service_version:
|
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:
|
671
|
+
log_level: Python logging level
|
657
672
|
enable_console_logging: Enable console output
|
658
|
-
custom_resource_attributes: Additional resource attributes
|
659
|
-
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:
|
710
|
-
function_name:
|
711
|
-
service_version:
|
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:
|
727
|
+
instrumentation_options: Azure Monitor instrumentation options
|
714
728
|
|
715
729
|
Returns:
|
716
|
-
Configured AzureLogger
|
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.
|
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:
|
13
|
-
Requires-Dist:
|
14
|
-
Requires-Dist: pytest
|
15
|
-
Requires-Dist:
|
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,,
|
azpaddypy-0.2.7.dist-info/RECORD
DELETED
@@ -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,,
|
File without changes
|
File without changes
|
File without changes
|