azpaddypy 0.3.8__py3-none-any.whl → 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- azpaddypy/mgmt/logging.py +916 -873
- {azpaddypy-0.3.8.dist-info → azpaddypy-0.4.0.dist-info}/METADATA +1 -1
- {azpaddypy-0.3.8.dist-info → azpaddypy-0.4.0.dist-info}/RECORD +6 -6
- {azpaddypy-0.3.8.dist-info → azpaddypy-0.4.0.dist-info}/WHEEL +0 -0
- {azpaddypy-0.3.8.dist-info → azpaddypy-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {azpaddypy-0.3.8.dist-info → azpaddypy-0.4.0.dist-info}/top_level.txt +0 -0
azpaddypy/mgmt/logging.py
CHANGED
@@ -1,873 +1,916 @@
|
|
1
|
-
import logging
|
2
|
-
import os
|
3
|
-
import functools
|
4
|
-
import time
|
5
|
-
import asyncio
|
6
|
-
import uuid
|
7
|
-
from typing import Optional, Dict, Any, Union, Callable
|
8
|
-
from datetime import datetime
|
9
|
-
from azure.monitor.opentelemetry import configure_azure_monitor
|
10
|
-
from opentelemetry import trace
|
11
|
-
from opentelemetry.trace import Status, StatusCode, Span
|
12
|
-
from opentelemetry import baggage
|
13
|
-
from opentelemetry.context import Context
|
14
|
-
|
15
|
-
|
16
|
-
class AzureLogger:
|
17
|
-
"""Azure-integrated logger with OpenTelemetry distributed tracing.
|
18
|
-
|
19
|
-
Provides comprehensive logging with Azure Monitor integration, correlation
|
20
|
-
tracking, baggage propagation, and automated function tracing for Azure
|
21
|
-
applications with seamless local development support.
|
22
|
-
|
23
|
-
CLOUD ROLE NAME INTEGRATION:
|
24
|
-
The service_name parameter automatically sets the cloud role name for
|
25
|
-
Application Insights. When multiple services emit telemetry to the same
|
26
|
-
Application Insights resource, each service will appear as a separate
|
27
|
-
node on the Application Map, enabling proper service topology visualization.
|
28
|
-
|
29
|
-
CORRELATION ID AUTOMATION:
|
30
|
-
The trace_function decorator automatically generates UUID4 correlation IDs
|
31
|
-
when none are manually set, ensuring consistent distributed tracing across
|
32
|
-
all function calls without requiring manual configuration.
|
33
|
-
|
34
|
-
Supports all standard logging levels (debug, info, warning, error, exception,
|
35
|
-
critical) with enhanced context including trace IDs, correlation IDs, and
|
36
|
-
baggage propagation.
|
37
|
-
|
38
|
-
Attributes:
|
39
|
-
service_name: Service identifier for telemetry and cloud role name
|
40
|
-
service_version: Service version for context
|
41
|
-
connection_string: Application Insights connection string
|
42
|
-
logger: Python logger instance
|
43
|
-
tracer: OpenTelemetry tracer for spans
|
44
|
-
"""
|
45
|
-
|
46
|
-
def __init__(
|
47
|
-
self,
|
48
|
-
service_name: str,
|
49
|
-
service_version: str = "1.0.0",
|
50
|
-
connection_string: Optional[str] = None,
|
51
|
-
log_level: int = logging.INFO,
|
52
|
-
enable_console_logging: bool = True,
|
53
|
-
custom_resource_attributes: Optional[Dict[str, str]] = None,
|
54
|
-
instrumentation_options: Optional[Dict[str, Any]] = None,
|
55
|
-
cloud_role_name: Optional[str] = None,
|
56
|
-
):
|
57
|
-
"""Initialize Azure Logger with OpenTelemetry tracing.
|
58
|
-
|
59
|
-
The service_name parameter automatically sets the cloud role name for
|
60
|
-
Application Insights Application Map visualization. When multiple services
|
61
|
-
emit telemetry to the same Application Insights resource, each service
|
62
|
-
will appear as a separate node on the Application Map.
|
63
|
-
|
64
|
-
Args:
|
65
|
-
service_name: Service identifier for telemetry and cloud role name
|
66
|
-
service_version: Service version for metadata
|
67
|
-
connection_string: Application Insights connection string
|
68
|
-
log_level: Python logging level (default: INFO)
|
69
|
-
enable_console_logging: Enable console output for local development
|
70
|
-
custom_resource_attributes: Additional OpenTelemetry resource attributes
|
71
|
-
instrumentation_options: Azure Monitor instrumentation options
|
72
|
-
cloud_role_name: Override cloud role name (defaults to service_name)
|
73
|
-
"""
|
74
|
-
self.service_name = service_name
|
75
|
-
self.service_version = service_version
|
76
|
-
self.connection_string = connection_string or os.getenv(
|
77
|
-
"APPLICATIONINSIGHTS_CONNECTION_STRING"
|
78
|
-
)
|
79
|
-
|
80
|
-
# Use explicit cloud role name or default to service name
|
81
|
-
effective_cloud_role_name = cloud_role_name or service_name
|
82
|
-
self.cloud_role_name = effective_cloud_role_name
|
83
|
-
|
84
|
-
# Configure resource attributes
|
85
|
-
# NOTE: service.name automatically maps to cloud role name in Application Insights
|
86
|
-
resource_attributes = {
|
87
|
-
"service.name": effective_cloud_role_name,
|
88
|
-
"service.version": service_version,
|
89
|
-
"service.instance.id": os.getenv("WEBSITE_INSTANCE_ID", "local"),
|
90
|
-
}
|
91
|
-
|
92
|
-
if custom_resource_attributes:
|
93
|
-
resource_attributes.update(custom_resource_attributes)
|
94
|
-
|
95
|
-
# Configure Azure Monitor if connection string available
|
96
|
-
if self.connection_string:
|
97
|
-
try:
|
98
|
-
configure_azure_monitor(
|
99
|
-
connection_string=self.connection_string,
|
100
|
-
resource_attributes=resource_attributes,
|
101
|
-
enable_live_metrics=True,
|
102
|
-
instrumentation_options=instrumentation_options,
|
103
|
-
)
|
104
|
-
self._telemetry_enabled = True
|
105
|
-
except Exception as e:
|
106
|
-
print(f"Warning: Failed to configure Azure Monitor: {e}")
|
107
|
-
self._telemetry_enabled = False
|
108
|
-
else:
|
109
|
-
self._telemetry_enabled = False
|
110
|
-
print(
|
111
|
-
"Warning: No Application Insights connection string found. Telemetry disabled."
|
112
|
-
)
|
113
|
-
|
114
|
-
# Configure Python logger
|
115
|
-
self.logger = logging.getLogger(service_name)
|
116
|
-
self.logger.setLevel(log_level)
|
117
|
-
self.logger.handlers.clear()
|
118
|
-
|
119
|
-
if enable_console_logging:
|
120
|
-
self._setup_console_handler()
|
121
|
-
|
122
|
-
# Initialize OpenTelemetry tracer and correlation context
|
123
|
-
self.tracer = trace.get_tracer(__name__)
|
124
|
-
self._correlation_id = None
|
125
|
-
|
126
|
-
self.info(
|
127
|
-
f"Azure Logger initialized for service '{service_name}' v{service_version} "
|
128
|
-
f"(cloud role: '{effective_cloud_role_name}')"
|
129
|
-
)
|
130
|
-
|
131
|
-
def _setup_console_handler(self):
|
132
|
-
"""Configure console handler for local development."""
|
133
|
-
console_handler = logging.StreamHandler()
|
134
|
-
formatter = logging.Formatter(
|
135
|
-
"%(asctime)s - %(name)s - %(levelname)s - %(message)s - %(pathname)s:%(lineno)d"
|
136
|
-
)
|
137
|
-
console_handler.setFormatter(formatter)
|
138
|
-
self.logger.addHandler(console_handler)
|
139
|
-
|
140
|
-
def set_correlation_id(self, correlation_id: str):
|
141
|
-
"""Set correlation ID for request/transaction tracking.
|
142
|
-
|
143
|
-
Manually sets the correlation ID that will be used for all subsequent
|
144
|
-
tracing operations. This value takes precedence over auto-generated
|
145
|
-
correlation IDs in the trace_function decorator.
|
146
|
-
|
147
|
-
Args:
|
148
|
-
correlation_id: Unique identifier for transaction correlation
|
149
|
-
|
150
|
-
Note:
|
151
|
-
If not set manually, the trace_function decorator will automatically
|
152
|
-
generate a UUID4 correlation_id on first use.
|
153
|
-
"""
|
154
|
-
self._correlation_id = correlation_id
|
155
|
-
|
156
|
-
def get_correlation_id(self) -> Optional[str]:
|
157
|
-
"""Get current correlation ID.
|
158
|
-
|
159
|
-
Returns the currently active correlation ID, whether manually set
|
160
|
-
or automatically generated by the trace_function decorator.
|
161
|
-
|
162
|
-
Returns:
|
163
|
-
Current correlation ID if set (manual or auto-generated), otherwise None
|
164
|
-
"""
|
165
|
-
return self._correlation_id
|
166
|
-
|
167
|
-
def set_baggage(self, key: str, value: str) -> Context:
|
168
|
-
"""Set baggage item in OpenTelemetry context.
|
169
|
-
|
170
|
-
Args:
|
171
|
-
key: Baggage key
|
172
|
-
value: Baggage value
|
173
|
-
|
174
|
-
Returns:
|
175
|
-
Updated context with baggage item
|
176
|
-
"""
|
177
|
-
return baggage.set_baggage(key, value)
|
178
|
-
|
179
|
-
def get_baggage(self, key: str) -> Optional[str]:
|
180
|
-
"""Get baggage item from current context.
|
181
|
-
|
182
|
-
Args:
|
183
|
-
key: Baggage key
|
184
|
-
|
185
|
-
Returns:
|
186
|
-
Baggage value if exists, otherwise None
|
187
|
-
"""
|
188
|
-
return baggage.get_baggage(key)
|
189
|
-
|
190
|
-
def get_all_baggage(self) -> Dict[str, str]:
|
191
|
-
"""Get all baggage items from current context.
|
192
|
-
|
193
|
-
Returns:
|
194
|
-
Dictionary of all baggage items
|
195
|
-
"""
|
196
|
-
return dict(baggage.get_all())
|
197
|
-
|
198
|
-
def _enhance_extra(self, extra: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
199
|
-
"""Enrich log records with contextual information.
|
200
|
-
|
201
|
-
Args:
|
202
|
-
extra: Optional custom data dictionary
|
203
|
-
|
204
|
-
Returns:
|
205
|
-
Enhanced dictionary with service context, correlation ID, trace
|
206
|
-
context, and baggage items, with built-in LogRecord attributes filtered out
|
207
|
-
"""
|
208
|
-
# Define built-in LogRecord attributes that should not be overwritten
|
209
|
-
reserved_attributes = {
|
210
|
-
'name', 'msg', 'args', 'levelname', 'levelno', 'pathname', 'filename',
|
211
|
-
'module', 'exc_info', 'exc_text', 'stack_info', 'lineno', 'funcName',
|
212
|
-
'created', 'msecs', 'relativeCreated', 'thread', 'threadName',
|
213
|
-
'processName', 'process', 'getMessage'
|
214
|
-
}
|
215
|
-
|
216
|
-
enhanced_extra = {
|
217
|
-
"service_name": self.service_name,
|
218
|
-
"service_version": self.service_version,
|
219
|
-
"timestamp": datetime.utcnow().isoformat(),
|
220
|
-
}
|
221
|
-
|
222
|
-
if self._correlation_id:
|
223
|
-
enhanced_extra["correlation_id"] = self._correlation_id
|
224
|
-
|
225
|
-
# Add span context if available
|
226
|
-
current_span = trace.get_current_span()
|
227
|
-
if current_span and current_span.is_recording():
|
228
|
-
span_context = current_span.get_span_context()
|
229
|
-
enhanced_extra["trace_id"] = format(span_context.trace_id, "032x")
|
230
|
-
enhanced_extra["span_id"] = format(span_context.span_id, "016x")
|
231
|
-
|
232
|
-
# Add baggage items, filtering out any reserved attribute names
|
233
|
-
baggage_items = self.get_all_baggage()
|
234
|
-
if baggage_items:
|
235
|
-
# Filter baggage items to avoid conflicts with LogRecord attributes
|
236
|
-
filtered_baggage = {k: v for k, v in baggage_items.items() if k not in reserved_attributes}
|
237
|
-
if filtered_baggage:
|
238
|
-
enhanced_extra["baggage"] = filtered_baggage
|
239
|
-
|
240
|
-
# Log warning if any baggage keys were filtered out
|
241
|
-
filtered_baggage_keys = set(baggage_items.keys()) - set(filtered_baggage.keys())
|
242
|
-
if filtered_baggage_keys:
|
243
|
-
# Use the base logger directly to avoid infinite recursion
|
244
|
-
self.logger.warning(
|
245
|
-
f"Filtered out reserved LogRecord attributes from baggage: {filtered_baggage_keys}"
|
246
|
-
)
|
247
|
-
|
248
|
-
if isinstance(extra, dict):
|
249
|
-
# Filter out any keys that would conflict with built-in LogRecord attributes
|
250
|
-
filtered_extra = {k: v for k, v in extra.items() if k not in reserved_attributes}
|
251
|
-
enhanced_extra.update(filtered_extra)
|
252
|
-
|
253
|
-
# Log warning if any keys were filtered out
|
254
|
-
filtered_keys = set(extra.keys()) - set(filtered_extra.keys())
|
255
|
-
if filtered_keys:
|
256
|
-
# Use the base logger directly to avoid infinite recursion
|
257
|
-
self.logger.warning(
|
258
|
-
f"Filtered out reserved LogRecord attributes from extra data: {filtered_keys}"
|
259
|
-
)
|
260
|
-
|
261
|
-
return enhanced_extra
|
262
|
-
|
263
|
-
def debug(self, message: str, extra: Optional[Dict[str, Any]] = None):
|
264
|
-
"""Log debug message with enhanced context."""
|
265
|
-
self.logger.debug(message, extra=self._enhance_extra(extra))
|
266
|
-
|
267
|
-
def info(self, message: str, extra: Optional[Dict[str, Any]] = None):
|
268
|
-
"""Log info message with enhanced context."""
|
269
|
-
self.logger.info(message, extra=self._enhance_extra(extra))
|
270
|
-
|
271
|
-
def warning(self, message: str, extra: Optional[Dict[str, Any]] = None):
|
272
|
-
"""Log warning message with enhanced context."""
|
273
|
-
self.logger.warning(message, extra=self._enhance_extra(extra))
|
274
|
-
|
275
|
-
def error(
|
276
|
-
self,
|
277
|
-
message: str,
|
278
|
-
extra: Optional[Dict[str, Any]] = None,
|
279
|
-
exc_info: bool = True,
|
280
|
-
):
|
281
|
-
"""Log error message with enhanced context and exception info."""
|
282
|
-
self.logger.error(message, extra=self._enhance_extra(extra), exc_info=exc_info)
|
283
|
-
|
284
|
-
def exception(self, message: str, extra: Optional[Dict[str, Any]] = None):
|
285
|
-
"""Log exception message with enhanced context and automatic exception info.
|
286
|
-
|
287
|
-
This method is a convenience method equivalent to calling error() with
|
288
|
-
exc_info=True. It should typically be called only from exception handlers.
|
289
|
-
|
290
|
-
Args:
|
291
|
-
message: Exception message to log
|
292
|
-
extra: Additional custom properties
|
293
|
-
"""
|
294
|
-
self.logger.error(message, extra=self._enhance_extra(extra), exc_info=True)
|
295
|
-
|
296
|
-
def critical(self, message: str, extra: Optional[Dict[str, Any]] = None):
|
297
|
-
"""Log critical message with enhanced context."""
|
298
|
-
self.logger.critical(message, extra=self._enhance_extra(extra))
|
299
|
-
|
300
|
-
def log_function_execution(
|
301
|
-
self,
|
302
|
-
function_name: str,
|
303
|
-
duration_ms: float,
|
304
|
-
success: bool = True,
|
305
|
-
extra: Optional[Dict[str, Any]] = None,
|
306
|
-
):
|
307
|
-
"""Log function execution metrics for performance monitoring.
|
308
|
-
|
309
|
-
Args:
|
310
|
-
function_name: Name of executed function
|
311
|
-
duration_ms: Execution duration in milliseconds
|
312
|
-
success: Whether function executed successfully
|
313
|
-
extra: Additional custom properties
|
314
|
-
"""
|
315
|
-
log_data = {
|
316
|
-
"function_name": function_name,
|
317
|
-
"duration_ms": duration_ms,
|
318
|
-
"success": success,
|
319
|
-
"performance_category": "function_execution",
|
320
|
-
}
|
321
|
-
|
322
|
-
if extra:
|
323
|
-
log_data.update(extra)
|
324
|
-
|
325
|
-
message = f"Function '{function_name}' executed in {duration_ms:.2f}ms - {'SUCCESS' if success else 'FAILED'}"
|
326
|
-
|
327
|
-
if success:
|
328
|
-
self.info(message, extra=log_data)
|
329
|
-
else:
|
330
|
-
self.error(message, extra=log_data, exc_info=False)
|
331
|
-
|
332
|
-
def log_request(
|
333
|
-
self,
|
334
|
-
method: str,
|
335
|
-
url: str,
|
336
|
-
status_code: int,
|
337
|
-
duration_ms: float,
|
338
|
-
extra: Optional[Dict[str, Any]] = None,
|
339
|
-
):
|
340
|
-
"""Log HTTP request with comprehensive details.
|
341
|
-
|
342
|
-
Args:
|
343
|
-
method: HTTP method (GET, POST, etc.)
|
344
|
-
url: Request URL
|
345
|
-
status_code: HTTP response status code
|
346
|
-
duration_ms: Request duration in milliseconds
|
347
|
-
extra: Additional custom properties
|
348
|
-
"""
|
349
|
-
log_data = {
|
350
|
-
"http_method": method,
|
351
|
-
"http_url": str(url),
|
352
|
-
"http_status_code": status_code,
|
353
|
-
"duration_ms": duration_ms,
|
354
|
-
"request_category": "http_request",
|
355
|
-
}
|
356
|
-
|
357
|
-
if extra:
|
358
|
-
log_data.update(extra)
|
359
|
-
|
360
|
-
# Determine log level and status based on status code
|
361
|
-
if status_code < 400:
|
362
|
-
log_level = logging.INFO
|
363
|
-
status_text = "SUCCESS"
|
364
|
-
elif status_code < 500:
|
365
|
-
log_level = logging.WARNING
|
366
|
-
status_text = "CLIENT_ERROR"
|
367
|
-
else:
|
368
|
-
log_level = logging.ERROR
|
369
|
-
status_text = "SERVER_ERROR"
|
370
|
-
|
371
|
-
message = (
|
372
|
-
f"{method} {url} - {status_code} - {duration_ms:.2f}ms - {status_text}"
|
373
|
-
)
|
374
|
-
self.logger.log(log_level, message, extra=self._enhance_extra(log_data))
|
375
|
-
|
376
|
-
def create_span(
|
377
|
-
self,
|
378
|
-
span_name: str,
|
379
|
-
attributes: Optional[Dict[str, Union[str, int, float, bool]]] = None,
|
380
|
-
) -> Span:
|
381
|
-
"""Create OpenTelemetry span for distributed tracing.
|
382
|
-
|
383
|
-
Args:
|
384
|
-
span_name: Name for the span
|
385
|
-
attributes: Initial span attributes
|
386
|
-
|
387
|
-
Returns:
|
388
|
-
OpenTelemetry span context manager
|
389
|
-
"""
|
390
|
-
span = self.tracer.start_span(span_name)
|
391
|
-
|
392
|
-
# Add default service attributes
|
393
|
-
span.set_attribute("service.name", self.service_name)
|
394
|
-
span.set_attribute("service.version", self.service_version)
|
395
|
-
|
396
|
-
if self._correlation_id:
|
397
|
-
span.set_attribute("correlation.id", self._correlation_id)
|
398
|
-
|
399
|
-
# Add custom attributes
|
400
|
-
if attributes:
|
401
|
-
for key, value in attributes.items():
|
402
|
-
span.set_attribute(key, value)
|
403
|
-
|
404
|
-
return span
|
405
|
-
|
406
|
-
def _setup_span_for_function_trace(
|
407
|
-
self,
|
408
|
-
span: Span,
|
409
|
-
func: Callable,
|
410
|
-
is_async: bool,
|
411
|
-
log_args: bool,
|
412
|
-
args: tuple,
|
413
|
-
kwargs: dict,
|
414
|
-
log_result: bool,
|
415
|
-
log_execution: bool,
|
416
|
-
):
|
417
|
-
"""Configure span attributes for function tracing."""
|
418
|
-
span.set_attribute("function.name", func.__name__)
|
419
|
-
span.set_attribute("function.module", func.__module__)
|
420
|
-
span.set_attribute("service.name", self.service_name)
|
421
|
-
span.set_attribute("function.is_async", is_async)
|
422
|
-
|
423
|
-
# Add decorator parameters as span attributes
|
424
|
-
span.set_attribute("function.decorator.log_args", log_args)
|
425
|
-
span.set_attribute("function.decorator.log_result", log_result)
|
426
|
-
span.set_attribute("function.decorator.log_execution", log_execution)
|
427
|
-
|
428
|
-
if self._correlation_id:
|
429
|
-
span.set_attribute("correlation.id", self._correlation_id)
|
430
|
-
|
431
|
-
if log_args:
|
432
|
-
if args:
|
433
|
-
span.set_attribute("function.args_count", len(args))
|
434
|
-
# Add positional arguments as span attributes
|
435
|
-
import inspect
|
436
|
-
try:
|
437
|
-
sig = inspect.signature(func)
|
438
|
-
param_names = list(sig.parameters.keys())
|
439
|
-
for i, arg_value in enumerate(args):
|
440
|
-
param_name = param_names[i] if i < len(param_names) else f"arg_{i}"
|
441
|
-
try:
|
442
|
-
# Convert to string for safe serialization
|
443
|
-
attr_value = str(arg_value)
|
444
|
-
# Truncate if too long to avoid excessive data
|
445
|
-
if len(attr_value) > 1000:
|
446
|
-
attr_value = attr_value[:1000] + "..."
|
447
|
-
span.set_attribute(f"function.arg.{param_name}", attr_value)
|
448
|
-
except Exception:
|
449
|
-
span.set_attribute(f"function.arg.{param_name}", "<non-serializable>")
|
450
|
-
except Exception:
|
451
|
-
# Fallback if signature inspection fails
|
452
|
-
for i, arg_value in enumerate(args):
|
453
|
-
try:
|
454
|
-
attr_value = str(arg_value)
|
455
|
-
if len(attr_value) > 1000:
|
456
|
-
attr_value = attr_value[:1000] + "..."
|
457
|
-
span.set_attribute(f"function.arg.{i}", attr_value)
|
458
|
-
except Exception:
|
459
|
-
span.set_attribute(f"function.arg.{i}", "<non-serializable>")
|
460
|
-
|
461
|
-
if kwargs:
|
462
|
-
span.set_attribute("function.kwargs_count", len(kwargs))
|
463
|
-
# Add keyword arguments as span attributes
|
464
|
-
for key, value in kwargs.items():
|
465
|
-
try:
|
466
|
-
attr_value = str(value)
|
467
|
-
# Truncate if too long to avoid excessive data
|
468
|
-
if len(attr_value) > 1000:
|
469
|
-
attr_value = attr_value[:1000] + "..."
|
470
|
-
span.set_attribute(f"function.kwarg.{key}", attr_value)
|
471
|
-
except Exception:
|
472
|
-
span.set_attribute(f"function.kwarg.{key}", "<non-serializable>")
|
473
|
-
|
474
|
-
def _handle_function_success(
|
475
|
-
self,
|
476
|
-
span: Span,
|
477
|
-
func: Callable,
|
478
|
-
duration_ms: float,
|
479
|
-
result: Any,
|
480
|
-
log_result: bool,
|
481
|
-
log_execution: bool,
|
482
|
-
is_async: bool,
|
483
|
-
args: tuple,
|
484
|
-
kwargs: dict,
|
485
|
-
):
|
486
|
-
"""Handle successful function execution in tracing."""
|
487
|
-
span.set_attribute("function.duration_ms", duration_ms)
|
488
|
-
span.set_attribute("function.success", True)
|
489
|
-
span.set_status(Status(StatusCode.OK))
|
490
|
-
|
491
|
-
if log_result and result is not None:
|
492
|
-
span.set_attribute("function.has_result", True)
|
493
|
-
span.set_attribute("function.result_type", type(result).__name__)
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
span
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
}
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
log_execution:
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
return
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
|
712
|
-
|
713
|
-
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
|
728
|
-
|
729
|
-
|
730
|
-
|
731
|
-
|
732
|
-
|
733
|
-
|
734
|
-
|
735
|
-
|
736
|
-
|
737
|
-
|
738
|
-
|
739
|
-
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
|
760
|
-
|
761
|
-
|
762
|
-
|
763
|
-
|
764
|
-
|
765
|
-
|
766
|
-
|
767
|
-
|
768
|
-
|
769
|
-
|
770
|
-
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
|
779
|
-
|
780
|
-
|
781
|
-
|
782
|
-
|
783
|
-
|
784
|
-
|
785
|
-
|
786
|
-
|
787
|
-
log_level
|
788
|
-
|
789
|
-
|
790
|
-
|
791
|
-
|
792
|
-
|
793
|
-
|
794
|
-
|
795
|
-
|
796
|
-
|
797
|
-
|
798
|
-
|
799
|
-
|
800
|
-
|
801
|
-
|
802
|
-
|
803
|
-
|
804
|
-
|
805
|
-
|
806
|
-
|
807
|
-
|
808
|
-
|
809
|
-
|
810
|
-
|
811
|
-
|
812
|
-
|
813
|
-
|
814
|
-
|
815
|
-
|
816
|
-
|
817
|
-
|
818
|
-
|
819
|
-
|
820
|
-
|
821
|
-
|
822
|
-
|
823
|
-
|
824
|
-
|
825
|
-
|
826
|
-
|
827
|
-
|
828
|
-
|
829
|
-
|
830
|
-
|
831
|
-
|
832
|
-
|
833
|
-
|
834
|
-
|
835
|
-
|
836
|
-
|
837
|
-
|
838
|
-
|
839
|
-
|
840
|
-
|
841
|
-
|
842
|
-
|
843
|
-
|
844
|
-
|
845
|
-
|
846
|
-
|
847
|
-
|
848
|
-
|
849
|
-
|
850
|
-
|
851
|
-
|
852
|
-
|
853
|
-
|
854
|
-
|
855
|
-
|
856
|
-
|
857
|
-
|
858
|
-
|
859
|
-
|
860
|
-
|
861
|
-
|
862
|
-
|
863
|
-
|
864
|
-
|
865
|
-
|
866
|
-
|
867
|
-
|
868
|
-
|
869
|
-
|
870
|
-
|
871
|
-
|
872
|
-
|
873
|
-
|
1
|
+
import logging
|
2
|
+
import os
|
3
|
+
import functools
|
4
|
+
import time
|
5
|
+
import asyncio
|
6
|
+
import uuid
|
7
|
+
from typing import Optional, Dict, Any, Union, Callable
|
8
|
+
from datetime import datetime
|
9
|
+
from azure.monitor.opentelemetry import configure_azure_monitor
|
10
|
+
from opentelemetry import trace
|
11
|
+
from opentelemetry.trace import Status, StatusCode, Span
|
12
|
+
from opentelemetry import baggage
|
13
|
+
from opentelemetry.context import Context
|
14
|
+
|
15
|
+
|
16
|
+
class AzureLogger:
|
17
|
+
"""Azure-integrated logger with OpenTelemetry distributed tracing.
|
18
|
+
|
19
|
+
Provides comprehensive logging with Azure Monitor integration, correlation
|
20
|
+
tracking, baggage propagation, and automated function tracing for Azure
|
21
|
+
applications with seamless local development support.
|
22
|
+
|
23
|
+
CLOUD ROLE NAME INTEGRATION:
|
24
|
+
The service_name parameter automatically sets the cloud role name for
|
25
|
+
Application Insights. When multiple services emit telemetry to the same
|
26
|
+
Application Insights resource, each service will appear as a separate
|
27
|
+
node on the Application Map, enabling proper service topology visualization.
|
28
|
+
|
29
|
+
CORRELATION ID AUTOMATION:
|
30
|
+
The trace_function decorator automatically generates UUID4 correlation IDs
|
31
|
+
when none are manually set, ensuring consistent distributed tracing across
|
32
|
+
all function calls without requiring manual configuration.
|
33
|
+
|
34
|
+
Supports all standard logging levels (debug, info, warning, error, exception,
|
35
|
+
critical) with enhanced context including trace IDs, correlation IDs, and
|
36
|
+
baggage propagation.
|
37
|
+
|
38
|
+
Attributes:
|
39
|
+
service_name: Service identifier for telemetry and cloud role name
|
40
|
+
service_version: Service version for context
|
41
|
+
connection_string: Application Insights connection string
|
42
|
+
logger: Python logger instance
|
43
|
+
tracer: OpenTelemetry tracer for spans
|
44
|
+
"""
|
45
|
+
|
46
|
+
def __init__(
|
47
|
+
self,
|
48
|
+
service_name: str,
|
49
|
+
service_version: str = "1.0.0",
|
50
|
+
connection_string: Optional[str] = None,
|
51
|
+
log_level: int = logging.INFO,
|
52
|
+
enable_console_logging: bool = True,
|
53
|
+
custom_resource_attributes: Optional[Dict[str, str]] = None,
|
54
|
+
instrumentation_options: Optional[Dict[str, Any]] = None,
|
55
|
+
cloud_role_name: Optional[str] = None,
|
56
|
+
):
|
57
|
+
"""Initialize Azure Logger with OpenTelemetry tracing.
|
58
|
+
|
59
|
+
The service_name parameter automatically sets the cloud role name for
|
60
|
+
Application Insights Application Map visualization. When multiple services
|
61
|
+
emit telemetry to the same Application Insights resource, each service
|
62
|
+
will appear as a separate node on the Application Map.
|
63
|
+
|
64
|
+
Args:
|
65
|
+
service_name: Service identifier for telemetry and cloud role name
|
66
|
+
service_version: Service version for metadata
|
67
|
+
connection_string: Application Insights connection string
|
68
|
+
log_level: Python logging level (default: INFO)
|
69
|
+
enable_console_logging: Enable console output for local development
|
70
|
+
custom_resource_attributes: Additional OpenTelemetry resource attributes
|
71
|
+
instrumentation_options: Azure Monitor instrumentation options
|
72
|
+
cloud_role_name: Override cloud role name (defaults to service_name)
|
73
|
+
"""
|
74
|
+
self.service_name = service_name
|
75
|
+
self.service_version = service_version
|
76
|
+
self.connection_string = connection_string or os.getenv(
|
77
|
+
"APPLICATIONINSIGHTS_CONNECTION_STRING"
|
78
|
+
)
|
79
|
+
|
80
|
+
# Use explicit cloud role name or default to service name
|
81
|
+
effective_cloud_role_name = cloud_role_name or service_name
|
82
|
+
self.cloud_role_name = effective_cloud_role_name
|
83
|
+
|
84
|
+
# Configure resource attributes
|
85
|
+
# NOTE: service.name automatically maps to cloud role name in Application Insights
|
86
|
+
resource_attributes = {
|
87
|
+
"service.name": effective_cloud_role_name,
|
88
|
+
"service.version": service_version,
|
89
|
+
"service.instance.id": os.getenv("WEBSITE_INSTANCE_ID", "local"),
|
90
|
+
}
|
91
|
+
|
92
|
+
if custom_resource_attributes:
|
93
|
+
resource_attributes.update(custom_resource_attributes)
|
94
|
+
|
95
|
+
# Configure Azure Monitor if connection string available
|
96
|
+
if self.connection_string:
|
97
|
+
try:
|
98
|
+
configure_azure_monitor(
|
99
|
+
connection_string=self.connection_string,
|
100
|
+
resource_attributes=resource_attributes,
|
101
|
+
enable_live_metrics=True,
|
102
|
+
instrumentation_options=instrumentation_options,
|
103
|
+
)
|
104
|
+
self._telemetry_enabled = True
|
105
|
+
except Exception as e:
|
106
|
+
print(f"Warning: Failed to configure Azure Monitor: {e}")
|
107
|
+
self._telemetry_enabled = False
|
108
|
+
else:
|
109
|
+
self._telemetry_enabled = False
|
110
|
+
print(
|
111
|
+
"Warning: No Application Insights connection string found. Telemetry disabled."
|
112
|
+
)
|
113
|
+
|
114
|
+
# Configure Python logger
|
115
|
+
self.logger = logging.getLogger(service_name)
|
116
|
+
self.logger.setLevel(log_level)
|
117
|
+
self.logger.handlers.clear()
|
118
|
+
|
119
|
+
if enable_console_logging:
|
120
|
+
self._setup_console_handler()
|
121
|
+
|
122
|
+
# Initialize OpenTelemetry tracer and correlation context
|
123
|
+
self.tracer = trace.get_tracer(__name__)
|
124
|
+
self._correlation_id = None
|
125
|
+
|
126
|
+
self.info(
|
127
|
+
f"Azure Logger initialized for service '{service_name}' v{service_version} "
|
128
|
+
f"(cloud role: '{effective_cloud_role_name}')"
|
129
|
+
)
|
130
|
+
|
131
|
+
def _setup_console_handler(self):
|
132
|
+
"""Configure console handler for local development."""
|
133
|
+
console_handler = logging.StreamHandler()
|
134
|
+
formatter = logging.Formatter(
|
135
|
+
"%(asctime)s - %(name)s - %(levelname)s - %(message)s - %(pathname)s:%(lineno)d"
|
136
|
+
)
|
137
|
+
console_handler.setFormatter(formatter)
|
138
|
+
self.logger.addHandler(console_handler)
|
139
|
+
|
140
|
+
def set_correlation_id(self, correlation_id: str):
|
141
|
+
"""Set correlation ID for request/transaction tracking.
|
142
|
+
|
143
|
+
Manually sets the correlation ID that will be used for all subsequent
|
144
|
+
tracing operations. This value takes precedence over auto-generated
|
145
|
+
correlation IDs in the trace_function decorator.
|
146
|
+
|
147
|
+
Args:
|
148
|
+
correlation_id: Unique identifier for transaction correlation
|
149
|
+
|
150
|
+
Note:
|
151
|
+
If not set manually, the trace_function decorator will automatically
|
152
|
+
generate a UUID4 correlation_id on first use.
|
153
|
+
"""
|
154
|
+
self._correlation_id = correlation_id
|
155
|
+
|
156
|
+
def get_correlation_id(self) -> Optional[str]:
|
157
|
+
"""Get current correlation ID.
|
158
|
+
|
159
|
+
Returns the currently active correlation ID, whether manually set
|
160
|
+
or automatically generated by the trace_function decorator.
|
161
|
+
|
162
|
+
Returns:
|
163
|
+
Current correlation ID if set (manual or auto-generated), otherwise None
|
164
|
+
"""
|
165
|
+
return self._correlation_id
|
166
|
+
|
167
|
+
def set_baggage(self, key: str, value: str) -> Context:
|
168
|
+
"""Set baggage item in OpenTelemetry context.
|
169
|
+
|
170
|
+
Args:
|
171
|
+
key: Baggage key
|
172
|
+
value: Baggage value
|
173
|
+
|
174
|
+
Returns:
|
175
|
+
Updated context with baggage item
|
176
|
+
"""
|
177
|
+
return baggage.set_baggage(key, value)
|
178
|
+
|
179
|
+
def get_baggage(self, key: str) -> Optional[str]:
|
180
|
+
"""Get baggage item from current context.
|
181
|
+
|
182
|
+
Args:
|
183
|
+
key: Baggage key
|
184
|
+
|
185
|
+
Returns:
|
186
|
+
Baggage value if exists, otherwise None
|
187
|
+
"""
|
188
|
+
return baggage.get_baggage(key)
|
189
|
+
|
190
|
+
def get_all_baggage(self) -> Dict[str, str]:
|
191
|
+
"""Get all baggage items from current context.
|
192
|
+
|
193
|
+
Returns:
|
194
|
+
Dictionary of all baggage items
|
195
|
+
"""
|
196
|
+
return dict(baggage.get_all())
|
197
|
+
|
198
|
+
def _enhance_extra(self, extra: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
199
|
+
"""Enrich log records with contextual information.
|
200
|
+
|
201
|
+
Args:
|
202
|
+
extra: Optional custom data dictionary
|
203
|
+
|
204
|
+
Returns:
|
205
|
+
Enhanced dictionary with service context, correlation ID, trace
|
206
|
+
context, and baggage items, with built-in LogRecord attributes filtered out
|
207
|
+
"""
|
208
|
+
# Define built-in LogRecord attributes that should not be overwritten
|
209
|
+
reserved_attributes = {
|
210
|
+
'name', 'msg', 'args', 'levelname', 'levelno', 'pathname', 'filename',
|
211
|
+
'module', 'exc_info', 'exc_text', 'stack_info', 'lineno', 'funcName',
|
212
|
+
'created', 'msecs', 'relativeCreated', 'thread', 'threadName',
|
213
|
+
'processName', 'process', 'getMessage'
|
214
|
+
}
|
215
|
+
|
216
|
+
enhanced_extra = {
|
217
|
+
"service_name": self.service_name,
|
218
|
+
"service_version": self.service_version,
|
219
|
+
"timestamp": datetime.utcnow().isoformat(),
|
220
|
+
}
|
221
|
+
|
222
|
+
if self._correlation_id:
|
223
|
+
enhanced_extra["correlation_id"] = self._correlation_id
|
224
|
+
|
225
|
+
# Add span context if available
|
226
|
+
current_span = trace.get_current_span()
|
227
|
+
if current_span and current_span.is_recording():
|
228
|
+
span_context = current_span.get_span_context()
|
229
|
+
enhanced_extra["trace_id"] = format(span_context.trace_id, "032x")
|
230
|
+
enhanced_extra["span_id"] = format(span_context.span_id, "016x")
|
231
|
+
|
232
|
+
# Add baggage items, filtering out any reserved attribute names
|
233
|
+
baggage_items = self.get_all_baggage()
|
234
|
+
if baggage_items:
|
235
|
+
# Filter baggage items to avoid conflicts with LogRecord attributes
|
236
|
+
filtered_baggage = {k: v for k, v in baggage_items.items() if k not in reserved_attributes}
|
237
|
+
if filtered_baggage:
|
238
|
+
enhanced_extra["baggage"] = filtered_baggage
|
239
|
+
|
240
|
+
# Log warning if any baggage keys were filtered out
|
241
|
+
filtered_baggage_keys = set(baggage_items.keys()) - set(filtered_baggage.keys())
|
242
|
+
if filtered_baggage_keys:
|
243
|
+
# Use the base logger directly to avoid infinite recursion
|
244
|
+
self.logger.warning(
|
245
|
+
f"Filtered out reserved LogRecord attributes from baggage: {filtered_baggage_keys}"
|
246
|
+
)
|
247
|
+
|
248
|
+
if isinstance(extra, dict):
|
249
|
+
# Filter out any keys that would conflict with built-in LogRecord attributes
|
250
|
+
filtered_extra = {k: v for k, v in extra.items() if k not in reserved_attributes}
|
251
|
+
enhanced_extra.update(filtered_extra)
|
252
|
+
|
253
|
+
# Log warning if any keys were filtered out
|
254
|
+
filtered_keys = set(extra.keys()) - set(filtered_extra.keys())
|
255
|
+
if filtered_keys:
|
256
|
+
# Use the base logger directly to avoid infinite recursion
|
257
|
+
self.logger.warning(
|
258
|
+
f"Filtered out reserved LogRecord attributes from extra data: {filtered_keys}"
|
259
|
+
)
|
260
|
+
|
261
|
+
return enhanced_extra
|
262
|
+
|
263
|
+
def debug(self, message: str, extra: Optional[Dict[str, Any]] = None):
|
264
|
+
"""Log debug message with enhanced context."""
|
265
|
+
self.logger.debug(message, extra=self._enhance_extra(extra))
|
266
|
+
|
267
|
+
def info(self, message: str, extra: Optional[Dict[str, Any]] = None):
|
268
|
+
"""Log info message with enhanced context."""
|
269
|
+
self.logger.info(message, extra=self._enhance_extra(extra))
|
270
|
+
|
271
|
+
def warning(self, message: str, extra: Optional[Dict[str, Any]] = None):
|
272
|
+
"""Log warning message with enhanced context."""
|
273
|
+
self.logger.warning(message, extra=self._enhance_extra(extra))
|
274
|
+
|
275
|
+
def error(
|
276
|
+
self,
|
277
|
+
message: str,
|
278
|
+
extra: Optional[Dict[str, Any]] = None,
|
279
|
+
exc_info: bool = True,
|
280
|
+
):
|
281
|
+
"""Log error message with enhanced context and exception info."""
|
282
|
+
self.logger.error(message, extra=self._enhance_extra(extra), exc_info=exc_info)
|
283
|
+
|
284
|
+
def exception(self, message: str, extra: Optional[Dict[str, Any]] = None):
|
285
|
+
"""Log exception message with enhanced context and automatic exception info.
|
286
|
+
|
287
|
+
This method is a convenience method equivalent to calling error() with
|
288
|
+
exc_info=True. It should typically be called only from exception handlers.
|
289
|
+
|
290
|
+
Args:
|
291
|
+
message: Exception message to log
|
292
|
+
extra: Additional custom properties
|
293
|
+
"""
|
294
|
+
self.logger.error(message, extra=self._enhance_extra(extra), exc_info=True)
|
295
|
+
|
296
|
+
def critical(self, message: str, extra: Optional[Dict[str, Any]] = None):
|
297
|
+
"""Log critical message with enhanced context."""
|
298
|
+
self.logger.critical(message, extra=self._enhance_extra(extra))
|
299
|
+
|
300
|
+
def log_function_execution(
|
301
|
+
self,
|
302
|
+
function_name: str,
|
303
|
+
duration_ms: float,
|
304
|
+
success: bool = True,
|
305
|
+
extra: Optional[Dict[str, Any]] = None,
|
306
|
+
):
|
307
|
+
"""Log function execution metrics for performance monitoring.
|
308
|
+
|
309
|
+
Args:
|
310
|
+
function_name: Name of executed function
|
311
|
+
duration_ms: Execution duration in milliseconds
|
312
|
+
success: Whether function executed successfully
|
313
|
+
extra: Additional custom properties
|
314
|
+
"""
|
315
|
+
log_data = {
|
316
|
+
"function_name": function_name,
|
317
|
+
"duration_ms": duration_ms,
|
318
|
+
"success": success,
|
319
|
+
"performance_category": "function_execution",
|
320
|
+
}
|
321
|
+
|
322
|
+
if extra:
|
323
|
+
log_data.update(extra)
|
324
|
+
|
325
|
+
message = f"Function '{function_name}' executed in {duration_ms:.2f}ms - {'SUCCESS' if success else 'FAILED'}"
|
326
|
+
|
327
|
+
if success:
|
328
|
+
self.info(message, extra=log_data)
|
329
|
+
else:
|
330
|
+
self.error(message, extra=log_data, exc_info=False)
|
331
|
+
|
332
|
+
def log_request(
|
333
|
+
self,
|
334
|
+
method: str,
|
335
|
+
url: str,
|
336
|
+
status_code: int,
|
337
|
+
duration_ms: float,
|
338
|
+
extra: Optional[Dict[str, Any]] = None,
|
339
|
+
):
|
340
|
+
"""Log HTTP request with comprehensive details.
|
341
|
+
|
342
|
+
Args:
|
343
|
+
method: HTTP method (GET, POST, etc.)
|
344
|
+
url: Request URL
|
345
|
+
status_code: HTTP response status code
|
346
|
+
duration_ms: Request duration in milliseconds
|
347
|
+
extra: Additional custom properties
|
348
|
+
"""
|
349
|
+
log_data = {
|
350
|
+
"http_method": method,
|
351
|
+
"http_url": str(url),
|
352
|
+
"http_status_code": status_code,
|
353
|
+
"duration_ms": duration_ms,
|
354
|
+
"request_category": "http_request",
|
355
|
+
}
|
356
|
+
|
357
|
+
if extra:
|
358
|
+
log_data.update(extra)
|
359
|
+
|
360
|
+
# Determine log level and status based on status code
|
361
|
+
if status_code < 400:
|
362
|
+
log_level = logging.INFO
|
363
|
+
status_text = "SUCCESS"
|
364
|
+
elif status_code < 500:
|
365
|
+
log_level = logging.WARNING
|
366
|
+
status_text = "CLIENT_ERROR"
|
367
|
+
else:
|
368
|
+
log_level = logging.ERROR
|
369
|
+
status_text = "SERVER_ERROR"
|
370
|
+
|
371
|
+
message = (
|
372
|
+
f"{method} {url} - {status_code} - {duration_ms:.2f}ms - {status_text}"
|
373
|
+
)
|
374
|
+
self.logger.log(log_level, message, extra=self._enhance_extra(log_data))
|
375
|
+
|
376
|
+
def create_span(
|
377
|
+
self,
|
378
|
+
span_name: str,
|
379
|
+
attributes: Optional[Dict[str, Union[str, int, float, bool]]] = None,
|
380
|
+
) -> Span:
|
381
|
+
"""Create OpenTelemetry span for distributed tracing.
|
382
|
+
|
383
|
+
Args:
|
384
|
+
span_name: Name for the span
|
385
|
+
attributes: Initial span attributes
|
386
|
+
|
387
|
+
Returns:
|
388
|
+
OpenTelemetry span context manager
|
389
|
+
"""
|
390
|
+
span = self.tracer.start_span(span_name)
|
391
|
+
|
392
|
+
# Add default service attributes
|
393
|
+
span.set_attribute("service.name", self.service_name)
|
394
|
+
span.set_attribute("service.version", self.service_version)
|
395
|
+
|
396
|
+
if self._correlation_id:
|
397
|
+
span.set_attribute("correlation.id", self._correlation_id)
|
398
|
+
|
399
|
+
# Add custom attributes
|
400
|
+
if attributes:
|
401
|
+
for key, value in attributes.items():
|
402
|
+
span.set_attribute(key, value)
|
403
|
+
|
404
|
+
return span
|
405
|
+
|
406
|
+
def _setup_span_for_function_trace(
|
407
|
+
self,
|
408
|
+
span: Span,
|
409
|
+
func: Callable,
|
410
|
+
is_async: bool,
|
411
|
+
log_args: bool,
|
412
|
+
args: tuple,
|
413
|
+
kwargs: dict,
|
414
|
+
log_result: bool,
|
415
|
+
log_execution: bool,
|
416
|
+
):
|
417
|
+
"""Configure span attributes for function tracing."""
|
418
|
+
span.set_attribute("function.name", func.__name__)
|
419
|
+
span.set_attribute("function.module", func.__module__)
|
420
|
+
span.set_attribute("service.name", self.service_name)
|
421
|
+
span.set_attribute("function.is_async", is_async)
|
422
|
+
|
423
|
+
# Add decorator parameters as span attributes
|
424
|
+
span.set_attribute("function.decorator.log_args", log_args)
|
425
|
+
span.set_attribute("function.decorator.log_result", log_result)
|
426
|
+
span.set_attribute("function.decorator.log_execution", log_execution)
|
427
|
+
|
428
|
+
if self._correlation_id:
|
429
|
+
span.set_attribute("correlation.id", self._correlation_id)
|
430
|
+
|
431
|
+
if log_args:
|
432
|
+
if args:
|
433
|
+
span.set_attribute("function.args_count", len(args))
|
434
|
+
# Add positional arguments as span attributes
|
435
|
+
import inspect
|
436
|
+
try:
|
437
|
+
sig = inspect.signature(func)
|
438
|
+
param_names = list(sig.parameters.keys())
|
439
|
+
for i, arg_value in enumerate(args):
|
440
|
+
param_name = param_names[i] if i < len(param_names) else f"arg_{i}"
|
441
|
+
try:
|
442
|
+
# Convert to string for safe serialization
|
443
|
+
attr_value = str(arg_value)
|
444
|
+
# Truncate if too long to avoid excessive data
|
445
|
+
if len(attr_value) > 1000:
|
446
|
+
attr_value = attr_value[:1000] + "..."
|
447
|
+
span.set_attribute(f"function.arg.{param_name}", attr_value)
|
448
|
+
except Exception:
|
449
|
+
span.set_attribute(f"function.arg.{param_name}", "<non-serializable>")
|
450
|
+
except Exception:
|
451
|
+
# Fallback if signature inspection fails
|
452
|
+
for i, arg_value in enumerate(args):
|
453
|
+
try:
|
454
|
+
attr_value = str(arg_value)
|
455
|
+
if len(attr_value) > 1000:
|
456
|
+
attr_value = attr_value[:1000] + "..."
|
457
|
+
span.set_attribute(f"function.arg.{i}", attr_value)
|
458
|
+
except Exception:
|
459
|
+
span.set_attribute(f"function.arg.{i}", "<non-serializable>")
|
460
|
+
|
461
|
+
if kwargs:
|
462
|
+
span.set_attribute("function.kwargs_count", len(kwargs))
|
463
|
+
# Add keyword arguments as span attributes
|
464
|
+
for key, value in kwargs.items():
|
465
|
+
try:
|
466
|
+
attr_value = str(value)
|
467
|
+
# Truncate if too long to avoid excessive data
|
468
|
+
if len(attr_value) > 1000:
|
469
|
+
attr_value = attr_value[:1000] + "..."
|
470
|
+
span.set_attribute(f"function.kwarg.{key}", attr_value)
|
471
|
+
except Exception:
|
472
|
+
span.set_attribute(f"function.kwarg.{key}", "<non-serializable>")
|
473
|
+
|
474
|
+
def _handle_function_success(
|
475
|
+
self,
|
476
|
+
span: Span,
|
477
|
+
func: Callable,
|
478
|
+
duration_ms: float,
|
479
|
+
result: Any,
|
480
|
+
log_result: bool,
|
481
|
+
log_execution: bool,
|
482
|
+
is_async: bool,
|
483
|
+
args: tuple,
|
484
|
+
kwargs: dict,
|
485
|
+
):
|
486
|
+
"""Handle successful function execution in tracing."""
|
487
|
+
span.set_attribute("function.duration_ms", duration_ms)
|
488
|
+
span.set_attribute("function.success", True)
|
489
|
+
span.set_status(Status(StatusCode.OK))
|
490
|
+
|
491
|
+
if log_result and result is not None:
|
492
|
+
span.set_attribute("function.has_result", True)
|
493
|
+
span.set_attribute("function.result_type", type(result).__name__)
|
494
|
+
try:
|
495
|
+
# Convert to string for safe serialization
|
496
|
+
attr_value = str(result)
|
497
|
+
# Truncate if too long to avoid excessive data
|
498
|
+
if len(attr_value) > 1000:
|
499
|
+
attr_value = attr_value[:1000] + "..."
|
500
|
+
span.set_attribute("function.result", attr_value)
|
501
|
+
except Exception:
|
502
|
+
span.set_attribute("function.result", "<non-serializable>")
|
503
|
+
|
504
|
+
if log_execution:
|
505
|
+
self.log_function_execution(
|
506
|
+
func.__name__,
|
507
|
+
duration_ms,
|
508
|
+
True,
|
509
|
+
{
|
510
|
+
"args_count": len(args) if args else 0,
|
511
|
+
"kwargs_count": len(kwargs) if kwargs else 0,
|
512
|
+
"is_async": is_async,
|
513
|
+
},
|
514
|
+
)
|
515
|
+
|
516
|
+
log_prefix = "Async function" if is_async else "Function"
|
517
|
+
self.debug(f"{log_prefix} execution completed: {func.__name__}")
|
518
|
+
|
519
|
+
def _handle_function_exception(
|
520
|
+
self,
|
521
|
+
span: Span,
|
522
|
+
func: Callable,
|
523
|
+
duration_ms: float,
|
524
|
+
e: Exception,
|
525
|
+
log_execution: bool,
|
526
|
+
is_async: bool,
|
527
|
+
):
|
528
|
+
"""Handle failed function execution in tracing."""
|
529
|
+
span.set_status(Status(StatusCode.ERROR, description=str(e)))
|
530
|
+
span.record_exception(e)
|
531
|
+
if log_execution:
|
532
|
+
self.log_function_execution(
|
533
|
+
function_name=func.__name__, duration_ms=duration_ms, success=False
|
534
|
+
)
|
535
|
+
self.error(
|
536
|
+
f"Exception in {'async ' if is_async else ''}"
|
537
|
+
f"function '{func.__name__}': {e}"
|
538
|
+
)
|
539
|
+
|
540
|
+
def trace_function(
|
541
|
+
self,
|
542
|
+
function_name: Optional[str] = None,
|
543
|
+
log_execution: bool = True,
|
544
|
+
log_args: bool = True,
|
545
|
+
log_result: bool = False,
|
546
|
+
) -> Callable:
|
547
|
+
"""Trace function execution with OpenTelemetry.
|
548
|
+
|
549
|
+
This decorator provides a simple way to instrument a function by automatically
|
550
|
+
creating an OpenTelemetry span to trace its execution. It measures execution
|
551
|
+
time, captures arguments, and records the result as attributes on the span.
|
552
|
+
It supports both synchronous and asynchronous functions.
|
553
|
+
|
554
|
+
A correlation ID is automatically managed. If one is not already set on the
|
555
|
+
logger instance, a new UUID4 correlation ID will be generated and used for
|
556
|
+
the function's trace, ensuring that all related logs and traces can be
|
557
|
+
linked together.
|
558
|
+
|
559
|
+
Args:
|
560
|
+
function_name (Optional[str]):
|
561
|
+
Specifies a custom name for the span. If not provided, the name of the
|
562
|
+
decorated function is used. Defaults to None.
|
563
|
+
log_execution (bool):
|
564
|
+
When True, a log message is emitted at the start and end of the function's
|
565
|
+
execution, including the execution duration. This provides a clear
|
566
|
+
record of the function's lifecycle. Defaults to True.
|
567
|
+
log_args (bool):
|
568
|
+
When True, the arguments passed to the function are recorded as
|
569
|
+
attributes on the span. It captures argument names and their string
|
570
|
+
representations, truncating long values to 1000 characters.
|
571
|
+
This is useful for understanding the context of the function call.
|
572
|
+
Sensitive arguments should be handled with care. Defaults to True.
|
573
|
+
log_result (bool):
|
574
|
+
When True, the function's return value is recorded as an attribute
|
575
|
+
on the span. It captures the result's type and its string
|
576
|
+
representation, truncating long values to 1000 characters. This
|
577
|
+
should be used cautiously, as large or sensitive return values may
|
578
|
+
be captured. Defaults to False.
|
579
|
+
|
580
|
+
Returns:
|
581
|
+
Callable: A decorator that can be applied to a function.
|
582
|
+
|
583
|
+
Examples:
|
584
|
+
Basic usage with default settings:
|
585
|
+
Logs execution, arguments, but not the result.
|
586
|
+
|
587
|
+
@logger.trace_function()
|
588
|
+
def sample_function(param1, param2="default"):
|
589
|
+
return "done"
|
590
|
+
|
591
|
+
Customizing the span name:
|
592
|
+
The span will be named "MyCustomTask" instead of "process_data".
|
593
|
+
|
594
|
+
@logger.trace_function(function_name="MyCustomTask")
|
595
|
+
def process_data(data):
|
596
|
+
# processing logic
|
597
|
+
return {"status": "processed"}
|
598
|
+
|
599
|
+
Logging the result:
|
600
|
+
Enable `log_result=True` to capture the function's return value.
|
601
|
+
|
602
|
+
@logger.trace_function(log_result=True)
|
603
|
+
def get_user_id(username):
|
604
|
+
return 12345
|
605
|
+
|
606
|
+
Disabling argument logging for sensitive data:
|
607
|
+
If a function handles sensitive information, disable argument logging.
|
608
|
+
|
609
|
+
@logger.trace_function(log_args=False)
|
610
|
+
def process_payment(credit_card_details):
|
611
|
+
# payment processing logic
|
612
|
+
pass
|
613
|
+
|
614
|
+
Tracing an asynchronous function:
|
615
|
+
The decorator works with async functions without any extra configuration.
|
616
|
+
|
617
|
+
@logger.trace_function(log_result=True)
|
618
|
+
async def fetch_remote_data(url):
|
619
|
+
# async data fetching
|
620
|
+
return await http_get(url)
|
621
|
+
"""
|
622
|
+
|
623
|
+
def decorator(func):
|
624
|
+
# Determine if the function is async at decoration time
|
625
|
+
is_async = asyncio.iscoroutinefunction(func)
|
626
|
+
|
627
|
+
@functools.wraps(func)
|
628
|
+
async def async_wrapper(*args, **kwargs):
|
629
|
+
span_name = function_name or f"{func.__module__}.{func.__name__}"
|
630
|
+
with self.tracer.start_as_current_span(span_name) as span:
|
631
|
+
# Auto-generate correlation_id if not set - ensures consistent tracing
|
632
|
+
# Manual correlation_ids take precedence over auto-generated ones
|
633
|
+
if not self._correlation_id:
|
634
|
+
self._correlation_id = str(uuid.uuid4())
|
635
|
+
|
636
|
+
self._setup_span_for_function_trace(
|
637
|
+
span, func, True, log_args, args, kwargs, log_result, log_execution
|
638
|
+
)
|
639
|
+
start_time = time.time()
|
640
|
+
try:
|
641
|
+
self.debug(
|
642
|
+
f"Starting async function execution: {func.__name__}"
|
643
|
+
)
|
644
|
+
result = await func(*args, **kwargs)
|
645
|
+
duration_ms = (time.time() - start_time) * 1000
|
646
|
+
self._handle_function_success(
|
647
|
+
span,
|
648
|
+
func,
|
649
|
+
duration_ms,
|
650
|
+
result,
|
651
|
+
log_result,
|
652
|
+
log_execution,
|
653
|
+
True,
|
654
|
+
args,
|
655
|
+
kwargs,
|
656
|
+
)
|
657
|
+
return result
|
658
|
+
except Exception as e:
|
659
|
+
duration_ms = (time.time() - start_time) * 1000
|
660
|
+
self._handle_function_exception(
|
661
|
+
span, func, duration_ms, e, log_execution, True
|
662
|
+
)
|
663
|
+
raise
|
664
|
+
|
665
|
+
@functools.wraps(func)
|
666
|
+
def sync_wrapper(*args, **kwargs):
|
667
|
+
span_name = function_name or f"{func.__module__}.{func.__name__}"
|
668
|
+
with self.tracer.start_as_current_span(span_name) as span:
|
669
|
+
# Auto-generate correlation_id if not set - ensures consistent tracing
|
670
|
+
# Manual correlation_ids take precedence over auto-generated ones
|
671
|
+
if not self._correlation_id:
|
672
|
+
self._correlation_id = str(uuid.uuid4())
|
673
|
+
|
674
|
+
self._setup_span_for_function_trace(
|
675
|
+
span, func, False, log_args, args, kwargs, log_result, log_execution
|
676
|
+
)
|
677
|
+
start_time = time.time()
|
678
|
+
try:
|
679
|
+
self.debug(f"Starting function execution: {func.__name__}")
|
680
|
+
result = func(*args, **kwargs)
|
681
|
+
duration_ms = (time.time() - start_time) * 1000
|
682
|
+
self._handle_function_success(
|
683
|
+
span,
|
684
|
+
func,
|
685
|
+
duration_ms,
|
686
|
+
result,
|
687
|
+
log_result,
|
688
|
+
log_execution,
|
689
|
+
False,
|
690
|
+
args,
|
691
|
+
kwargs,
|
692
|
+
)
|
693
|
+
return result
|
694
|
+
except Exception as e:
|
695
|
+
duration_ms = (time.time() - start_time) * 1000
|
696
|
+
self._handle_function_exception(
|
697
|
+
span, func, duration_ms, e, log_execution, False
|
698
|
+
)
|
699
|
+
raise
|
700
|
+
|
701
|
+
# Return appropriate wrapper based on function type
|
702
|
+
if is_async:
|
703
|
+
return async_wrapper
|
704
|
+
return sync_wrapper
|
705
|
+
|
706
|
+
return decorator
|
707
|
+
|
708
|
+
def add_span_attributes(self, attributes: Dict[str, Union[str, int, float, bool]]):
|
709
|
+
"""Add attributes to current active span."""
|
710
|
+
current_span = trace.get_current_span()
|
711
|
+
if current_span and current_span.is_recording():
|
712
|
+
for key, value in attributes.items():
|
713
|
+
current_span.set_attribute(key, value)
|
714
|
+
|
715
|
+
def add_span_event(self, name: str, attributes: Optional[Dict[str, Any]] = None):
|
716
|
+
"""Add event to current active span."""
|
717
|
+
current_span = trace.get_current_span()
|
718
|
+
if current_span and current_span.is_recording():
|
719
|
+
event_attributes = attributes or {}
|
720
|
+
if self._correlation_id:
|
721
|
+
event_attributes["correlation_id"] = self._correlation_id
|
722
|
+
current_span.add_event(name, event_attributes)
|
723
|
+
|
724
|
+
def set_span_status(
|
725
|
+
self, status_code: StatusCode, description: Optional[str] = None
|
726
|
+
):
|
727
|
+
"""Set status of current active span."""
|
728
|
+
current_span = trace.get_current_span()
|
729
|
+
if current_span and current_span.is_recording():
|
730
|
+
current_span.set_status(Status(status_code, description))
|
731
|
+
|
732
|
+
def log_with_span(
|
733
|
+
self,
|
734
|
+
span_name: str,
|
735
|
+
message: str,
|
736
|
+
level: int = logging.INFO,
|
737
|
+
extra: Optional[Dict[str, Any]] = None,
|
738
|
+
span_attributes: Optional[Dict[str, Union[str, int, float, bool]]] = None,
|
739
|
+
):
|
740
|
+
"""Log message within a span context.
|
741
|
+
|
742
|
+
Args:
|
743
|
+
span_name: Name for the span
|
744
|
+
message: Log message
|
745
|
+
level: Python logging level
|
746
|
+
extra: Additional log properties
|
747
|
+
span_attributes: Attributes to add to span
|
748
|
+
"""
|
749
|
+
with self.tracer.start_as_current_span(span_name) as span:
|
750
|
+
if span_attributes:
|
751
|
+
for key, value in span_attributes.items():
|
752
|
+
span.set_attribute(key, value)
|
753
|
+
|
754
|
+
self.logger.log(level, message, extra=self._enhance_extra(extra))
|
755
|
+
|
756
|
+
def log_dependency(
|
757
|
+
self,
|
758
|
+
dependency_type: str,
|
759
|
+
name: str,
|
760
|
+
command: str,
|
761
|
+
success: bool,
|
762
|
+
duration_ms: float,
|
763
|
+
extra: Optional[Dict[str, Any]] = None,
|
764
|
+
):
|
765
|
+
"""Log external dependency calls for monitoring.
|
766
|
+
|
767
|
+
Args:
|
768
|
+
dependency_type: Type of dependency (SQL, HTTP, etc.)
|
769
|
+
name: Dependency identifier
|
770
|
+
command: Command/query executed
|
771
|
+
success: Whether call was successful
|
772
|
+
duration_ms: Call duration in milliseconds
|
773
|
+
extra: Additional properties
|
774
|
+
"""
|
775
|
+
log_data = {
|
776
|
+
"dependency_type": dependency_type,
|
777
|
+
"dependency_name": name,
|
778
|
+
"dependency_command": command,
|
779
|
+
"dependency_success": success,
|
780
|
+
"duration_ms": duration_ms,
|
781
|
+
"category": "dependency_call",
|
782
|
+
}
|
783
|
+
|
784
|
+
if extra:
|
785
|
+
log_data.update(extra)
|
786
|
+
|
787
|
+
log_level = logging.INFO if success else logging.ERROR
|
788
|
+
status = "SUCCESS" if success else "FAILED"
|
789
|
+
message = f"Dependency call: {dependency_type}:{name} - {duration_ms:.2f}ms - {status}"
|
790
|
+
|
791
|
+
self.logger.log(log_level, message, extra=self._enhance_extra(log_data))
|
792
|
+
|
793
|
+
def flush(self):
|
794
|
+
"""Flush pending telemetry data."""
|
795
|
+
if self._telemetry_enabled:
|
796
|
+
try:
|
797
|
+
from opentelemetry.sdk.trace import TracerProvider
|
798
|
+
|
799
|
+
tracer_provider = trace.get_tracer_provider()
|
800
|
+
if hasattr(tracer_provider, "force_flush"):
|
801
|
+
tracer_provider.force_flush(timeout_millis=5000)
|
802
|
+
except Exception as e:
|
803
|
+
self.warning(f"Failed to flush telemetry: {e}")
|
804
|
+
|
805
|
+
|
806
|
+
# Factory functions with logger caching
|
807
|
+
_loggers: Dict[Any, "AzureLogger"] = {}
|
808
|
+
|
809
|
+
|
810
|
+
def create_app_logger(
|
811
|
+
service_name: str,
|
812
|
+
service_version: str = "1.0.0",
|
813
|
+
connection_string: Optional[str] = None,
|
814
|
+
log_level: int = logging.INFO,
|
815
|
+
enable_console_logging: bool = True,
|
816
|
+
custom_resource_attributes: Optional[Dict[str, str]] = None,
|
817
|
+
instrumentation_options: Optional[Dict[str, Any]] = None,
|
818
|
+
cloud_role_name: Optional[str] = None,
|
819
|
+
) -> AzureLogger:
|
820
|
+
"""Create cached AzureLogger instance for applications.
|
821
|
+
|
822
|
+
Returns existing logger if one with same configuration exists.
|
823
|
+
The service_name automatically becomes the cloud role name in Application Insights
|
824
|
+
unless explicitly overridden with cloud_role_name parameter.
|
825
|
+
|
826
|
+
Args:
|
827
|
+
service_name: Service identifier for telemetry and cloud role name
|
828
|
+
service_version: Service version for metadata
|
829
|
+
connection_string: Application Insights connection string
|
830
|
+
log_level: Python logging level
|
831
|
+
enable_console_logging: Enable console output
|
832
|
+
custom_resource_attributes: Additional OpenTelemetry resource attributes
|
833
|
+
instrumentation_options: Azure Monitor instrumentation options
|
834
|
+
cloud_role_name: Override cloud role name (defaults to service_name)
|
835
|
+
|
836
|
+
Returns:
|
837
|
+
Configured AzureLogger instance
|
838
|
+
"""
|
839
|
+
resolved_connection_string = connection_string or os.getenv(
|
840
|
+
"APPLICATIONINSIGHTS_CONNECTION_STRING"
|
841
|
+
)
|
842
|
+
|
843
|
+
attr_items = (
|
844
|
+
tuple(sorted(custom_resource_attributes.items()))
|
845
|
+
if custom_resource_attributes
|
846
|
+
else None
|
847
|
+
)
|
848
|
+
|
849
|
+
params_key = (
|
850
|
+
service_name,
|
851
|
+
service_version,
|
852
|
+
resolved_connection_string,
|
853
|
+
log_level,
|
854
|
+
enable_console_logging,
|
855
|
+
attr_items,
|
856
|
+
cloud_role_name,
|
857
|
+
)
|
858
|
+
|
859
|
+
if params_key in _loggers:
|
860
|
+
return _loggers[params_key]
|
861
|
+
|
862
|
+
logger = AzureLogger(
|
863
|
+
service_name=service_name,
|
864
|
+
service_version=service_version,
|
865
|
+
connection_string=connection_string,
|
866
|
+
log_level=log_level,
|
867
|
+
enable_console_logging=enable_console_logging,
|
868
|
+
custom_resource_attributes=custom_resource_attributes,
|
869
|
+
instrumentation_options=instrumentation_options,
|
870
|
+
cloud_role_name=cloud_role_name,
|
871
|
+
)
|
872
|
+
_loggers[params_key] = logger
|
873
|
+
return logger
|
874
|
+
|
875
|
+
|
876
|
+
def create_function_logger(
|
877
|
+
function_app_name: str,
|
878
|
+
function_name: str,
|
879
|
+
service_version: str = "1.0.0",
|
880
|
+
connection_string: Optional[str] = None,
|
881
|
+
instrumentation_options: Optional[Dict[str, Any]] = None,
|
882
|
+
cloud_role_name: Optional[str] = None,
|
883
|
+
) -> AzureLogger:
|
884
|
+
"""Create AzureLogger optimized for Azure Functions.
|
885
|
+
|
886
|
+
Automatically creates cloud role name in the format '{function_app_name}.{function_name}'
|
887
|
+
unless explicitly overridden. This ensures each function appears as a separate
|
888
|
+
component in the Application Insights Application Map.
|
889
|
+
|
890
|
+
Args:
|
891
|
+
function_app_name: Azure Function App name
|
892
|
+
function_name: Specific function name
|
893
|
+
service_version: Service version for metadata
|
894
|
+
connection_string: Application Insights connection string
|
895
|
+
instrumentation_options: Azure Monitor instrumentation options
|
896
|
+
cloud_role_name: Override cloud role name (defaults to '{function_app_name}.{function_name}')
|
897
|
+
|
898
|
+
Returns:
|
899
|
+
Configured AzureLogger with Azure Functions context
|
900
|
+
"""
|
901
|
+
custom_attributes = {
|
902
|
+
"azure.function.app": function_app_name,
|
903
|
+
"azure.function.name": function_name,
|
904
|
+
"azure.resource.type": "function",
|
905
|
+
}
|
906
|
+
|
907
|
+
default_service_name = f"{function_app_name}.{function_name}"
|
908
|
+
|
909
|
+
return create_app_logger(
|
910
|
+
service_name=default_service_name,
|
911
|
+
service_version=service_version,
|
912
|
+
connection_string=connection_string,
|
913
|
+
custom_resource_attributes=custom_attributes,
|
914
|
+
instrumentation_options=instrumentation_options,
|
915
|
+
cloud_role_name=cloud_role_name,
|
916
|
+
)
|