azpaddypy 0.3.3__py3-none-any.whl → 0.3.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
azpaddypy/mgmt/logging.py CHANGED
@@ -3,6 +3,7 @@ import os
3
3
  import functools
4
4
  import time
5
5
  import asyncio
6
+ import uuid
6
7
  from typing import Optional, Dict, Any, Union, Callable
7
8
  from datetime import datetime
8
9
  from azure.monitor.opentelemetry import configure_azure_monitor
@@ -19,12 +20,23 @@ class AzureLogger:
19
20
  tracking, baggage propagation, and automated function tracing for Azure
20
21
  applications with seamless local development support.
21
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
+
22
34
  Supports all standard logging levels (debug, info, warning, error, exception,
23
35
  critical) with enhanced context including trace IDs, correlation IDs, and
24
36
  baggage propagation.
25
37
 
26
38
  Attributes:
27
- service_name: Service identifier for telemetry
39
+ service_name: Service identifier for telemetry and cloud role name
28
40
  service_version: Service version for context
29
41
  connection_string: Application Insights connection string
30
42
  logger: Python logger instance
@@ -40,17 +52,24 @@ class AzureLogger:
40
52
  enable_console_logging: bool = True,
41
53
  custom_resource_attributes: Optional[Dict[str, str]] = None,
42
54
  instrumentation_options: Optional[Dict[str, Any]] = None,
55
+ cloud_role_name: Optional[str] = None,
43
56
  ):
44
57
  """Initialize Azure Logger with OpenTelemetry tracing.
45
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
+
46
64
  Args:
47
- service_name: Service identifier for telemetry
65
+ service_name: Service identifier for telemetry and cloud role name
48
66
  service_version: Service version for metadata
49
67
  connection_string: Application Insights connection string
50
68
  log_level: Python logging level (default: INFO)
51
69
  enable_console_logging: Enable console output for local development
52
70
  custom_resource_attributes: Additional OpenTelemetry resource attributes
53
71
  instrumentation_options: Azure Monitor instrumentation options
72
+ cloud_role_name: Override cloud role name (defaults to service_name)
54
73
  """
55
74
  self.service_name = service_name
56
75
  self.service_version = service_version
@@ -58,9 +77,14 @@ class AzureLogger:
58
77
  "APPLICATIONINSIGHTS_CONNECTION_STRING"
59
78
  )
60
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
+
61
84
  # Configure resource attributes
85
+ # NOTE: service.name automatically maps to cloud role name in Application Insights
62
86
  resource_attributes = {
63
- "service.name": service_name,
87
+ "service.name": effective_cloud_role_name,
64
88
  "service.version": service_version,
65
89
  "service.instance.id": os.getenv("WEBSITE_INSTANCE_ID", "local"),
66
90
  }
@@ -100,7 +124,8 @@ class AzureLogger:
100
124
  self._correlation_id = None
101
125
 
102
126
  self.info(
103
- f"Azure Logger initialized for service '{service_name}' v{service_version}"
127
+ f"Azure Logger initialized for service '{service_name}' v{service_version} "
128
+ f"(cloud role: '{effective_cloud_role_name}')"
104
129
  )
105
130
 
106
131
  def _setup_console_handler(self):
@@ -115,16 +140,27 @@ class AzureLogger:
115
140
  def set_correlation_id(self, correlation_id: str):
116
141
  """Set correlation ID for request/transaction tracking.
117
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
+
118
147
  Args:
119
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.
120
153
  """
121
154
  self._correlation_id = correlation_id
122
155
 
123
156
  def get_correlation_id(self) -> Optional[str]:
124
157
  """Get current correlation ID.
125
158
 
159
+ Returns the currently active correlation ID, whether manually set
160
+ or automatically generated by the trace_function decorator.
161
+
126
162
  Returns:
127
- Current correlation ID if set, otherwise None
163
+ Current correlation ID if set (manual or auto-generated), otherwise None
128
164
  """
129
165
  return self._correlation_id
130
166
 
@@ -510,16 +546,38 @@ class AzureLogger:
510
546
  log_args: bool = True,
511
547
  log_result: bool = False
512
548
  ) -> Callable:
513
- """Decorator for automatic function execution tracing.
549
+ """Decorator for automatic function execution tracing with correlation ID support.
514
550
 
515
551
  Supports both synchronous and asynchronous functions with comprehensive
516
- logging and OpenTelemetry span creation.
552
+ logging and OpenTelemetry span creation. Automatically generates and manages
553
+ correlation IDs for distributed tracing.
554
+
555
+ CORRELATION ID AUTOMATION:
556
+ - If no correlation_id is set, automatically generates a UUID4 correlation_id
557
+ - If correlation_id is manually set, preserves the existing value
558
+ - Correlation_id is automatically added to all spans and log entries
559
+ - Ensures consistent tracing across function calls
517
560
 
518
561
  Args:
519
562
  function_name: Custom span name (defaults to function name)
520
563
  log_execution: Whether to log execution metrics
