graftcode-telemetry-appinsights 1.0.0__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 (20) hide show
  1. graftcode_telemetry_appinsights-1.0.0/PKG-INFO +118 -0
  2. graftcode_telemetry_appinsights-1.0.0/README.md +100 -0
  3. graftcode_telemetry_appinsights-1.0.0/graftcode_telemetry_appinsights/GraftCodeAppInsightsConnector.py +29 -0
  4. graftcode_telemetry_appinsights-1.0.0/graftcode_telemetry_appinsights/GraftCodeAppInsightsConnectorOptions.py +46 -0
  5. graftcode_telemetry_appinsights-1.0.0/graftcode_telemetry_appinsights/GraftCodeAppInsightsConnectorPipeline.py +243 -0
  6. graftcode_telemetry_appinsights-1.0.0/graftcode_telemetry_appinsights/GraftCodeAppInsightsLogger.py +152 -0
  7. graftcode_telemetry_appinsights-1.0.0/graftcode_telemetry_appinsights/GraftCodeAppInsightsRequestTelemetryMiddleware.py +123 -0
  8. graftcode_telemetry_appinsights-1.0.0/graftcode_telemetry_appinsights/MessageTemplate.py +70 -0
  9. graftcode_telemetry_appinsights-1.0.0/graftcode_telemetry_appinsights/RateLimitedSampler.py +48 -0
  10. graftcode_telemetry_appinsights-1.0.0/graftcode_telemetry_appinsights/__init__.py +15 -0
  11. graftcode_telemetry_appinsights-1.0.0/graftcode_telemetry_appinsights.egg-info/PKG-INFO +118 -0
  12. graftcode_telemetry_appinsights-1.0.0/graftcode_telemetry_appinsights.egg-info/SOURCES.txt +18 -0
  13. graftcode_telemetry_appinsights-1.0.0/graftcode_telemetry_appinsights.egg-info/dependency_links.txt +1 -0
  14. graftcode_telemetry_appinsights-1.0.0/graftcode_telemetry_appinsights.egg-info/requires.txt +10 -0
  15. graftcode_telemetry_appinsights-1.0.0/graftcode_telemetry_appinsights.egg-info/top_level.txt +1 -0
  16. graftcode_telemetry_appinsights-1.0.0/pyproject.toml +33 -0
  17. graftcode_telemetry_appinsights-1.0.0/setup.cfg +4 -0
  18. graftcode_telemetry_appinsights-1.0.0/tests/test_GraftCodeAppInsightsConnector.py +154 -0
  19. graftcode_telemetry_appinsights-1.0.0/tests/test_GraftCodeAppInsightsLogger.py +121 -0
  20. graftcode_telemetry_appinsights-1.0.0/tests/test_GraftCodeAppInsightsRequestTelemetryMiddleware.py +126 -0
