warpzone-sdk 15.0.0.dev10__tar.gz → 15.0.0.dev12__tar.gz

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.
Files changed (60) hide show
  1. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/PKG-INFO +1 -3
  2. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/pyproject.toml +1 -3
  3. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/function/monitor.py +29 -15
  4. warpzone_sdk-15.0.0.dev12/warpzone/monitor/__init__.py +2 -0
  5. warpzone_sdk-15.0.0.dev12/warpzone/monitor/logs.py +81 -0
  6. warpzone_sdk-15.0.0.dev12/warpzone/monitor/traces.py +124 -0
  7. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/servicebus/events/client.py +13 -13
  8. warpzone_sdk-15.0.0.dev10/warpzone/monitor/__init__.py +0 -2
  9. warpzone_sdk-15.0.0.dev10/warpzone/monitor/logs.py +0 -33
  10. warpzone_sdk-15.0.0.dev10/warpzone/monitor/traces.py +0 -142
  11. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/README.md +0 -0
  12. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/__init__.py +0 -0
  13. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/blobstorage/__init__.py +0 -0
  14. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/blobstorage/client.py +0 -0
  15. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/db/__init__.py +0 -0
  16. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/db/client.py +0 -0
  17. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/deltastorage/__init__.py +0 -0
  18. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/deltastorage/data_types.py +0 -0
  19. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/deltastorage/generated_columns.py +0 -0
  20. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/deltastorage/lock_client.py +0 -0
  21. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/deltastorage/schema.py +0 -0
  22. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/deltastorage/slicing.py +0 -0
  23. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/deltastorage/store.py +0 -0
  24. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/deltastorage/table.py +0 -0
  25. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/enums/__init__.py +0 -0
  26. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/enums/topicenum.py +0 -0
  27. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/function/__init__.py +0 -0
  28. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/function/checks.py +0 -0
  29. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/function/functionize.py +0 -0
  30. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/function/integrations.py +0 -0
  31. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/function/process.py +0 -0
  32. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/function/processors/__init__.py +0 -0
  33. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/function/processors/dependencies.py +0 -0
  34. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/function/processors/outputs.py +0 -0
  35. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/function/processors/triggers.py +0 -0
  36. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/function/signature.py +0 -0
  37. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/function/types.py +0 -0
  38. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/healthchecks/__init__.py +0 -0
  39. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/healthchecks/model.py +0 -0
  40. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/servicebus/data/__init__.py +0 -0
  41. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/servicebus/data/client.py +0 -0
  42. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/servicebus/events/__init__.py +0 -0
  43. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/servicebus/events/triggers.py +0 -0
  44. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/tablestorage/db/__init__.py +0 -0
  45. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/tablestorage/db/base_client.py +0 -0
  46. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/tablestorage/db/client.py +0 -0
  47. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/tablestorage/db/table_config.py +0 -0
  48. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/tablestorage/tables/__init__.py +0 -0
  49. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/tablestorage/tables/client.py +0 -0
  50. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/tablestorage/tables/entities.py +0 -0
  51. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/tablestorage/tables/helpers.py +0 -0
  52. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/testing/__init__.py +0 -0
  53. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/testing/assertions.py +0 -0
  54. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/testing/data.py +0 -0
  55. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/testing/matchers.py +0 -0
  56. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/tools/__init__.py +0 -0
  57. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/tools/copy.py +0 -0
  58. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/transform/__init__.py +0 -0
  59. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/transform/data.py +0 -0
  60. {warpzone_sdk-15.0.0.dev10 → warpzone_sdk-15.0.0.dev12}/warpzone/transform/schema.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: warpzone-sdk
3
- Version: 15.0.0.dev10
3
+ Version: 15.0.0.dev12
4
4
  Summary: The main objective of this package is to centralize logic used to interact with Azure Functions, Azure Service Bus and Azure Table Storage
5
5
  Author: Team Enigma
6
6
  Author-email: enigma@energinet.dk
@@ -13,11 +13,9 @@ Classifier: Programming Language :: Python :: 3.13
13
13
  Classifier: Programming Language :: Python :: 3.14
14
14
  Requires-Dist: aiohttp (>=3.8.3)
15
15
  Requires-Dist: azure-core (>=1.26.3)
16
- Requires-Dist: azure-core-tracing-opentelemetry (>=1.0.0b12)
17
16
  Requires-Dist: azure-data-tables (>=12.4.0)
