warpzone-sdk 15.0.0.dev7__tar.gz → 15.0.0.dev9__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 (59) hide show
  1. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/PKG-INFO +1 -1
  2. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/pyproject.toml +1 -1
  3. warpzone_sdk-15.0.0.dev9/warpzone/function/monitor.py +57 -0
  4. warpzone_sdk-15.0.0.dev9/warpzone/monitor/traces.py +142 -0
  5. warpzone_sdk-15.0.0.dev7/warpzone/function/monitor.py +0 -93
  6. warpzone_sdk-15.0.0.dev7/warpzone/monitor/traces.py +0 -112
  7. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/README.md +0 -0
  8. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/__init__.py +0 -0
  9. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/blobstorage/__init__.py +0 -0
  10. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/blobstorage/client.py +0 -0
  11. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/db/__init__.py +0 -0
  12. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/db/client.py +0 -0
  13. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/deltastorage/__init__.py +0 -0
  14. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/deltastorage/data_types.py +0 -0
  15. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/deltastorage/generated_columns.py +0 -0
  16. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/deltastorage/lock_client.py +0 -0
  17. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/deltastorage/schema.py +0 -0
  18. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/deltastorage/slicing.py +0 -0
  19. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/deltastorage/store.py +0 -0
  20. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/deltastorage/table.py +0 -0
  21. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/enums/__init__.py +0 -0
  22. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/enums/topicenum.py +0 -0
  23. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/function/__init__.py +0 -0
  24. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/function/checks.py +0 -0
  25. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/function/functionize.py +0 -0
  26. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/function/integrations.py +0 -0
  27. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/function/process.py +0 -0
  28. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/function/processors/__init__.py +0 -0
  29. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/function/processors/dependencies.py +0 -0
  30. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/function/processors/outputs.py +0 -0
  31. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/function/processors/triggers.py +0 -0
  32. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/function/signature.py +0 -0
  33. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/function/types.py +0 -0
  34. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/healthchecks/__init__.py +0 -0
  35. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/healthchecks/model.py +0 -0
  36. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/monitor/__init__.py +0 -0
  37. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/monitor/logs.py +0 -0
  38. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/servicebus/data/__init__.py +0 -0
  39. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/servicebus/data/client.py +0 -0
  40. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/servicebus/events/__init__.py +0 -0
  41. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/servicebus/events/client.py +0 -0
  42. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/servicebus/events/triggers.py +0 -0
  43. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/tablestorage/db/__init__.py +0 -0
  44. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/tablestorage/db/base_client.py +0 -0
  45. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/tablestorage/db/client.py +0 -0
  46. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/tablestorage/db/table_config.py +0 -0
  47. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/tablestorage/tables/__init__.py +0 -0
  48. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/tablestorage/tables/client.py +0 -0
  49. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/tablestorage/tables/entities.py +0 -0
  50. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/tablestorage/tables/helpers.py +0 -0
  51. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/testing/__init__.py +0 -0
  52. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/testing/assertions.py +0 -0
  53. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/testing/data.py +0 -0
  54. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/testing/matchers.py +0 -0
  55. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/tools/__init__.py +0 -0
  56. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/tools/copy.py +0 -0
  57. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/transform/__init__.py +0 -0
  58. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/warpzone/transform/data.py +0 -0
  59. {warpzone_sdk-15.0.0.dev7 → warpzone_sdk-15.0.0.dev9}/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.dev7