@@ -0,0 +1,118 @@
1
+ Metadata-Version: 2.4
2
+ Name: graftcode-telemetry-appinsights
3
+ Version: 1.0.0
4
+ Summary: Application Insights connector for Graftcode / Hypertube telemetry in Python
5
+ Author-email: "Graftcode S.A." <support@graftcode.com>
6
+ License: MIT
7
+ Requires-Python: >=3.8
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: opentelemetry-api>=1.39
10
+ Requires-Dist: opentelemetry-sdk>=1.39
11
+ Requires-Dist: azure-monitor-opentelemetry-exporter>=1.0.0b40
12
+ Requires-Dist: graftcode-instrumentation>=1.0.0
13
+ Requires-Dist: hypertube-python-sdk>=3.0.0
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest; extra == "dev"
16
+ Requires-Dist: starlette; extra == "dev"
17
+ Requires-Dist: httpx; extra == "dev"
18
+
19
+ # GraftCode App Insights Connector (Python)
20
+
21
+ Configures Application Insights for an application, together with the Graft bridge spans, in one call. It is a port of the .NET (`GraftCode.Netcore.Telemetry.AppInsightsConnector`) and Java (`com.graftcode.telemetry.appinsights`) connectors.
22
+
23
+ ```python
24
+ from graftcode_telemetry_appinsights import GraftCodeAppInsightsConnector
25
+
26
+ with GraftCodeAppInsightsConnector.start("MyService") as telemetry:
27
+ span = telemetry.start_span("MyOperation")
28
+ telemetry.logger.info("My operation started {Step}", "one")
29
+ span.end()
30
+ ```
31
+
32
+ The connector reads `APPLICATIONINSIGHTS_CONNECTION_STRING` and exports traces and logs with the Azure Monitor exporter (`azure-monitor-opentelemetry-exporter`). It also attaches `graftcode-instrumentation`, so Hypertube calls produce `Graftcode` spans and Hypertube log lines go to the same pipeline. You can configure an explicit connection string with a callback or an options object:
33
+
34
+ ```python
35
+ from graftcode_telemetry_appinsights import GraftCodeAppInsightsConnector, GraftCodeAppInsightsConnectorOptions
36
+
37
+ def configure(options):
38
+ options.service_name = "MyService"
39
+ options.connection_string = settings["ApplicationInsights:ConnectionString"]
40
+
41
+ telemetry = GraftCodeAppInsightsConnector.start(configure)
42
+ telemetry = GraftCodeAppInsightsConnector.start(GraftCodeAppInsightsConnectorOptions(service_name="MyService"))
43
+ ```
44
+
45
+ | Option | Default |
46
+ |--------|---------|
47
+ | `service_name` | required; becomes `service.name` (the App Insights cloud role) and the tracer/logger name |
48
+ | `connection_string` | falls back to `APPLICATIONINSIGHTS_CONNECTION_STRING` |
49
+ | `minimum_log_level` | `LogsLevel.Info`; applies to the connector logger and to forwarded Hypertube lines |
50
+ | `traces_per_second` | `None` (every trace is kept) |
51
+ | `enable_console_logging` | `True` |
52
+
53
+ ## Pipeline (`GraftCodeAppInsightsConnectorPipeline`)
54
+
55
+ | Member | Purpose |
56
+ |--------|---------|
57
+ | `tracer`, `start_span(name, kind=INTERNAL, context=None)` | spans for the application |
58
+ | `logger` | `GraftCodeAppInsightsLogger` (`trace`, `debug`, `info`, `warning`, `error`, `log`) |
59
+ | `instrumentation` | the attached `GraftcodeInstrumentation` |
60
+ | `force_flush(timeout_millis=5000)` | flushes traces and logs |
61
+ | `close()` / context manager | flushes, detaches the instrumentation, and shuts the providers down; calling it twice is safe |
62
+ | `is_exporting_to_app_insights()` | `False` when no connection string was resolved |
63
+
64
+ The OpenTelemetry providers are not registered as globals, which matches the Java connector. Use `pipeline.tracer` or `pipeline.start_span` for application spans.
65
+
66
+ ## Logger
67
+
68
+ The logger uses the same message templates as the .NET and Java connectors:
69
+
70
+ - `{Name}` placeholders are filled positionally. `{{` and `}}` escape braces, and format specifiers (`{Count:D3}`) are ignored. `None` renders as `(null)`.
71
+ - Each placeholder becomes a log attribute. The raw template is stored as `{OriginalFormat}`.
72
+ - Records are correlated with the current span.
73
+ - Console output follows the .NET console layout:
74
+
75
+ ```
76
+ info: TestGgPythonWebApi[0]
77
+ Caller after Graft call Result=fetched
78
+ ```
79
+
80
+ To log an exception, pass it first: `logger.error(exception, "failed {Step}", "fetch")`. It is attached as `exception.type`, `exception.message` and `exception.stacktrace`.
81
+
82
+ Hypertube's own lines reach App Insights through the standard-library logger `Hypertube`. The pipeline sets that logger's level from `minimum_log_level` and adds a handler that emits through the OpenTelemetry logs API (instrumentation scope `Hypertube`).
83
+
84
+ ## Request telemetry (ASGI)
85
+
86
+ Add the middleware to FastAPI or Starlette. It plays the role of `UseGraftCodeAppInsightsConnectorRequestTelemetry` in .NET and the servlet filter in Java:
87
+
88
+ ```python
89
+ app.add_middleware(GraftCodeAppInsightsRequestTelemetryMiddleware, telemetry=telemetry)
90
+ ```
91
+
92
+ Each HTTP request gets a `SERVER` span that is current while the endpoint runs:
93
+
94
+ - **Parent:** the incoming W3C `traceparent`, when present.
95
+ - **Attributes:** `http.method`, `http.target` and `http.status_code`. A status of 500 or higher marks the span as failed, and exceptions are recorded and re-raised. The path goes in `http.target` rather than `http.route` (as .NET and Java use) because the Python Azure Monitor exporter replaces the request name with `METHOD route` whenever `http.route` or a full URL is present; with `http.target` the request keeps the controller-based name, as in .NET and Java.
96
+ - **Name:** `{ServiceName}.{controller}` when the routed endpoint belongs to a class, for example a `FetchController.fetch` endpoint gives `TestGgPythonWebApi.Fetch`. An explicit name can be set in `scope["state"]["graft.controller"]`. Otherwise the name is `{ServiceName} {METHOD} {path}`.
97
+
98
+ Synchronous endpoints run in Starlette's thread pool with a copy of the context, so Graft calls made there share the request's trace.
99
+
100
+ ## Sampling
101
+
102
+ Every trace is kept by default (`ALWAYS_ON`); the reason is the same as in .NET and Java. Set `traces_per_second` to opt into a token-bucket `RateLimitedSampler`.
103
+
104
+ ## Flushing
105
+
106
+ The pipeline flushes on `close()`, at interpreter exit (`atexit`), and on unhandled exceptions; both `sys.excepthook` and `threading.excepthook` are chained. Short-lived processes should still call `force_flush()` before exiting.
107
+
108
+ ## Diagnosing missing telemetry
109
+
110
+ When no connection string is resolved, `is_exporting_to_app_insights()` returns `False` and telemetry stays local. The connector then logs `Application Insights telemetry for {ServiceName} is disabled: no connection string was configured. ...`.
111
+
112
+ ## Tests
113
+
114
+ ```powershell
115
+ $env:PYTHONPATH = "<HYPERTUBE>\src\python;<graftcode-extensions>\observability\Instrumentation\python"
116
+ python -m pip install -e .[dev]
117
+ python -m pytest
118
+ ```
@@ -0,0 +1,100 @@
1
+ # GraftCode App Insights Connector (Python)
2
+
3
+ Configures Application Insights for an application, together with the Graft bridge spans, in one call. It is a port of the .NET (`GraftCode.Netcore.Telemetry.AppInsightsConnector`) and Java (`com.graftcode.telemetry.appinsights`) connectors.
4
+
5
+ ```python
6
+ from graftcode_telemetry_appinsights import GraftCodeAppInsightsConnector
7
+
8
+ with GraftCodeAppInsightsConnector.start("MyService") as telemetry:
9
+ span = telemetry.start_span("MyOperation")
10
+ telemetry.logger.info("My operation started {Step}", "one")
11
+ span.end()
12
+ ```
13
+
14
+ The connector reads `APPLICATIONINSIGHTS_CONNECTION_STRING` and exports traces and logs with the Azure Monitor exporter (`azure-monitor-opentelemetry-exporter`). It also attaches `graftcode-instrumentation`, so Hypertube calls produce `Graftcode` spans and Hypertube log lines go to the same pipeline. You can configure an explicit connection string with a callback or an options object:
15
+
16
+ ```python
17
+ from graftcode_telemetry_appinsights import GraftCodeAppInsightsConnector, GraftCodeAppInsightsConnectorOptions
18
+
19
+ def configure(options):
20
+ options.service_name = "MyService"
21
+ options.connection_string = settings["ApplicationInsights:ConnectionString"]
22
+
23
+ telemetry = GraftCodeAppInsightsConnector.start(configure)
24
+ telemetry = GraftCodeAppInsightsConnector.start(GraftCodeAppInsightsConnectorOptions(service_name="MyService"))
25
+ ```
26
+
27
+ | Option | Default |
28
+ |--------|---------|
29
+ | `service_name` | required; becomes `service.name` (the App Insights cloud role) and the tracer/logger name |
30
+ | `connection_string` | falls back to `APPLICATIONINSIGHTS_CONNECTION_STRING` |
31
+ | `minimum_log_level` | `LogsLevel.Info`; applies to the connector logger and to forwarded Hypertube lines |
32
+ | `traces_per_second` | `None` (every trace is kept) |
33
+ | `enable_console_logging` | `True` |
34
+
35
+ ## Pipeline (`GraftCodeAppInsightsConnectorPipeline`)
36
+
37
+ | Member | Purpose |
38
+ |--------|---------|
39
+ | `tracer`, `start_span(name, kind=INTERNAL, context=None)` | spans for the application |
40
+ | `logger` | `GraftCodeAppInsightsLogger` (`trace`, `debug`, `info`, `warning`, `error`, `log`) |
41
+ | `instrumentation` | the attached `GraftcodeInstrumentation` |
42
+ | `force_flush(timeout_millis=5000)` | flushes traces and logs |
43
+ | `close()` / context manager | flushes, detaches the instrumentation, and shuts the providers down; calling it twice is safe |
44
+ | `is_exporting_to_app_insights()` | `False` when no connection string was resolved |
45
+
46
+ The OpenTelemetry providers are not registered as globals, which matches the Java connector. Use `pipeline.tracer` or `pipeline.start_span` for application spans.
47
+
48
+ ## Logger
49
+
50
+ The logger uses the same message templates as the .NET and Java connectors:
51
+
52
+ - `{Name}` placeholders are filled positionally. `{{` and `}}` escape braces, and format specifiers (`{Count:D3}`) are ignored. `None` renders as `(null)`.
53
+ - Each placeholder becomes a log attribute. The raw template is stored as `{OriginalFormat}`.
54
+ - Records are correlated with the current span.
55
+ - Console output follows the .NET console layout:
56
+
57
+ ```
58
+ info: TestGgPythonWebApi[0]
59
+ Caller after Graft call Result=fetched
60
+ ```
61
+
62
+ To log an exception, pass it first: `logger.error(exception, "failed {Step}", "fetch")`. It is attached as `exception.type`, `exception.message` and `exception.stacktrace`.
63
+
64
+ Hypertube's own lines reach App Insights through the standard-library logger `Hypertube`. The pipeline sets that logger's level from `minimum_log_level` and adds a handler that emits through the OpenTelemetry logs API (instrumentation scope `Hypertube`).
65
+
66
+ ## Request telemetry (ASGI)
67
+
68
+ Add the middleware to FastAPI or Starlette. It plays the role of `UseGraftCodeAppInsightsConnectorRequestTelemetry` in .NET and the servlet filter in Java:
69
+
70
+ ```python
71
+ app.add_middleware(GraftCodeAppInsightsRequestTelemetryMiddleware, telemetry=telemetry)
72
+ ```
73
+
74
+ Each HTTP request gets a `SERVER` span that is current while the endpoint runs:
75
+
76
+ - **Parent:** the incoming W3C `traceparent`, when present.
77
+ - **Attributes:** `http.method`, `http.target` and `http.status_code`. A status of 500 or higher marks the span as failed, and exceptions are recorded and re-raised. The path goes in `http.target` rather than `http.route` (as .NET and Java use) because the Python Azure Monitor exporter replaces the request name with `METHOD route` whenever `http.route` or a full URL is present; with `http.target` the request keeps the controller-based name, as in .NET and Java.
78
+ - **Name:** `{ServiceName}.{controller}` when the routed endpoint belongs to a class, for example a `FetchController.fetch` endpoint gives `TestGgPythonWebApi.Fetch`. An explicit name can be set in `scope["state"]["graft.controller"]`. Otherwise the name is `{ServiceName} {METHOD} {path}`.
79
+
80
+ Synchronous endpoints run in Starlette's thread pool with a copy of the context, so Graft calls made there share the request's trace.
81
+
82
+ ## Sampling
83
+
84
+ Every trace is kept by default (`ALWAYS_ON`); the reason is the same as in .NET and Java. Set `traces_per_second` to opt into a token-bucket `RateLimitedSampler`.
85
+
86
+ ## Flushing
87
+
88
+ The pipeline flushes on `close()`, at interpreter exit (`atexit`), and on unhandled exceptions; both `sys.excepthook` and `threading.excepthook` are chained. Short-lived processes should still call `force_flush()` before exiting.
89
+
90
+ ## Diagnosing missing telemetry
91
+
92
+ When no connection string is resolved, `is_exporting_to_app_insights()` returns `False` and telemetry stays local. The connector then logs `Application Insights telemetry for {ServiceName} is disabled: no connection string was configured. ...`.
93
+
94
+ ## Tests
95
+
96
+ ```powershell
97
+ $env:PYTHONPATH = "<HYPERTUBE>\src\python;<graftcode-extensions>\observability\Instrumentation\python"
98
+ python -m pip install -e .[dev]
99
+ python -m pytest
100
+ ```
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Callable, Union
4
+
5
+ from graftcode_telemetry_appinsights.GraftCodeAppInsightsConnectorOptions import GraftCodeAppInsightsConnectorOptions
6
+ from graftcode_telemetry_appinsights.GraftCodeAppInsightsConnectorPipeline import GraftCodeAppInsightsConnectorPipeline
7
+
8
+ ConfigureCallback = Callable[[GraftCodeAppInsightsConnectorOptions], None]
9
+
10
+
11
+ class GraftCodeAppInsightsConnector:
12
+
13
+ @staticmethod
14
+ def start(
15
+ service_name_or_configure: Union[str, ConfigureCallback, GraftCodeAppInsightsConnectorOptions, None],
16
+ ) -> GraftCodeAppInsightsConnectorPipeline:
17
+ if service_name_or_configure is None:
18
+ raise ValueError("start requires a service name, a configure callback or options")
19
+
20
+ if isinstance(service_name_or_configure, GraftCodeAppInsightsConnectorOptions):
21
+ return GraftCodeAppInsightsConnectorPipeline(service_name_or_configure)
22
+
23
+ options = GraftCodeAppInsightsConnectorOptions()
24
+ if callable(service_name_or_configure):
25
+ service_name_or_configure(options)
26
+ else:
27
+ options.service_name = service_name_or_configure
28
+
29
+ return GraftCodeAppInsightsConnectorPipeline(options)
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from typing import Callable, Optional, Union
5
+
6
+ from hypertube.utils.logging.LogsLevel import LogsLevel
7
+
8
+
9
+ class GraftCodeAppInsightsConnectorOptions:
10
+ CONNECTION_STRING_VARIABLE = "APPLICATIONINSIGHTS_CONNECTION_STRING"
11
+
12
+ def __init__(
13
+ self,
14
+ service_name: str = "",
15
+ connection_string: Optional[str] = None,
16
+ minimum_log_level: LogsLevel = LogsLevel.Info,
17
+ traces_per_second: Optional[float] = None,
18
+ enable_console_logging: bool = True,
19
+ ) -> None:
20
+ self.service_name = service_name
21
+ self.connection_string = connection_string
22
+ self.minimum_log_level = minimum_log_level
23
+ self.traces_per_second = traces_per_second
24
+ self.enable_console_logging = enable_console_logging
25
+ self._environment: Callable[[str], Optional[str]] = os.environ.get
26
+
27
+ def resolve_connection_string(self) -> Optional[str]:
28
+ if not GraftCodeAppInsightsConnectorOptions._is_blank(self.connection_string):
29
+ return self.connection_string
30
+
31
+ environment_value = self._environment(GraftCodeAppInsightsConnectorOptions.CONNECTION_STRING_VARIABLE)
32
+ return None if GraftCodeAppInsightsConnectorOptions._is_blank(environment_value) else environment_value
33
+
34
+ def validate(self) -> None:
35
+ if GraftCodeAppInsightsConnectorOptions._is_blank(self.service_name):
36
+ raise ValueError("service_name must be configured for Azure Monitor telemetry.")
37
+
38
+ if self.minimum_log_level is None:
39
+ raise ValueError("minimum_log_level must not be None.")
40
+
41
+ if self.traces_per_second is not None and not self.traces_per_second > 0:
42
+ raise ValueError("traces_per_second must be greater than zero.")
43
+
44
+ @staticmethod
45
+ def _is_blank(value: Union[str, None]) -> bool:
46
+ return value is None or not str(value).strip()
@@ -0,0 +1,243 @@
1
+ from __future__ import annotations
2
+
3
+ import atexit
4
+ import logging
5
+ import sys
6
+ import threading
7
+ import traceback
8
+ from types import TracebackType
9
+ from typing import Optional, Tuple, Type
10
+
11
+ from opentelemetry import context as otel_context
12
+ from opentelemetry._logs import LogRecord, SeverityNumber
13
+ from opentelemetry.sdk._logs import LoggerProvider
14
+ from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
15
+ from opentelemetry.sdk.resources import Resource
16
+ from opentelemetry.sdk.trace import TracerProvider
17
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
18
+ from opentelemetry.sdk.trace.sampling import ALWAYS_ON
19
+ from opentelemetry.trace import Context, SpanKind
20
+
21
+ from hypertube.utils.logging.LogsLevel import LogsLevel
22
+
23
+ from graftcode_instrumentation import GraftcodeInstrumentation
24
+
25
+ from graftcode_telemetry_appinsights.GraftCodeAppInsightsConnectorOptions import GraftCodeAppInsightsConnectorOptions
26
+ from graftcode_telemetry_appinsights.GraftCodeAppInsightsLogger import GraftCodeAppInsightsLogger
27
+ from graftcode_telemetry_appinsights.RateLimitedSampler import RateLimitedSampler
28
+
29
+ _DEFAULT_FLUSH_MILLISECONDS = 5000
30
+
31
+ _MISSING_CONNECTION_STRING = (
32
+ "Application Insights telemetry for {ServiceName} is disabled: no connection string was configured. "
33
+ "Set " + GraftCodeAppInsightsConnectorOptions.CONNECTION_STRING_VARIABLE + " in this process or set "
34
+ "GraftCodeAppInsightsConnectorOptions.connection_string.")
35
+
36
+ _PYTHON_LEVELS = {
37
+ LogsLevel.Trace: 5,
38
+ LogsLevel.Debug: logging.DEBUG,
39
+ LogsLevel.Info: logging.INFO,
40
+ LogsLevel.Warning: logging.WARNING,
41
+ LogsLevel.Error: logging.ERROR,
42
+ LogsLevel.Off: logging.CRITICAL + 1,
43
+ }
44
+
45
+ _STANDARD_RECORD_ATTRIBUTES = frozenset(
46
+ logging.LogRecord("", 0, "", 0, "", None, None).__dict__.keys()) | {"message", "asctime", "taskName"}
47
+
48
+
49
+ class _HypertubeLogHandler(logging.Handler):
50
+
51
+ def __init__(self, logger_provider: LoggerProvider) -> None:
52
+ super().__init__(level=0)
53
+ self._logger = logger_provider.get_logger(GraftcodeInstrumentation.LOGGER_NAME)
54
+
55
+ def emit(self, record: logging.LogRecord) -> None:
56
+ try:
57
+ attributes = {
58
+ key: value for key, value in record.__dict__.items()
59
+ if key not in _STANDARD_RECORD_ATTRIBUTES and isinstance(value, (str, bool, int, float))
60
+ }
61
+
62
+ if record.exc_info and record.exc_info[1] is not None:
63
+ exception = record.exc_info[1]
64
+ attributes["exception.type"] = type(exception).__qualname__
65
+ if str(exception):
66
+ attributes["exception.message"] = str(exception)
67
+ attributes["exception.stacktrace"] = "".join(traceback.format_exception(*record.exc_info))
68
+
69
+ self._logger.emit(LogRecord(
70
+ timestamp=int(record.created * 1e9),
71
+ context=otel_context.get_current(),
72
+ severity_number=_HypertubeLogHandler._severity(record.levelno),
73
+ severity_text="Information" if record.levelno == logging.INFO else record.levelname.capitalize(),
74
+ body=record.getMessage(),
75
+ attributes=attributes))
76
+ except Exception:
77
+ self.handleError(record)
78
+
79
+ @staticmethod
80
+ def _severity(level: int) -> SeverityNumber:
81
+ if level >= logging.ERROR:
82
+ return SeverityNumber.ERROR
83
+ if level >= logging.WARNING:
84
+ return SeverityNumber.WARN
85
+ if level >= logging.INFO:
86
+ return SeverityNumber.INFO
87
+ if level >= logging.DEBUG:
88
+ return SeverityNumber.DEBUG
89
+ return SeverityNumber.TRACE
90
+
91
+
92
+ class GraftCodeAppInsightsConnectorPipeline:
93
+
94
+ def __init__(
95
+ self,
96
+ options: GraftCodeAppInsightsConnectorOptions,
97
+ providers: Optional[Tuple[TracerProvider, LoggerProvider]] = None,
98
+ ) -> None:
99
+ options.validate()
100
+
101
+ self._flush_gate = threading.Lock()
102
+ self._closed = False
103
+ self.service_name = options.service_name
104
+ connection_string = options.resolve_connection_string() if providers is None else None
105
+ self._exporting_to_app_insights = connection_string is not None
106
+
107
+ if providers is not None:
108
+ self.tracer_provider, self.logger_provider = providers
109
+ elif connection_string is not None:
110
+ self.tracer_provider, self.logger_provider = GraftCodeAppInsightsConnectorPipeline._build_exporting_sdk(
111
+ self.service_name, connection_string, options.traces_per_second)
112
+ else:
113
+ self.tracer_provider, self.logger_provider = GraftCodeAppInsightsConnectorPipeline._build_local_sdk(
114
+ self.service_name)
115
+
116
+ self.tracer = self.tracer_provider.get_tracer(self.service_name)
117
+ self.logger = GraftCodeAppInsightsLogger(
118
+ self.service_name,
119
+ self.logger_provider.get_logger(self.service_name),
120
+ options.minimum_log_level,
121
+ True if options.enable_console_logging else None)
122
+
123
+ self._hypertube_logger = logging.getLogger(GraftcodeInstrumentation.LOGGER_NAME)
124
+ self._hypertube_logger.setLevel(_PYTHON_LEVELS[options.minimum_log_level])
125
+ self._hypertube_logger.propagate = False
126
+ self._log_handler = _HypertubeLogHandler(self.logger_provider)
127
+ self._hypertube_logger.addHandler(self._log_handler)
128
+ self.instrumentation = GraftcodeInstrumentation.attach(self.tracer_provider, self._hypertube_logger)
129
+
130
+ if providers is None and connection_string is None:
131
+ self._warn_about_missing_connection_string(options)
132
+
133
+ atexit.register(self.close)
134
+
135
+ self._previous_excepthook = sys.excepthook
136
+ self._previous_thread_excepthook = threading.excepthook
137
+ sys.excepthook = self._on_uncaught_exception
138
+ threading.excepthook = self._on_uncaught_thread_exception
139
+
140
+ def is_exporting_to_app_insights(self) -> bool:
141
+ return self._exporting_to_app_insights
142
+
143
+ def start_span(
144
+ self,
145
+ name: str,
146
+ kind: SpanKind = SpanKind.INTERNAL,
147
+ context: Optional[Context] = None,
148
+ ):
149
+ return self.tracer.start_span(name, context=context, kind=kind)
150
+
151
+ def force_flush(self, timeout_millis: int = _DEFAULT_FLUSH_MILLISECONDS) -> bool:
152
+ with self._flush_gate:
153
+ if self._closed:
154
+ return True
155
+
156
+ traces = self.tracer_provider.force_flush(timeout_millis)
157
+ logs = self.logger_provider.force_flush(timeout_millis)
158
+ return bool(traces) and bool(logs)
159
+
160
+ def close(self) -> None:
161
+ with self._flush_gate:
162
+ if self._closed:
163
+ return
164
+
165
+ self.tracer_provider.force_flush(_DEFAULT_FLUSH_MILLISECONDS)
166
+ self.logger_provider.force_flush(_DEFAULT_FLUSH_MILLISECONDS)
167
+ self._closed = True
168
+
169
+ self.instrumentation.close()
170
+ self._hypertube_logger.removeHandler(self._log_handler)
171
+
172
+ try:
173
+ atexit.unregister(self.close)
174
+ except Exception:
175
+ pass
176
+
177
+ if sys.excepthook == self._on_uncaught_exception:
178
+ sys.excepthook = self._previous_excepthook
179
+ if threading.excepthook == self._on_uncaught_thread_exception:
180
+ threading.excepthook = self._previous_thread_excepthook
181
+
182
+ self.tracer_provider.shutdown()
183
+ self.logger_provider.shutdown()
184
+
185
+ def dispose(self) -> None:
186
+ self.close()
187
+
188
+ def __enter__(self) -> GraftCodeAppInsightsConnectorPipeline:
189
+ return self
190
+
191
+ def __exit__(
192
+ self,
193
+ exc_type: Optional[Type[BaseException]],
194
+ exc: Optional[BaseException],
195
+ traceback_obj: Optional[TracebackType],
196
+ ) -> bool:
197
+ self.close()
198
+ return False
199
+
200
+ @staticmethod
201
+ def _build_exporting_sdk(
202
+ service_name: str,
203
+ connection_string: str,
204
+ traces_per_second: Optional[float],
205
+ ) -> Tuple[TracerProvider, LoggerProvider]:
206
+ from azure.monitor.opentelemetry.exporter import AzureMonitorLogExporter, AzureMonitorTraceExporter
207
+
208
+ sampler = ALWAYS_ON if traces_per_second is None else RateLimitedSampler(traces_per_second)
209
+ resource = Resource.create({"service.name": service_name})
210
+
211
+ tracer_provider = TracerProvider(resource=resource, sampler=sampler)
212
+ tracer_provider.add_span_processor(BatchSpanProcessor(AzureMonitorTraceExporter(
213
+ connection_string=connection_string, tracer_provider=tracer_provider)))
214
+
215
+ logger_provider = LoggerProvider(resource=resource)
216
+ logger_provider.add_log_record_processor(
217
+ BatchLogRecordProcessor(AzureMonitorLogExporter(connection_string=connection_string)))
218
+
219
+ return tracer_provider, logger_provider
220
+
221
+ @staticmethod
222
+ def _build_local_sdk(service_name: str) -> Tuple[TracerProvider, LoggerProvider]:
223
+ resource = Resource.create({"service.name": service_name})
224
+ return TracerProvider(resource=resource), LoggerProvider(resource=resource)
225
+
226
+ def _warn_about_missing_connection_string(self, options: GraftCodeAppInsightsConnectorOptions) -> None:
227
+ self.logger.log(LogsLevel.Warning, _MISSING_CONNECTION_STRING, self.service_name)
228
+
229
+ if not options.enable_console_logging:
230
+ sys.stderr.write(_MISSING_CONNECTION_STRING.replace("{ServiceName}", self.service_name) + "\n")
231
+
232
+ def _on_uncaught_exception(
233
+ self,
234
+ exc_type: Optional[Type[BaseException]],
235
+ exc_value: Optional[BaseException],
236
+ exc_traceback: Optional[TracebackType],
237
+ ) -> None:
238
+ self.force_flush()
239
+ self._previous_excepthook(exc_type, exc_value, exc_traceback)
240
+
241
+ def _on_uncaught_thread_exception(self, args: threading.ExceptHookArgs) -> None:
242
+ self.force_flush()
243
+ self._previous_thread_excepthook(args)