521
564
  log_args: Whether to log function arguments
522
565
  log_result: Whether to log function result
566
+
567
+ Returns:
568
+ Decorated function with automatic tracing and correlation ID support
569
+
570
+ Example:
571
+ # Automatic correlation_id generation
572
+ @logger.trace_function()
573
+ def my_function():
574
+ return "result"
575
+
576
+ # Manual correlation_id (preserved)
577
+ logger.set_correlation_id("manual-id")
578
+ @logger.trace_function()
579
+ def my_function():
580
+ return "result"
523
581
  """
524
582
 
525
583
  def decorator(func):
@@ -527,6 +585,11 @@ class AzureLogger:
527
585
  async def async_wrapper(*args, **kwargs):
528
586
  span_name = function_name or f"{func.__module__}.{func.__name__}"
529
587
  with self.tracer.start_as_current_span(span_name) as span:
588
+ # Auto-generate correlation_id if not set - ensures consistent tracing
589
+ # Manual correlation_ids take precedence over auto-generated ones
590
+ if not self._correlation_id:
591
+ self._correlation_id = str(uuid.uuid4())
592
+
530
593
  self._setup_span_for_function_trace(
531
594
  span, func, True, log_args, args, kwargs, log_result, log_execution
532
595
  )
@@ -560,6 +623,11 @@ class AzureLogger:
560
623
  def sync_wrapper(*args, **kwargs):
561
624
  span_name = function_name or f"{func.__module__}.{func.__name__}"
562
625
  with self.tracer.start_as_current_span(span_name) as span:
626
+ # Auto-generate correlation_id if not set - ensures consistent tracing
627
+ # Manual correlation_ids take precedence over auto-generated ones
628
+ if not self._correlation_id:
629
+ self._correlation_id = str(uuid.uuid4())
630
+
563
631
  self._setup_span_for_function_trace(
564
632
  span, func, False, log_args, args, kwargs, log_result, log_execution
565
633
  )
@@ -704,19 +772,23 @@ def create_app_logger(
704
772
  enable_console_logging: bool = True,
705
773
  custom_resource_attributes: Optional[Dict[str, str]] = None,
706
774
  instrumentation_options: Optional[Dict[str, Any]] = None,
775
+ cloud_role_name: Optional[str] = None,
707
776
  ) -> AzureLogger:
708
777
  """Create cached AzureLogger instance for applications.
709
778
 
710
779
  Returns existing logger if one with same configuration exists.
780
+ The service_name automatically becomes the cloud role name in Application Insights
781
+ unless explicitly overridden with cloud_role_name parameter.
711
782
 
712
783
  Args:
713
- service_name: Service identifier for telemetry
784
+ service_name: Service identifier for telemetry and cloud role name
714
785
  service_version: Service version for metadata
715
786
  connection_string: Application Insights connection string
716
787
  log_level: Python logging level
717
788
  enable_console_logging: Enable console output
718
789
  custom_resource_attributes: Additional OpenTelemetry resource attributes
719
790
  instrumentation_options: Azure Monitor instrumentation options
791
+ cloud_role_name: Override cloud role name (defaults to service_name)
720
792
 
721
793
  Returns:
722
794
  Configured AzureLogger instance
@@ -738,6 +810,7 @@ def create_app_logger(
738
810
  log_level,
739
811
  enable_console_logging,
740
812
  attr_items,
813
+ cloud_role_name,
741
814
  )
742
815
 
743
816
  if params_key in _loggers:
@@ -750,6 +823,8 @@ def create_app_logger(
750
823
  log_level=log_level,
751
824
  enable_console_logging=enable_console_logging,
752
825
  custom_resource_attributes=custom_resource_attributes,
826
+ instrumentation_options=instrumentation_options,
827
+ cloud_role_name=cloud_role_name,
753
828
  )
754
829
  _loggers[params_key] = logger
755
830
  return logger
@@ -761,15 +836,21 @@ def create_function_logger(
761
836
  service_version: str = "1.0.0",
762
837
  connection_string: Optional[str] = None,
763
838
  instrumentation_options: Optional[Dict[str, Any]] = None,
839
+ cloud_role_name: Optional[str] = None,
764
840
  ) -> AzureLogger:
765
841
  """Create AzureLogger optimized for Azure Functions.
766
842
 
843
+ Automatically creates cloud role name in the format '{function_app_name}.{function_name}'
844
+ unless explicitly overridden. This ensures each function appears as a separate
845
+ component in the Application Insights Application Map.
846
+
767
847
  Args:
768
848
  function_app_name: Azure Function App name
769
849
  function_name: Specific function name
770
850
  service_version: Service version for metadata
771
851
  connection_string: Application Insights connection string
772
852
  instrumentation_options: Azure Monitor instrumentation options
853
+ cloud_role_name: Override cloud role name (defaults to '{function_app_name}.{function_name}')
773
854
 
774
855
  Returns:
775
856
  Configured AzureLogger with Azure Functions context
@@ -780,10 +861,13 @@ def create_function_logger(
780
861
  "azure.resource.type": "function",
781
862
  }
782
863
 
864
+ default_service_name = f"{function_app_name}.{function_name}"
865
+
783
866
  return create_app_logger(
784
- service_name=f"{function_app_name}.{function_name}",
867
+ service_name=default_service_name,
785
868
  service_version=service_version,
786
869
  connection_string=connection_string,
787
870
  custom_resource_attributes=custom_attributes,
788
871
  instrumentation_options=instrumentation_options,
872
+ cloud_role_name=cloud_role_name,
789
873
  )
@@ -0,0 +1,12 @@
1
+ """Azure resources package for azpaddypy.
2
+
3
+ This package contains modules for interacting with various Azure resources
4
+ including Key Vault, Storage, and other Azure services.
5
+ """
6
+
7
+ from .keyvault import AzureKeyVault, create_azure_keyvault
8
+
9
+ __all__ = [
10
+ "AzureKeyVault",
11
+ "create_azure_keyvault",
12
+ ]
@@ -0,0 +1,530 @@
1
+ from typing import Optional, Dict, Any, Union, List
2
+ from azure.keyvault.secrets import SecretClient
3
+ from azure.keyvault.keys import KeyClient
4
+ from azure.keyvault.certificates import CertificateClient
5
+ from azure.core.exceptions import ResourceNotFoundError, ClientAuthenticationError
6
+ from azure.core.credentials import TokenCredential
7
+ from ..mgmt.logging import AzureLogger
8
+ from ..mgmt.identity import AzureIdentity
9
+
10
+
11
+ class AzureKeyVault:
12
+ """Azure Key Vault management with comprehensive secret, key, and certificate operations.
13
+
14
+ Provides standardized Azure Key Vault operations using Azure SDK clients
15
+ with integrated logging, error handling, and OpenTelemetry tracing support.
16
+ Supports operations for secrets, keys, and certificates with proper
17
+ authentication and authorization handling.
18
+
19
+ Attributes:
20
+ vault_url: Azure Key Vault URL
21
+ service_name: Service identifier for logging and tracing
22
+ service_version: Service version for context
23
+ logger: AzureLogger instance for structured logging
24
+ credential: Azure credential for authentication
25
+ secret_client: Azure Key Vault SecretClient instance
26
+ key_client: Azure Key Vault KeyClient instance
27
+ certificate_client: Azure Key Vault CertificateClient instance
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ vault_url: str,
33
+ credential: Optional[TokenCredential] = None,
34
+ azure_identity: Optional[AzureIdentity] = None,
35
+ service_name: str = "azure_keyvault",
36
+ service_version: str = "1.0.0",
37
+ logger: Optional[AzureLogger] = None,
38
+ connection_string: Optional[str] = None,
39
+ enable_secrets: bool = True,
40
+ enable_keys: bool = True,
41
+ enable_certificates: bool = True,
42
+ ):
43
+ """Initialize Azure Key Vault with comprehensive configuration.
44
+
45
+ Args:
46
+ vault_url: Azure Key Vault URL (e.g., https://vault.vault.azure.net/)
47
+ credential: Azure credential for authentication
48
+ azure_identity: AzureIdentity instance for credential management
49
+ service_name: Service name for tracing context
50
+ service_version: Service version for metadata
51
+ logger: Optional AzureLogger instance
52
+ connection_string: Application Insights connection string
53
+ enable_secrets: Enable secret operations client
54
+ enable_keys: Enable key operations client
55
+ enable_certificates: Enable certificate operations client
56
+
57
+ Raises:
58
+ ValueError: If neither credential nor azure_identity is provided
59
+ Exception: If client initialization fails
60
+ """
61
+ self.vault_url = vault_url
62
+ self.service_name = service_name
63
+ self.service_version = service_version
64
+ self.enable_secrets = enable_secrets
65
+ self.enable_keys = enable_keys
66
+ self.enable_certificates = enable_certificates
67
+
68
+ # Initialize logger - use provided instance or create new one
69
+ if logger is not None:
70
+ self.logger = logger
71
+ else:
72
+ self.logger = AzureLogger(
73
+ service_name=service_name,
74
+ service_version=service_version,
75
+ connection_string=connection_string,
76
+ enable_console_logging=True,
77
+ )
78
+
79
+ # Setup credential
80
+ if azure_identity is not None:
81
+ self.credential = azure_identity.get_credential()
82
+ self.azure_identity = azure_identity
83
+ elif credential is not None:
84
+ self.credential = credential
85
+ self.azure_identity = None
86
+ else:
87
+ raise ValueError("Either 'credential' or 'azure_identity' must be provided")
88
+
89
+ # Initialize clients
90
+ self.secret_client = None
91
+ self.key_client = None
92
+ self.certificate_client = None
93
+
94
+ self._setup_clients()
95
+
96
+ self.logger.info(
97
+ f"Azure Key Vault initialized for service '{service_name}' v{service_version}",
98
+ extra={
99
+ "vault_url": vault_url,
100
+ "secrets_enabled": enable_secrets,
101
+ "keys_enabled": enable_keys,
102
+ "certificates_enabled": enable_certificates,
103
+ }
104
+ )
105
+
106
+ def _setup_clients(self):
107
+ """Initialize Key Vault clients based on enabled features.
108
+
109
+ Raises:
110
+ Exception: If client initialization fails
111
+ """
112
+ try:
113
+ if self.enable_secrets:
114
+ self.secret_client = SecretClient(
115
+ vault_url=self.vault_url,
116
+ credential=self.credential
117
+ )
118
+ self.logger.debug("SecretClient initialized successfully")
119
+
120
+ if self.enable_keys:
121
+ self.key_client = KeyClient(
122
+ vault_url=self.vault_url,
123
+ credential=self.credential
124
+ )
125
+ self.logger.debug("KeyClient initialized successfully")
126
+
127
+ if self.enable_certificates:
128
+ self.certificate_client = CertificateClient(
129
+ vault_url=self.vault_url,
130
+ credential=self.credential
131
+ )
132
+ self.logger.debug("CertificateClient initialized successfully")
133
+
134
+ except Exception as e:
135
+ self.logger.error(
136
+ f"Failed to initialize Key Vault clients: {e}",
137
+ exc_info=True
138
+ )
139
+ raise
140
+
141
+ # Secret Operations
142
+ def get_secret(
143
+ self,
144
+ secret_name: str,
145
+ version: Optional[str] = None,
146
+ **kwargs
147
+ ) -> Optional[str]:
148
+ """Retrieve a secret from Azure Key Vault.
149
+
150
+ Args:
151
+ secret_name: Name of the secret
152
+ version: Optional specific version of the secret
153
+ **kwargs: Additional parameters for the secret retrieval
154
+
155
+ Returns:
156
+ Secret value if found, None if not found
157
+
158
+ Raises:
159
+ RuntimeError: If secret client is not initialized
160
+ Exception: If secret retrieval fails for reasons other than not found
161
+ """
162
+ with self.logger.create_span(
163
+ "AzureKeyVault.get_secret",
164
+ attributes={
165
+ "service.name": self.service_name,
166
+ "operation.type": "secret_retrieval",
167
+ "keyvault.secret_name": secret_name,
168
+ "keyvault.version": version or "latest",
169
+ "keyvault.vault_url": self.vault_url
170
+ }
171
+ ):
172
+ if self.secret_client is None:
173
+ error_msg = "Secret client not initialized. Enable secrets during initialization."
174
+ self.logger.error(error_msg)
175
+ raise RuntimeError(error_msg)
176
+
177
+ try:
178
+ self.logger.debug(
179
+ "Retrieving secret from Key Vault",
180
+ extra={
181
+ "secret_name": secret_name,
182
+ "version": version,
183
+ "vault_url": self.vault_url
184
+ }
185
+ )
186
+
187
+ secret = self.secret_client.get_secret(secret_name, version=version, **kwargs)
188
+
189
+ self.logger.info(
190
+ "Secret retrieved successfully",
191
+ extra={
192
+ "secret_name": secret_name,
193
+ "version": secret.properties.version if secret.properties else None,
194
+ "content_type": secret.properties.content_type if secret.properties else None
195
+ }
196
+ )
197
+
198
+ return secret.value
199
+
200
+ except ResourceNotFoundError:
201
+ self.logger.warning(
202
+ f"Secret '{secret_name}' not found in Key Vault",
203
+ extra={"secret_name": secret_name, "version": version}
204
+ )
205
+ return None
206
+ except ClientAuthenticationError as e:
207
+ self.logger.error(
208
+ f"Authentication failed for secret '{secret_name}': {e}",
209
+ extra={"secret_name": secret_name},
210
+ exc_info=True
211
+ )
212
+ raise
213
+ except Exception as e:
214
+ self.logger.error(
215
+ f"Failed to retrieve secret '{secret_name}': {e}",
216
+ extra={"secret_name": secret_name, "version": version},
217
+ exc_info=True
218
+ )
219
+ raise
220
+
221
+ def set_secret(
222
+ self,
223
+ secret_name: str,
224
+ secret_value: str,
225
+ content_type: Optional[str] = None,
226
+ tags: Optional[Dict[str, str]] = None,
227
+ **kwargs
228
+ ) -> bool:
229
+ """Set a secret in Azure Key Vault.
230
+
231
+ Args:
232
+ secret_name: Name of the secret
233
+ secret_value: Value of the secret
234
+ content_type: Optional content type for the secret
235
+ tags: Optional tags for the secret
236
+ **kwargs: Additional parameters for secret creation
237
+
238
+ Returns:
239
+ True if secret was set successfully
240
+
241
+ Raises:
242
+ RuntimeError: If secret client is not initialized
243
+ Exception: If secret creation fails
244
+ """
245
+ with self.logger.create_span(
246
+ "AzureKeyVault.set_secret",
247
+ attributes={
248
+ "service.name": self.service_name,
249
+ "operation.type": "secret_creation",
250
+ "keyvault.secret_name": secret_name,
251
+ "keyvault.content_type": content_type or "text/plain",
252
+ "keyvault.vault_url": self.vault_url
253
+ }
254
+ ):
255
+ if self.secret_client is None:
256
+ error_msg = "Secret client not initialized. Enable secrets during initialization."
257
+ self.logger.error(error_msg)
258
+ raise RuntimeError(error_msg)
259
+
260
+ try:
261
+ self.logger.debug(
262
+ "Setting secret in Key Vault",
263
+ extra={
264
+ "secret_name": secret_name,
265
+ "content_type": content_type,
266
+ "has_tags": tags is not None,
267
+ "vault_url": self.vault_url
268
+ }
269
+ )
270
+
271
+ secret = self.secret_client.set_secret(
272
+ secret_name,
273
+ secret_value,
274
+ content_type=content_type,
275
+ tags=tags,
276
+ **kwargs
277
+ )
278
+
279
+ self.logger.info(
280
+ "Secret set successfully",
281
+ extra={
282
+ "secret_name": secret_name,
283
+ "version": secret.properties.version if secret.properties else None,
284
+ "content_type": secret.properties.content_type if secret.properties else None
285
+ }
286
+ )
287
+
288
+ return True
289
+
290
+ except Exception as e:
291
+ self.logger.error(
292
+ f"Failed to set secret '{secret_name}': {e}",
293
+ extra={"secret_name": secret_name, "content_type": content_type},
294
+ exc_info=True
295
+ )
296
+ raise
297
+
298
+ def delete_secret(self, secret_name: str, **kwargs) -> bool:
299
+ """Delete a secret from Azure Key Vault.
300
+
301
+ Args:
302
+ secret_name: Name of the secret to delete
303
+ **kwargs: Additional parameters for secret deletion
304
+
305
+ Returns:
306
+ True if secret was deleted successfully
307
+
308
+ Raises:
309
+ RuntimeError: If secret client is not initialized
310
+ Exception: If secret deletion fails
311
+ """
312
+ with self.logger.create_span(
313
+ "AzureKeyVault.delete_secret",
314
+ attributes={
315
+ "service.name": self.service_name,
316
+ "operation.type": "secret_deletion",
317
+ "keyvault.secret_name": secret_name,
318
+ "keyvault.vault_url": self.vault_url
319
+ }
320
+ ):
321
+ if self.secret_client is None:
322
+ error_msg = "Secret client not initialized. Enable secrets during initialization."
323
+ self.logger.error(error_msg)
324
+ raise RuntimeError(error_msg)
325
+
326
+ try:
327
+ self.logger.debug(
328
+ "Deleting secret from Key Vault",
329
+ extra={"secret_name": secret_name, "vault_url": self.vault_url}
330
+ )
331
+
332
+ self.secret_client.begin_delete_secret(secret_name, **kwargs)
333
+
334
+ self.logger.info(
335
+ "Secret deletion initiated successfully",
336
+ extra={"secret_name": secret_name}
337
+ )
338
+
339
+ return True
340
+
341
+ except ResourceNotFoundError:
342
+ self.logger.warning(
343
+ f"Secret '{secret_name}' not found for deletion",
344
+ extra={"secret_name": secret_name}
345
+ )
346
+ return False
347
+ except Exception as e:
348
+ self.logger.error(
349
+ f"Failed to delete secret '{secret_name}': {e}",
350
+ extra={"secret_name": secret_name},
351
+ exc_info=True
352
+ )
353
+ raise
354
+
355
+ def list_secrets(self, **kwargs) -> List[str]:
356
+ """List all secrets in the Key Vault.
357
+
358
+ Args:
359
+ **kwargs: Additional parameters for listing secrets
360
+
361
+ Returns:
362
+ List of secret names
363
+
364
+ Raises:
365
+ RuntimeError: If secret client is not initialized
366
+ Exception: If listing secrets fails
367
+ """
368
+ with self.logger.create_span(
369
+ "AzureKeyVault.list_secrets",
370
+ attributes={
371
+ "service.name": self.service_name,
372
+ "operation.type": "secret_listing",
373
+ "keyvault.vault_url": self.vault_url
374
+ }
375
+ ):
376
+ if self.secret_client is None:
377
+ error_msg = "Secret client not initialized. Enable secrets during initialization."
378
+ self.logger.error(error_msg)
379
+ raise RuntimeError(error_msg)
380
+
381
+ try:
382
+ self.logger.debug(
383
+ "Listing secrets from Key Vault",
384
+ extra={"vault_url": self.vault_url}
385
+ )
386
+
387
+ secret_names = []
388
+ for secret_property in self.secret_client.list_properties_of_secrets(**kwargs):
389
+ secret_names.append(secret_property.name)
390
+
391
+ self.logger.info(
392
+ "Secrets listed successfully",
393
+ extra={"secret_count": len(secret_names)}
394
+ )
395
+
396
+ return secret_names
397
+
398
+ except Exception as e:
399
+ self.logger.error(
400
+ f"Failed to list secrets: {e}",
401
+ exc_info=True
402
+ )
403
+ raise
404
+
405
+ def test_connection(self) -> bool:
406
+ """Test connection to Key Vault by attempting to list secrets.
407
+
408
+ Returns:
409
+ True if connection is successful, False otherwise
410
+ """
411
+ with self.logger.create_span(
412
+ "AzureKeyVault.test_connection",
413
+ attributes={
414
+ "service.name": self.service_name,
415
+ "operation.type": "connection_test",
416
+ "keyvault.vault_url": self.vault_url
417
+ }
418
+ ):
419
+ try:
420
+ self.logger.debug(
421
+ "Testing Key Vault connection",
422
+ extra={"vault_url": self.vault_url}
423
+ )
424
+
425
+ if self.secret_client is not None:
426
+ # Try to list secrets (limited to 1) to test connection
427
+ list(self.secret_client.list_properties_of_secrets(max_page_size=1))
428
+ elif self.key_client is not None:
429
+ # Try to list keys if secrets are disabled
430
+ list(self.key_client.list_properties_of_keys(max_page_size=1))
431
+ elif self.certificate_client is not None:
432
+ # Try to list certificates if keys are disabled
433
+ list(self.certificate_client.list_properties_of_certificates(max_page_size=1))
434
+ else:
435
+ self.logger.error("No clients available for connection testing")
436
+ return False
437
+
438
+ self.logger.info("Key Vault connection test successful")
439
+ return True
440
+
441
+ except Exception as e:
442
+ self.logger.warning(
443
+ f"Key Vault connection test failed: {e}",
444
+ extra={"vault_url": self.vault_url}
445
+ )
446
+ return False
447
+
448
+ def set_correlation_id(self, correlation_id: str):
449
+ """Set correlation ID for request/transaction tracking.
450
+
451
+ Args:
452
+ correlation_id: Unique identifier for transaction correlation
453
+ """
454
+ self.logger.set_correlation_id(correlation_id)
455
+
456
+ def get_correlation_id(self) -> Optional[str]:
457
+ """Get current correlation ID.
458
+
459
+ Returns:
460
+ Current correlation ID if set, otherwise None
461
+ """
462
+ return self.logger.get_correlation_id()
463
+
464
+
465
+ def create_azure_keyvault(
466
+ vault_url: str,
467
+ credential: Optional[TokenCredential] = None,
468
+ azure_identity: Optional[AzureIdentity] = None,
469
+ service_name: str = "azure_keyvault",
470
+ service_version: str = "1.0.0",
471
+ logger: Optional[AzureLogger] = None,
472
+ connection_string: Optional[str] = None,
473
+ enable_secrets: bool = True,
474
+ enable_keys: bool = True,
475
+ enable_certificates: bool = True,
476
+ ) -> AzureKeyVault:
477
+ """Factory function to create AzureKeyVault instance.
478
+
479
+ Provides a convenient way to create an AzureKeyVault instance with
480
+ common configuration patterns. If no credential or azure_identity
481
+ is provided, creates a default AzureIdentity instance.
482
+
483
+ Args:
484
+ vault_url: Azure Key Vault URL
485
+ credential: Azure credential for authentication
486
+ azure_identity: AzureIdentity instance for credential management
487
+ service_name: Service name for tracing context
488
+ service_version: Service version for metadata
489
+ logger: Optional AzureLogger instance
490
+ connection_string: Application Insights connection string
491
+ enable_secrets: Enable secret operations client
492
+ enable_keys: Enable key operations client
493
+ enable_certificates: Enable certificate operations client
494
+
495
+ Returns:
496
+ Configured AzureKeyVault instance
497
+
498
+ Example:
499
+ # Basic usage with default credential
500
+ kv = create_azure_keyvault("https://vault.vault.azure.net/")
501
+
502
+ # With custom service name and specific features
503
+ kv = create_azure_keyvault(
504
+ "https://vault.vault.azure.net/",
505
+ service_name="my_app",
506
+ enable_keys=False,
507
+ enable_certificates=False
508
+ )
509
+ """
510
+ if credential is None and azure_identity is None:
511
+ # Create default AzureIdentity instance
512
+ from ..mgmt.identity import create_azure_identity
513
+ azure_identity = create_azure_identity(
514
+ service_name=f"{service_name}_identity",
515
+ service_version=service_version,
516
+ connection_string=connection_string,
517
+ )
518
+
519
+ return AzureKeyVault(
520
+ vault_url=vault_url,
521
+ credential=credential,
522
+ azure_identity=azure_identity,
523
+ service_name=service_name,
524
+ service_version=service_version,
525
+ logger=logger,
526
+ connection_string=connection_string,
527
+ enable_secrets=enable_secrets,
528
+ enable_keys=enable_keys,
529
+ enable_certificates=enable_certificates,
530
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: azpaddypy
3
- Version: 0.3.3
3
+ Version: 0.3.5
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
@@ -9,9 +9,8 @@ Description-Content-Type: text/markdown
9
9
  License-File: LICENSE
10
10
  Requires-Dist: azure-monitor-opentelemetry==1.6.10
11
11
  Requires-Dist: azure-functions==1.23.0
12
- Provides-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"
12
+ Requires-Dist: azure-identity==1.23.0
13
+ Requires-Dist: azure-keyvault-secrets==4.10.0
14
+ Requires-Dist: azure-keyvault-keys==4.10.0
15
+ Requires-Dist: azure-keyvault-certificates==4.10.0
17
16
  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=pivPsHeySF1Dyx6HKCSos7HBOYyJMVeRP25wYZu3Sno,35117
4
+ azpaddypy/resources/__init__.py,sha256=Bvt3VK4RqwoxYpoh6EbLXIR18RuFPKaLP6zLL-icyFk,314
5
+ azpaddypy/resources/keyvault.py,sha256=4J08vLqoLFd1_UUDBji2oG2fatZaPkgnRyT_Z6wHAOc,20312
6
+ azpaddypy-0.3.5.dist-info/licenses/LICENSE,sha256=hQ6t0g2QaewGCQICHqTckBFbMVakGmoyTAzDpmEYV4c,1089
7
+ azpaddypy-0.3.5.dist-info/METADATA,sha256=m56dD6Ev3sTewyiNU-UgPzw6L73idRtUlCM5o7lOwjg,665
8
+ azpaddypy-0.3.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
+ azpaddypy-0.3.5.dist-info/top_level.txt,sha256=hsDuboDhT61320ML8X479ezSTwT3rrlDWz1_Z45B2cs,10
10
+ azpaddypy-0.3.5.dist-info/RECORD,,
@@ -1,112 +0,0 @@
1
- import logging
2
- import json
3
- import time
4
- import azure.functions as func
5
- from azpaddypy.mgmt.logging import create_function_logger
6
-
7
- # Initialize the logger
8
- logger = create_function_logger(
9
- function_app_name="test-function-app", function_name="test-function"
10
- )
11
-
12
-
13
- @logger.trace_function(log_args=True, log_result=True)
14
- def process_request(req_body: dict) -> dict:
15
- """Process the request body and return a response"""
16
- # Simulate some processing time
17
- time.sleep(0.1)
18
-
19
- # Log the request processing
20
- logger.info(
21
- "Processing request",
22
- extra={
23
- "request_id": req_body.get("request_id", "unknown"),
24
- "action": req_body.get("action", "unknown"),
25
- },
26
- )
27
-
28
- return {
29
- "status": "success",
30
- "message": "Request processed successfully",
31
- "data": req_body,
32
- }
33
-
34
-
35
- def main(req: func.HttpRequest) -> func.HttpResponse:
36
- """Azure Function entry point"""
37
- try:
38
- # Start timing the request
39
- start_time = time.time()
40
-
41
- # Get request details
42
- method = req.method
43
- url = req.url
44
- headers = dict(req.headers)
45
-
46
- # Log the incoming request
47
- logger.log_request(
48
- method=method,
49
- url=url,
50
- status_code=200, # We'll update this if there's an error
51
- duration_ms=0, # We'll update this at the end
52
- extra={"headers": headers, "request_type": "http_trigger"},
53
- )
54
-
55
- # Create a span for the entire function execution
56
- with logger.create_span("function_execution") as span:
57
- # Add request metadata to the span
58
- span.set_attribute("http.method", method)
59
- span.set_attribute("http.url", url)
60
-
61
- # Parse request body
62
- try:
63
- req_body = req.get_json()
64
- except ValueError:
65
- req_body = {}
66
-
67
- # Log the request body
68
- logger.info("Received request body", extra={"body": req_body})
69
-
70
- # Process the request
71
- result = process_request(req_body)
72
-
73
- # Calculate request duration
74
- duration_ms = (time.time() - start_time) * 1000
75
-
76
- # Log successful completion
77
- logger.log_function_execution(
78
- function_name="main",
79
- duration_ms=duration_ms,
80
- success=True,
81
- extra={"method": method, "url": url},
82
- )
83
-
84
- # Return the response
85
- return func.HttpResponse(
86
- json.dumps(result), mimetype="application/json", status_code=200
87
- )
88
-
89
- except Exception as e:
90
- # Calculate request duration
91
- duration_ms = (time.time() - start_time) * 1000
92
-
93
- # Log the error
94
- logger.error(
95
- f"Error processing request: {str(e)}",
96
- extra={"method": method, "url": url, "error_type": type(e).__name__},
97
- )
98
-
99
- # Log failed execution
100
- logger.log_function_execution(
101
- function_name="main",
102
- duration_ms=duration_ms,
103
- success=False,
104
- extra={"error": str(e), "error_type": type(e).__name__},
105
- )
106
-
107
- # Return error response
108
- return func.HttpResponse(
109
- json.dumps({"status": "error", "message": str(e)}),
110
- mimetype="application/json",
111
- status_code=500,
112
- )
@@ -1,129 +0,0 @@
1
- import logging
2
- import json
3
- import time
4
- import asyncio
5
- import azure.functions as func
6
- from azpaddypy.mgmt.logging import create_function_logger
7
-
8
- app = func.FunctionApp()
9
-
10
- # Initialize the logger
11
- logger = create_function_logger(
12
- function_app_name="test-function-app", function_name="test-function"
13
- )
14
-
15
-
16
- @logger.trace_function(log_args=True, log_result=True)
17
- def process_request(req_body: dict) -> dict:
18
- """Process the request body and return a response"""
19
- # Simulate some processing time
20
- time.sleep(0.1)
21
-
22
- # Log the request processing
23
- logger.info(
24
- "Processing request",
25
- extra={
26
- "request_id": req_body.get("request_id", "unknown"),
27
- "action": req_body.get("action", "unknown"),
28
- },
29
- )
30
-
31
- return {
32
- "status": "success",
33
- "message": "Request processed successfully",
34
- "data": req_body,
35
- }
36
-
37
-
38
- @logger.trace_function(log_args=True, log_result=True)
39
- async def process_request_async(req_body: dict) -> dict:
40
- """Process the request body asynchronously and return a response"""
41
- # Simulate some async processing time
42
- await asyncio.sleep(0.1)
43
-
44
- # Log the request processing
45
- logger.info(
46
- "Processing async request",
47
- extra={
48
- "request_id": req_body.get("request_id", "unknown"),
49
- "action": req_body.get("action", "unknown"),
50
- "is_async": True,
51
- },
52
- )
53
-
54
- return {
55
- "status": "success",
56
- "message": "Async request processed successfully",
57
- "data": req_body,
58
- }
59
-
60
-
61
- @app.function_name(name="test-function")
62
- @app.route(route="test-function", auth_level=func.AuthLevel.ANONYMOUS)
63
- async def test_function(req: func.HttpRequest) -> func.HttpResponse:
64
- """Azure Function HTTP trigger that processes requests both synchronously and asynchronously"""
65
- start_time = time.time()
66
- method = req.method
67
- url = str(req.url)
68
-
69
- try:
70
- # Get request body
71
- req_body = req.get_json()
72
-
73
- # Process request based on the action
74
- action = req_body.get("action", "").lower()
75
-
76
- if action == "async":
77
- # Process request asynchronously
78
- result = await process_request_async(req_body)
79
- else:
80
- # Process request synchronously
81
- result = process_request(req_body)
82
-
83
- # Calculate request duration
84
- duration_ms = (time.time() - start_time) * 1000
85
-
86
- # Log successful request
87
- logger.log_request(
88
- method=method,
89
- url=url,
90
- status_code=200,
91
- duration_ms=duration_ms,
92
- extra={
93
- "request_id": req_body.get("request_id", "unknown"),
94
- "action": action,
95
- "is_async": action == "async",
96
- },
97
- )
98
-
99
- # Return success response
100
- return func.HttpResponse(
101
- json.dumps(result),
102
- mimetype="application/json",
103
- status_code=200,
104
- )
105
-
106
- except Exception as e:
107
- # Calculate request duration
108
- duration_ms = (time.time() - start_time) * 1000
109
-
110
- # Log the error
111
- logger.error(
112
- f"Error processing request: {str(e)}",
113
- extra={"method": method, "url": url, "error_type": type(e).__name__},
114
- )
115
-
116
- # Log failed execution
117
- logger.log_function_execution(
118
- function_name="test_function",
119
- duration_ms=duration_ms,
120
- success=False,
121
- extra={"error": str(e), "error_type": type(e).__name__},
122
- )
123
-
124
- # Return error response
125
- return func.HttpResponse(
126
- json.dumps({"status": "error", "message": str(e)}),
127
- mimetype="application/json",
128
- status_code=500,
129
- )
@@ -1,10 +0,0 @@
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=Cz_xPjFYfiNh2_S7jww8QR1EaGgQfXcFzEWZqCDp8ks,30638
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.3.3.dist-info/licenses/LICENSE,sha256=hQ6t0g2QaewGCQICHqTckBFbMVakGmoyTAzDpmEYV4c,1089
7
- azpaddypy-0.3.3.dist-info/METADATA,sha256=8Pnqt9ZOhDafwPlx2VeC_Zknadyb9kHBPv76Ss-OPLQ,705
8
- azpaddypy-0.3.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
- azpaddypy-0.3.3.dist-info/top_level.txt,sha256=hsDuboDhT61320ML8X479ezSTwT3rrlDWz1_Z45B2cs,10
10
- azpaddypy-0.3.3.dist-info/RECORD,,