18
17
  Requires-Dist: azure-functions (>=1.12.0)
19
18
  Requires-Dist: azure-identity (>=1.15.0)
20
- Requires-Dist: azure-monitor-opentelemetry (>=1.8.4)
21
19
  Requires-Dist: azure-monitor-opentelemetry-exporter (>=1.0.0b36)
22
20
  Requires-Dist: azure-servicebus (>=7.8.0)
23
21
  Requires-Dist: azure-storage-blob (>=12.14.1)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "warpzone-sdk"
3
- version = "15.0.0.dev10"
3
+ version = "15.0.0.dev12"
4
4
  description = "The main objective of this package is to centralize logic used to interact with Azure Functions, Azure Service Bus and Azure Table Storage"
5
5
  authors = [{ name = "Team Enigma", email = "enigma@energinet.dk" }]
6
6
  requires-python = ">=3.10"
@@ -23,8 +23,6 @@ azure-identity = ">=1.15.0"
23
23
  azure-servicebus = ">=7.8.0"
24
24
  azure-storage-blob = ">=12.14.1"
25
25
  aiohttp = ">=3.8.3"
26
- azure-core-tracing-opentelemetry = ">=1.0.0b12"
27
- azure-monitor-opentelemetry = ">=1.8.4"
28
26
  azure-monitor-opentelemetry-exporter = ">=1.0.0b36"
29
27
  opentelemetry-sdk = ">=1.32.0"
30
28
  azure-functions = ">=1.12.0"
@@ -1,29 +1,43 @@
1
- import inspect
1
+ import asyncio
2
+ import logging
2
3
  from contextlib import contextmanager
3
4
  from typing import Callable
4
5
 
5
6
  import azure.functions as func
6
7
 
7
8
  from warpzone.function.types import SingleArgumentCallable
8
- from warpzone.monitor import traces
9
-
10
- # Configure tracing at import time (once per worker process)
11
- traces.configure_tracing()
9
+ from warpzone.monitor import logs, traces
12
10
 
13
11
  SUBJECT_IDENTIFIER = "<Subject>"
14
12
 
13
+ tracer = traces.get_tracer(__name__)
14
+ logger = logs.get_logger(__name__)
15
15
 
16
- @contextmanager
17
- def run_in_trace_context(context: func.Context):
18
- """Set up trace context and ensure log correlation for this function invocation.
19
16
 
20
- This sets:
21
- 1. OpenTelemetry trace context (for distributed tracing via Service Bus)
22
- 2. Azure Functions invocation_id in thread-local storage (for log correlation)
17
+ def configure_monitoring():
23
18
  """