3
+ Version: 15.0.0.dev9
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "warpzone-sdk"
3
- version = "15.0.0.dev7"
3
+ version = "15.0.0.dev9"
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"
@@ -0,0 +1,57 @@
1
+ import inspect
2
+ from contextlib import contextmanager
3
+ from typing import Callable
4
+
5
+ import azure.functions as func
6
+
7
+ 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()
12
+
13
+ SUBJECT_IDENTIFIER = "<Subject>"
14
+
15
+
16
+ @contextmanager
17
+ def run_in_trace_context(context: func.Context):
18
+ trace_context = context.trace_context
19
+ with traces.set_trace_context(
20
+ trace_context.trace_parent, trace_context.trace_state
21
+ ):
22
+ yield
23
+
24
+
25
+ def monitor(main: SingleArgumentCallable) -> Callable:
26
+ """Wrap Azure function with logging and tracing
27
+ configured for monitoring in App Insights.
28
+
29
+ Args:
30
+ f (SingleArgumentCallable): Azure function to be wrapped
31
+
32
+ Returns:
33
+ Callable: Azure function with
34
+ - argument
35
+ name: "arg"
36
+ description: argument of the original function
37
+ - argument
38
+ name: "context"
39
+ description: Azure function context
40
+ - return value
41
+ description: return value of original function
42
+ """
43
+
44
+ async def wrapper_async(arg, context: func.Context):
45
+ with run_in_trace_context(context):
46
+ result = await main(arg)
47
+ return result
48
+
49
+ def wrapper(arg, context: func.Context):
50
+ with run_in_trace_context(context):
51
+ result = main(arg)
52
+ return result
53
+
54
+ if inspect.iscoroutinefunction(main):
55
+ return wrapper_async
56
+ else:
57
+ return wrapper
@@ -0,0 +1,142 @@
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
@@ -1,93 +0,0 @@
1
- import inspect
2
- import threading
3
- from contextlib import contextmanager
4
- from typing import Callable
5
-
6
- import azure.functions as func
7
- from azure.monitor.opentelemetry import configure_azure_monitor
8
- from opentelemetry import trace
9
-
10
- from warpzone.function.types import SingleArgumentCallable
11
- from warpzone.monitor import traces
12
-
13
- # Thread-safe lazy initialization of Azure Monitor
14
- _azure_monitor_configured = False
15
- _azure_monitor_lock = threading.Lock()
16
-
17
-
18
- def _ensure_azure_monitor_configured():
19
- """Ensure Azure Monitor is configured exactly once, thread-safely."""
20
- global _azure_monitor_configured
21
- if _azure_monitor_configured:
22
- return
23
-
24
- with _azure_monitor_lock:
25
- # Double-check after acquiring lock
26
- if _azure_monitor_configured:
27
- return
28
-
29
- # Disable OpenTelemetry logging instrumentation - let Azure Functions
30
- # handle log correlation via its native thread-local storage mechanism.
31
- # This prevents conflicts between OpenTelemetry's context-based correlation
32
- # and Azure Functions' invocation_id-based correlation which can cause
33
- # logs to leak between concurrent function invocations.
34
- configure_azure_monitor(
35
- disable_logging=True,
36
- )
37
-
38
- # Apply trace filtering to suppress all Azure SDK traces except Service Bus
39
- tracer_provider = trace.get_tracer_provider()
40
- if hasattr(tracer_provider, "_active_span_processor"):
41
- original_processor = tracer_provider._active_span_processor
42
- filtered_processor = traces.AzureSDKTraceFilter(original_processor)
43
- tracer_provider._active_span_processor = filtered_processor
44
-
45
- _azure_monitor_configured = True
46
-
47
-
48
- SUBJECT_IDENTIFIER = "<Subject>"
49
-
50
-
51
- @contextmanager
52
- def run_in_trace_context(context: func.Context):
53
- trace_context = context.trace_context
54
- span_name = context.function_name or "azure_function"
55
- with traces.set_trace_context(trace_context.trace_parent, span_name=span_name):
56
- yield
57
-
58
-
59
- def monitor(main: SingleArgumentCallable) -> Callable:
60
- """Wrap Azure function with logging and tracing
61
- configured for monitoring in App Insights.
62
-
63
- Args:
64
- f (SingleArgumentCallable): Azure function to be wrapped
65
-
66
- Returns:
67
- Callable: Azure function with
68
- - argument
69
- name: "arg"
70
- description: argument of the original function
71
- - argument
72
- name: "context"
73
- description: Azure function context
74
- - return value
75
- description: return value of original function
76
- """
77
- # Ensure Azure Monitor is configured before any function runs
78
- _ensure_azure_monitor_configured()
79
-
80
- async def wrapper_async(arg, context: func.Context):
81
- with run_in_trace_context(context):
82
- result = await main(arg)
83
- return result
84
-
85
- def wrapper(arg, context: func.Context):
86
- with run_in_trace_context(context):
87
- result = main(arg)
88
- return result
89
-
90
- if inspect.iscoroutinefunction(main):
91
- return wrapper_async
92
- else:
93
- return wrapper
@@ -1,112 +0,0 @@
1
- from contextlib import contextmanager
2
-
3
- from azure.core.settings import settings
4
- from opentelemetry import context, trace
5
- from opentelemetry.sdk.trace import ReadableSpan, SpanProcessor
6
- from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
7
-
8
- settings.tracing_implementation = "opentelemetry"
9
-
10
-
11
- class AzureSDKTraceFilter(SpanProcessor):
12
- """Custom SpanProcessor to filter out Azure SDK traces except Service Bus.
13
-
14
- It drops spans from Azure SDK libraries except Service Bus messages,
15
- preventing them from being exported to Azure Monitor.
16
- """
17
-
18
- def __init__(self, wrapped_processor: SpanProcessor):
19
- """Initialize with the actual processor to wrap.
20
-
21
- Args:
22
- wrapped_processor: The underlying processor (e.g., BatchSpanProcessor)
23
- """
24
- self.wrapped_processor = wrapped_processor
25
-
26
- def on_start(
27
- self, span: ReadableSpan, parent_context: context.Context = None
28
- ) -> None:
29
- """Called when a span is started."""
30
- self.wrapped_processor.on_start(span, parent_context)
31
-
32
- def on_end(self, span: ReadableSpan) -> None:
33
- """Called when a span is ended. Filter based on span attributes."""
34
- # Safely get span name, handling None or mock objects
35
- span_name = getattr(span, "name", None)
36
- if span_name is None or not isinstance(span_name, str):
37
- # Pass through spans without valid names (e.g., in test mocks)
38
- self.wrapped_processor.on_end(span)
39
- return
40
-
41
- # Check if service bus span - always allow
42
- if "servicebus.message" in span_name.lower():
43
- self.wrapped_processor.on_end(span)
44
- return
45
-
46
- # Check if this is an Azure SDK span we want to suppress
47
- instrumentation_scope = span.instrumentation_scope
48
- if instrumentation_scope and instrumentation_scope.name:
49
- # Suppress spans from Azure SDK libraries
50
- if instrumentation_scope.name.startswith("azure."):
51
- return # Drop this span
52
-
53
- # Pass through all other spans
54
- self.wrapped_processor.on_end(span)
55
-
56
- def shutdown(self) -> None:
57
- """Shutdown the wrapped processor."""
58
- self.wrapped_processor.shutdown()
59
-
60
- def force_flush(self, timeout_millis: int = 30000) -> bool:
61
- """Force flush the wrapped processor."""
62
- return self.wrapped_processor.force_flush(timeout_millis)
63
-
64
-
65
- @contextmanager
66
- def set_trace_context(trace_parent: str, span_name: str = "function_execution"):
67
- """Context manager for setting the trace context.
68
-
69
- Attaches the propagated trace context and creates a child span for tracing.
70
- Note: Log correlation is handled by Azure Functions' native mechanism,
71
- not OpenTelemetry, so this primarily affects span/dependency tracking.
72
-
73
- Args:
74
- trace_parent (str): Trace parent ID from the incoming request
75
- span_name (str): Name for the span. Defaults to "function_execution".
76
- """
77
- carrier = {"traceparent": trace_parent}
78
- ctx = TraceContextTextMapPropagator().extract(carrier=carrier)
79
-
80
- token = context.attach(ctx)
81
- try:
82
- tracer = trace.get_tracer(__name__)
83
- with tracer.start_as_current_span(span_name):
84
- yield
85
- finally:
86
- context.detach(token)
87
-
88
-
89
- def get_tracer(name: str):
90
- tracer = trace.get_tracer(name)
91
- return tracer
92
-
93
-
94
- def get_current_diagnostic_id() -> str:
95
- """Gets diagnostic id from current span
96
-
97
- The diagnostic id is a concatenation of operation-id and parent-id
98
-
99
- Returns:
100
- str: diagnostic id
101
- """
102
- span = trace.get_current_span()
103
-
104
- if not span.is_recording():
105
- return ""
106
-
107
- operation_id = "{:016x}".format(span.context.trace_id)
108
- parent_id = "{:016x}".format(span.context.span_id)
109
-
110
- diagnostic_id = f"00-{operation_id}-{parent_id}-01"
111
-
112
- return diagnostic_id