24
- # Ensure Azure Functions' log correlation uses this invocation's ID
25
- # This prevents log leakage when multiple functions run concurrently
26
- context.thread_local_storage.invocation_id = context.invocation_id
19
+ Configure logging and tracing on Azure Function to
20
+ - export telemetry to App Insights
21
+ - suppress spamming logs
22
+ """
23
+ # disable logging for HTTP calls to avoid log spamming
24
+ logging.getLogger("azure.core.pipeline.policies.http_logging_policy").setLevel(
25
+ logging.WARNING
26
+ )
27
+
28
+ # disable logging for Service Bus underlying uAMQP library to avoid log spamming
29
+ logging.getLogger("uamqp").setLevel(logging.WARNING)
30
+
31
+ # configure tracer provider
32
+ traces.configure_tracing()
33
+
34
+ # configure logger provider
35
+ logs.configure_logging()
36
+
37
+
38
+ @contextmanager
39
+ def run_in_trace_context(context: func.Context):
40
+ configure_monitoring()
27
41
 
28
42
  trace_context = context.trace_context
29
43
  with traces.set_trace_context(
@@ -61,7 +75,7 @@ def monitor(main: SingleArgumentCallable) -> Callable:
61
75
  result = main(arg)
62
76
  return result
63
77
 
64
- if inspect.iscoroutinefunction(main):
78
+ if asyncio.iscoroutinefunction(main):
65
79
  return wrapper_async
66
80
  else:
67
81
  return wrapper
@@ -0,0 +1,2 @@
1
+ from .logs import get_logger
2
+ from .traces import get_current_diagnostic_id, get_tracer, servicebus_send_span
@@ -0,0 +1,81 @@
1
+ # NOTE: OpenTelemetry logging to Azure is still in EXPERIMENTAL mode!
2
+ import logging
3
+ import os
4
+ from logging import StreamHandler
5
+
6
+ from azure.monitor.opentelemetry.exporter import AzureMonitorLogExporter
7
+ from opentelemetry import _logs as logs
8
+ from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
9
+ from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
10
+ from opentelemetry.sdk.resources import SERVICE_NAME, Resource
11
+
12
+ logger = logging.getLogger(__name__)
13
+ logger.addHandler(StreamHandler())
14
+
15
+ # Suppress verbose logging from Azure SDK and infrastructure
16
+ _NOISY_LOGGERS = [
17
+ "azure.core.pipeline.policies.http_logging_policy",
18
+ "azure.data.tables",
19
+ "azure.storage.blob",
20
+ "azure.servicebus",
21
+ "azure.identity",
22
+ "azure.monitor.opentelemetry.exporter",
23
+ "azure_functions_worker",
24
+ "azure.functions",
25
+ "uamqp",
26
+ ]
27
+ for _logger_name in _NOISY_LOGGERS:
28
+ logging.getLogger(_logger_name).setLevel(logging.WARNING)
29
+
30
+ LOGGING_IS_CONFIGURED = False
31
+
32
+
33
+ def configure_logging():
34
+ global LOGGING_IS_CONFIGURED
35
+ if LOGGING_IS_CONFIGURED:
36
+ # logging should only be set up once
37
+ # to avoid duplicated log handling.
38
+ # Global variables is the pattern used
39
+ # by opentelemetry, so we use the same
40
+ return
41
+
42
+ # set up logger provider based on the Azure Function resource
43
+ # (this is make sure App Insights can track the log source correctly)
44
+ # (https://learn.microsoft.com/en-us/azure/azure-monitor/app/opentelemetry-enable?tabs=net#set-the-cloud-role-name-and-the-cloud-role-instance)
45
+ resource = Resource.create({SERVICE_NAME: os.getenv("WEBSITE_SITE_NAME")})
46
+ logs.set_logger_provider(
47
+ LoggerProvider(
48
+ resource=resource,
49
+ )
50
+ )
51
+
52
+ # setup azure monitor log exporter to send telemetry to App Insights
53
+ try:
54
+ log_exporter = AzureMonitorLogExporter()
55
+ except ValueError:
56
+ # if no App Insights instrumentation key is set (e.g. when running unit tests),
57
+ # the exporter creation will fail. In this case we skip it
58
+ logger.warning(
59
+ "Cant set up logging to App Insights, as no instrumentation key is set."
60
+ )
61
+ else:
62
+ log_record_processor = BatchLogRecordProcessor(log_exporter)
63
+ logs.get_logger_provider().add_log_record_processor(log_record_processor)
64
+
65
+ LOGGING_IS_CONFIGURED = True
66
+
67
+
68
+ def get_logger(name: str):
69
+ # set up standard logger
70
+ logger = logging.getLogger(name)
71
+ logger.setLevel(logging.INFO)
72
+
73
+ # Check if OTEL handler is already added to this specific logger
74
+ # (not using hasHandlers() as it also checks parent/root handlers)
75
+ has_otel_handler = any(isinstance(h, LoggingHandler) for h in logger.handlers)
76
+ if not has_otel_handler:
77
+ # add OTEL handler for trace correlation
78
+ handler = LoggingHandler()
79
+ logger.addHandler(handler)
80
+
81
+ return logger
@@ -0,0 +1,124 @@
1
+ import logging
2
+ import os
3
+ from contextlib import contextmanager
4
+ from logging import StreamHandler
5
+
6
+ from azure.monitor.opentelemetry.exporter import AzureMonitorTraceExporter
7
+ from opentelemetry import context, trace
8
+ from opentelemetry.sdk.resources import SERVICE_NAME, Resource
9
+ from opentelemetry.sdk.trace import TracerProvider
10
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
11
+ from opentelemetry.sdk.trace.sampling import ALWAYS_ON
12
+ from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
13
+
14
+ logger = logging.getLogger(__name__)
15
+ logger.addHandler(StreamHandler())
16
+
17
+ tracer = trace.get_tracer(__name__)
18
+
19
+ TRACING_IS_CONFIGURED = False
20
+
21
+
22
+ def configure_tracing():
23
+ global TRACING_IS_CONFIGURED
24
+ if TRACING_IS_CONFIGURED:
25
+ # tracing should only be set up once
26
+ # to avoid duplicated trace handling.
27
+ # Global variables is the pattern used
28
+ # by opentelemetry, so we use the same
29
+ return
30
+
31
+ # set up tracer provider based on the Azure Function resource
32
+ # (this is make sure App Insights can track the trace source correctly)
33
+ # (https://learn.microsoft.com/en-us/azure/azure-monitor/app/opentelemetry-enable?tabs=net#set-the-cloud-role-name-and-the-cloud-role-instance).
34
+ # We use the ALWAYS ON sampler since otherwise spans will not be
35
+ # recording upon creation
36
+ # (https://anecdotes.dev/opentelemetry-on-google-cloud-unraveling-the-mystery-f61f044c18be)
37
+ resource = Resource.create({SERVICE_NAME: os.getenv("WEBSITE_SITE_NAME")})
38
+ trace.set_tracer_provider(
39
+ TracerProvider(
40
+ sampler=ALWAYS_ON,
41
+ resource=resource,
42
+ )
43
+ )
44
+
45
+ # setup azure monitor trace exporter to send telemetry to App Insights
46
+ try:
47
+ trace_exporter = AzureMonitorTraceExporter()
48
+ except ValueError:
49
+ # if no App Insights instrumentation key is set (e.g. when running unit tests),
50
+ # the exporter creation will fail. In this case we skip it
51
+ logger.warning(
52
+ "Cant set up tracing to App Insights, as no instrumentation key is set."
53
+ )
54
+ else:
55
+ span_processor = BatchSpanProcessor(trace_exporter)
56
+ trace.get_tracer_provider().add_span_processor(span_processor)
57
+
58
+ TRACING_IS_CONFIGURED = True
59
+
60
+
61
+ @contextmanager
62
+ def set_trace_context(trace_parent: str, trace_state: str = ""):
63
+ """Context manager for setting the trace context
64
+
65
+ Args:
66
+ trace_parent (str): Trace parent ID
67
+ trace_state (str, optional): Trace state. Defaults to "".
68
+ """
69
+ carrier = {"traceparent": trace_parent, "tracestate": trace_state}
70
+ ctx = TraceContextTextMapPropagator().extract(carrier=carrier)
71
+
72
+ token = context.attach(ctx) # attach context before run
73
+ try:
74
+ yield
75
+ finally:
76
+ context.detach(token) # detach context after run
77
+
78
+
79
+ def get_tracer(name: str):
80
+ tracer = trace.get_tracer(name)
81
+ return tracer
82
+
83
+
84
+ def get_current_diagnostic_id() -> str:
85
+ """Gets diagnostic id from current span
86
+
87
+ The diagnostic id is a concatenation of operation-id and parent-id
88
+
89
+ Returns:
90
+ str: diagnostic id
91
+ """
92
+ span = trace.get_current_span()
93
+
94
+ if not span.is_recording():
95
+ return ""
96
+
97
+ operation_id = "{:016x}".format(span.context.trace_id)
98
+ parent_id = "{:016x}".format(span.context.span_id)
99
+
100
+ diagnostic_id = f"00-{operation_id}-{parent_id}-01"
101
+
102
+ return diagnostic_id
103
+
104
+
105
+ # Service Bus trace constants (these were removed from azure-servicebus SDK)
106
+ _SB_TRACE_NAMESPACE = "Microsoft.ServiceBus"
107
+
108
+
109
+ @contextmanager
110
+ def servicebus_send_span(subject: str) -> trace.Span:
111
+ """Start span for Service Bus message tracing.
112
+
113
+ Args:
114
+ subject: The message subject (used as span name for easy identification)
115
+
116
+ Yields:
117
+ trace.Span: the span
118
+ """
119
+ with tracer.start_as_current_span(
120
+ subject, kind=trace.SpanKind.PRODUCER
121
+ ) as msg_span:
122
+ msg_span.set_attributes({"az.namespace": _SB_TRACE_NAMESPACE})
123
+
124
+ yield msg_span
@@ -125,20 +125,20 @@ class WarpzoneEventClient:
125
125
  ):
126
126
  typeguard.check_type(value=topic, expected_type=Topic)
127
127
  topic_name = topic.value
128
+ with traces.servicebus_send_span(event_msg.subject):
129
+ diagnostic_id = traces.get_current_diagnostic_id()
130
+
131
+ az_sdk_msg = ServiceBusMessage(
132
+ body=json.dumps(event_msg.event),
133
+ subject=event_msg.subject,
134
+ content_type="application/json",
135
+ message_id=event_msg.message_id,
136
+ application_properties={"Diagnostic-Id": diagnostic_id},
137
+ time_to_live=event_msg.time_to_live,
138
+ )
128
139
 
129
- diagnostic_id = traces.get_current_diagnostic_id()
130
-
131
- az_sdk_msg = ServiceBusMessage(
132
- body=json.dumps(event_msg.event),
133
- subject=event_msg.subject,
134
- content_type="application/json",
135
- message_id=event_msg.message_id,
136
- application_properties={"Diagnostic-Id": diagnostic_id},
137
- time_to_live=event_msg.time_to_live,
138
- )
139
-
140
- with self._get_topic_sender(topic_name) as sender:
141
- sender.send_messages(message=az_sdk_msg)
140
+ with self._get_topic_sender(topic_name) as sender:
141
+ sender.send_messages(message=az_sdk_msg)
142
142
 
143
143
  def list_subscriptions(
144
144
  self,
@@ -1,2 +0,0 @@
1
- from .logs import get_logger
2
- from .traces import get_current_diagnostic_id, get_tracer
@@ -1,33 +0,0 @@
1
- import logging
2
-
3
- # Suppress verbose logging from Azure SDK
4
- logging.getLogger("azure.core.pipeline.policies.http_logging_policy").setLevel(
5
- logging.WARNING
6
- )
7
- logging.getLogger("azure.data.tables").setLevel(logging.WARNING)
8
- logging.getLogger("azure.storage.blob").setLevel(logging.WARNING)
9
- logging.getLogger("azure.servicebus").setLevel(logging.WARNING)
10
- logging.getLogger("uamqp").setLevel(logging.WARNING)
11
- logging.getLogger("azure.identity").setLevel(logging.WARNING)
12
- # Suppress Azure Functions host/worker logging (e.g., trigger details)
13
- logging.getLogger("azure_functions_worker").setLevel(logging.WARNING)
14
- logging.getLogger("azure.functions").setLevel(logging.WARNING)
15
- # Suppress Azure Monitor exporter logging (e.g., transmission success messages)
16
- logging.getLogger("azure.monitor.opentelemetry.exporter").setLevel(logging.WARNING)
17
-
18
-
19
- def get_logger(name: str):
20
- """Get a logger instance.
21
-
22
- Logs are correlated with the correct function invocation via Azure Functions'
23
- thread-local storage invocation_id, which is set by the monitor decorator.
24
-
25
- Args:
26
- name: Logger name, typically __name__
27
-
28
- Returns:
29
- A configured logger instance
30
- """
31
- logger = logging.getLogger(name)
32
- logger.setLevel(logging.INFO)
33
- return logger
@@ -1,142 +0,0 @@
1
- import logging
2
- import os
3
- from contextlib import contextmanager
4
-
5
- from azure.core.settings import settings
6
- from azure.monitor.opentelemetry.exporter import AzureMonitorTraceExporter
7
- from opentelemetry import context, trace
8
- from opentelemetry.sdk.resources import SERVICE_NAME, Resource
9
- from opentelemetry.sdk.trace import ReadableSpan, SpanProcessor, TracerProvider
10
- from opentelemetry.sdk.trace.export import BatchSpanProcessor
11
- from opentelemetry.sdk.trace.sampling import ALWAYS_ON
12
- from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
13
-
14
- # Enable OpenTelemetry tracing for Azure SDK (including Service Bus)
15
- # This must be set before any Azure SDK clients are created
16
- settings.tracing_implementation = "opentelemetry"
17
-
18
- logger = logging.getLogger(__name__)
19
-
20
- _TRACING_IS_CONFIGURED = False
21
-
22
- # Span name prefixes to allow from Azure SDK (Service Bus for function chaining)
23
- _ALLOWED_AZURE_SPAN_NAMES = frozenset({"ServiceBus.message"})
24
-
25
-
26
- class AzureSDKTraceFilter(SpanProcessor):
27
- """SpanProcessor that filters out noisy Azure SDK traces.
28
-
29
- Allows:
30
- - All non-Azure SDK spans (custom/user traces)
31
- - Service Bus spans (for tracing function chains)
32
-
33
- Drops:
34
- - Other Azure SDK spans (HTTP client, blob storage, etc.)
35
- """
36
-
37
- def __init__(self, wrapped_processor: SpanProcessor):
38
- self.wrapped_processor = wrapped_processor
39
-
40
- def on_start(
41
- self, span: ReadableSpan, parent_context: context.Context = None
42
- ) -> None:
43
- self.wrapped_processor.on_start(span, parent_context)
44
-
45
- def on_end(self, span: ReadableSpan) -> None:
46
- scope = span.instrumentation_scope
47
- if scope and scope.name:
48
- # Check if it's an Azure SDK span (uses azure.core.tracing wrapper)
49
- if scope.name.startswith("azure."):
50
- # Allow only specific Service Bus span names
51
- span_name = getattr(span, "name", "") or ""
52
- if span_name in _ALLOWED_AZURE_SPAN_NAMES:
53
- self.wrapped_processor.on_end(span)
54
- # Drop other Azure SDK spans
55
- return
56
-
57
- # Pass through: non-Azure SDK spans (custom traces)
58
- self.wrapped_processor.on_end(span)
59
-
60
- def shutdown(self) -> None:
61
- self.wrapped_processor.shutdown()
62
-
63
- def force_flush(self, timeout_millis: int = 30000) -> bool:
64
- return self.wrapped_processor.force_flush(timeout_millis)
65
-
66
-
67
- def configure_tracing():
68
- """Configure OpenTelemetry tracing with Azure Monitor exporter.
69
-
70
- This sets up tracing for Service Bus and custom spans.
71
- Other Azure SDK traces (HTTP, blob, etc.) are filtered out.
72
-
73
- Should be called once before any tracing is performed.
74
- """
75
- global _TRACING_IS_CONFIGURED
76
- if _TRACING_IS_CONFIGURED:
77
- return
78
-
79
- # Set up tracer provider with Azure Function resource info
80
- resource = Resource.create({SERVICE_NAME: os.getenv("WEBSITE_SITE_NAME")})
81
- provider = TracerProvider(
82
- sampler=ALWAYS_ON,
83
- resource=resource,
84
- )
85
- trace.set_tracer_provider(provider)
86
-
87
- # Set up Azure Monitor trace exporter with filter
88
- try:
89
- trace_exporter = AzureMonitorTraceExporter()
90
- span_processor = BatchSpanProcessor(trace_exporter)
91
- filtered_processor = AzureSDKTraceFilter(span_processor)
92
- provider.add_span_processor(filtered_processor)
93
- except ValueError:
94
- logger.warning(
95
- "Cannot set up tracing to App Insights, as no instrumentation key is set."
96
- )
97
-
98
- _TRACING_IS_CONFIGURED = True
99
-
100
-
101
- @contextmanager
102
- def set_trace_context(trace_parent: str, trace_state: str = ""):
103
- """Context manager for setting the trace context.
104
-
105
- Args:
106
- trace_parent (str): Trace parent ID
107
- trace_state (str, optional): Trace state. Defaults to "".
108
- """
109
- carrier = {"traceparent": trace_parent, "tracestate": trace_state}
110
- ctx = TraceContextTextMapPropagator().extract(carrier=carrier)
111
-
112
- token = context.attach(ctx)
113
- try:
114
- yield
115
- finally:
116
- context.detach(token)
117
-
118
-
119
- def get_tracer(name: str):
120
- tracer = trace.get_tracer(name)
121
- return tracer
122
-
123
-
124
- def get_current_diagnostic_id() -> str:
125
- """Gets diagnostic id from current span
126
-
127
- The diagnostic id is a concatenation of operation-id and parent-id
128
-
129
- Returns:
130
- str: diagnostic id
131
- """
132
- span = trace.get_current_span()
133
-
134
- if not span.is_recording():
135
- return ""
136
-
137
- operation_id = "{:016x}".format(span.context.trace_id)
138
- parent_id = "{:016x}".format(span.context.span_id)
139
-
140
- diagnostic_id = f"00-{operation_id}-{parent_id}-01"
141
-
142
- return diagnostic_id