Skip to content

Commit 80f0252

Browse files
authored
Implement live-metrics skeleton (#33983)
1 parent 82edd19 commit 80f0252

29 files changed

+5053
-14
lines changed

sdk/monitor/azure-monitor-opentelemetry-exporter/CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
### Features Added
66

7+
- Add live metrics skeleton + swagger definitions
8+
([#33983](https://github.com/Azure/azure-sdk-for-python/pull/33983))
9+
710
### Breaking Changes
811

912
### Bugs Fixed
@@ -34,7 +37,7 @@
3437
- Implement distro detection for statsbeat feature
3538
([#33761](https://github.com/Azure/azure-sdk-for-python/pull/33761))
3639
- Use empty resource for statsbeat `MeterProvider`
37-
([#33761](https://github.com/Azure/azure-sdk-for-python/pull/33761))
40+
([#33768](https://github.com/Azure/azure-sdk-for-python/pull/33768))
3841

3942
## 1.0.0b19 (2023-11-20)
4043

sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/_connection_string_parser.py

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import re
55
import typing
66

7+
LIVE_ENDPOINT = "liveendpoint"
78
INGESTION_ENDPOINT = "ingestionendpoint"
89
INSTRUMENTATION_KEY = "instrumentationkey"
910

@@ -32,6 +33,7 @@ def __init__(
3233
) -> None:
3334
self.instrumentation_key = None
3435
self.endpoint = ""
36+
self.live_endpoint = ""
3537
self._connection_string = connection_string
3638
self._initialize()
3739
self._validate_instrumentation_key()
@@ -56,7 +58,7 @@ def _initialize(self) -> None:
5658
or env_cs.get(INSTRUMENTATION_KEY)
5759
or env_ikey
5860
)
59-
# The priority of the ingestion endpoint is as follows:
61+
# The priority of the endpoints is as follows:
6062
# 1. The endpoint explicitly passed in connection string
6163
# 2. The endpoint from the connection string in environment variable
6264
# 3. The default breeze endpoint
@@ -65,6 +67,12 @@ def _initialize(self) -> None:
6567
or env_cs.get(INGESTION_ENDPOINT)
6668
or "https://dc.services.visualstudio.com"
6769
)
70+
self.live_endpoint = (
71+
code_cs.get(LIVE_ENDPOINT)
72+
or env_cs.get(LIVE_ENDPOINT)
73+
or "https://rt.services.visualstudio.com"
74+
)
75+
6876

6977
def _validate_instrumentation_key(self) -> None:
7078
"""Validates the instrumentation key used for Azure Monitor.
@@ -93,22 +101,34 @@ def _parse_connection_string(self, connection_string) -> typing.Dict:
93101
auth = result.get("authorization")
94102
if auth is not None and auth.lower() != "ikey":
95103
raise ValueError("Invalid authorization mechanism")
96-
# Construct the ingestion endpoint if not passed in explicitly
104+
105+
# Construct the endpoints if not passed in explicitly
106+
endpoint_suffix = ""
107+
location_prefix = ""
108+
suffix = result.get("endpointsuffix")
109+
# Get regional information if provided
110+
prefix = result.get("location")
111+
if suffix is not None:
112+
endpoint_suffix = suffix
113+
# Get regional information if provided
114+
prefix = result.get("location")
115+
if prefix is not None:
116+
location_prefix = prefix + "."
117+
# Construct the endpoints if not passed in explicitly
97118
if result.get(INGESTION_ENDPOINT) is None:
98-
endpoint_suffix = ""
99-
location_prefix = ""
100-
suffix = result.get("endpointsuffix")
101-
if suffix is not None:
102-
endpoint_suffix = suffix
103-
# Get regional information if provided
104-
prefix = result.get("location")
105-
if prefix is not None:
106-
location_prefix = prefix + "."
107-
endpoint = "https://{0}dc.{1}".format(
119+
if endpoint_suffix:
120+
result[INGESTION_ENDPOINT] = "https://{0}dc.{1}".format(
108121
location_prefix, endpoint_suffix
109122
)
110-
result[INGESTION_ENDPOINT] = endpoint
111123
else:
112124
# Default to None if cannot construct
113125
result[INGESTION_ENDPOINT] = None
126+
if result.get(LIVE_ENDPOINT) is None:
127+
if endpoint_suffix:
128+
result[LIVE_ENDPOINT] = "https://{0}live.{1}".format(
129+
location_prefix, endpoint_suffix
130+
)
131+
else:
132+
result[LIVE_ENDPOINT] = None
133+
114134
return result
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# -------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License in the project root for
4+
# license information.
5+
# -------------------------------------------------------------------------
6+
7+
from azure.monitor.opentelemetry.exporter._quickpulse._exporter import QuickpulseExporter
8+
9+
__all__ = [
10+
"QuickpulseExporter",
11+
]
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
import logging
4+
5+
from typing import Any
6+
7+
from opentelemetry.sdk.metrics import (
8+
Counter,
9+
Histogram,
10+
ObservableCounter,
11+
ObservableGauge,
12+
ObservableUpDownCounter,
13+
UpDownCounter,
14+
)
15+
from opentelemetry.sdk.metrics.export import (
16+
AggregationTemporality,
17+
MetricExporter,
18+
MetricExportResult,
19+
MetricsData as OTMetricsData,
20+
)
21+
from azure.monitor.opentelemetry.exporter._quickpulse._generated._client import QuickpulseClient
22+
from azure.monitor.opentelemetry.exporter._connection_string_parser import ConnectionStringParser
23+
24+
_logger = logging.getLogger(__name__)
25+
26+
__all__ = ["QuickpulseExporter"]
27+
28+
29+
APPLICATION_INSIGHTS_METRIC_TEMPORALITIES = {
30+
Counter: AggregationTemporality.DELTA,
31+
Histogram: AggregationTemporality.DELTA,
32+
ObservableCounter: AggregationTemporality.DELTA,
33+
ObservableGauge: AggregationTemporality.CUMULATIVE,
34+
ObservableUpDownCounter: AggregationTemporality.CUMULATIVE,
35+
UpDownCounter: AggregationTemporality.CUMULATIVE,
36+
}
37+
38+
39+
class QuickpulseExporter(MetricExporter):
40+
41+
def __init__(self, **kwargs: Any) -> None:
42+
"""Metric exporter for Quickpulse.
43+
44+
:keyword str connection_string: The connection string used for your Application Insights resource.
45+
:rtype: None
46+
"""
47+
parsed_connection_string = ConnectionStringParser(kwargs.get('connection_string'))
48+
49+
self._endpoint = parsed_connection_string.endpoint
50+
# TODO: Support AADaudience (scope)/credentials
51+
52+
self.client = QuickpulseClient(host=self._endpoint, **kwargs)
53+
# TODO: Support redirect
54+
55+
MetricExporter.__init__(
56+
self,
57+
preferred_temporality=APPLICATION_INSIGHTS_METRIC_TEMPORALITIES, # type: ignore
58+
preferred_aggregation=kwargs.get("preferred_aggregation"), # type: ignore
59+
)
60+
61+
def export(
62+
self,
63+
metrics_data: OTMetricsData,
64+
timeout_millis: float = 10_000, # pylint: disable=unused-argument
65+
**kwargs: Any, # pylint: disable=unused-argument
66+
) -> MetricExportResult:
67+
"""Exports a batch of metric data
68+
69+
:param metrics_data: OpenTelemetry Metric(s) to export.
70+
:type metrics_data: Sequence[~opentelemetry.sdk.metrics._internal.point.MetricsData]
71+
:param timeout_millis: The maximum amount of time to wait for each export. Not currently used.
72+
:type timeout_millis: float
73+
:return: The result of the export.
74+
:rtype: ~opentelemetry.sdk.metrics.export.MetricExportResult
75+
"""
76+
# TODO
77+
return MetricExportResult.SUCCESS
78+
79+
def force_flush(
80+
self,
81+
timeout_millis: float = 10_000,
82+
) -> bool:
83+
"""
84+
Ensure that export of any metrics currently received by the exporter
85+
are completed as soon as possible. Called when SDK is flushed.
86+
87+
:param timeout_millis: The maximum amount of time to wait for shutdown. Not currently used.
88+
:type timeout_millis: float
89+
:return: The result of the export.
90+
:rtype: bool
91+
"""
92+
return True
93+
94+
95+
def shutdown(
96+
self,
97+
timeout_millis: float = 30_000, # pylint: disable=unused-argument
98+
**kwargs: Any, # pylint: disable=unused-argument
99+
) -> None:
100+
"""Shuts down the exporter.
101+
102+
Called when the SDK is shut down.
103+
104+
:param timeout_millis: The maximum amount of time to wait for shutdown. Not currently used.
105+
:type timeout_millis: float
106+
"""
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# coding=utf-8
2+
# --------------------------------------------------------------------------
3+
# Copyright (c) Microsoft Corporation. All rights reserved.
4+
# Licensed under the MIT License. See License.txt in the project root for license information.
5+
# Code generated by Microsoft (R) AutoRest Code Generator.
6+
# Changes may cause incorrect behavior and will be lost if the code is regenerated.
7+
# --------------------------------------------------------------------------
8+
9+
from ._client import QuickpulseClient
10+
11+
try:
12+
from ._patch import __all__ as _patch_all
13+
from ._patch import * # pylint: disable=unused-wildcard-import
14+
except ImportError:
15+
_patch_all = []
16+
from ._patch import patch_sdk as _patch_sdk
17+
18+
__all__ = [
19+
"QuickpulseClient",
20+
]
21+
__all__.extend([p for p in _patch_all if p not in __all__])
22+
23+
_patch_sdk()
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# coding=utf-8
2+
# --------------------------------------------------------------------------
3+
# Copyright (c) Microsoft Corporation. All rights reserved.
4+
# Licensed under the MIT License. See License.txt in the project root for license information.
5+
# Code generated by Microsoft (R) AutoRest Code Generator.
6+
# Changes may cause incorrect behavior and will be lost if the code is regenerated.
7+
# --------------------------------------------------------------------------
8+
9+
from copy import deepcopy
10+
from typing import Any
11+
12+
from azure.core import PipelineClient
13+
from azure.core.pipeline import policies
14+
from azure.core.rest import HttpRequest, HttpResponse
15+
16+
from . import models as _models
17+
from ._configuration import QuickpulseClientConfiguration
18+
from ._operations import QuickpulseClientOperationsMixin
19+
from ._serialization import Deserializer, Serializer
20+
21+
22+
class QuickpulseClient(QuickpulseClientOperationsMixin): # pylint: disable=client-accepts-api-version-keyword
23+
"""Quickpulse Client.
24+
25+
:param host: QuickPulse endpoint: https://rt.services.visualstudio.com. Default value is
26+
"https://rt.services.visualstudio.com".
27+
:type host: str
28+
"""
29+
30+
def __init__( # pylint: disable=missing-client-constructor-parameter-credential
31+
self, host: str = "https://rt.services.visualstudio.com", **kwargs: Any
32+
) -> None:
33+
_endpoint = "{Host}"
34+
self._config = QuickpulseClientConfiguration(host=host, **kwargs)
35+
_policies = kwargs.pop("policies", None)
36+
if _policies is None:
37+
_policies = [
38+
policies.RequestIdPolicy(**kwargs),
39+
self._config.headers_policy,
40+
self._config.user_agent_policy,
41+
self._config.proxy_policy,
42+
policies.ContentDecodePolicy(**kwargs),
43+
self._config.redirect_policy,
44+
self._config.retry_policy,
45+
self._config.authentication_policy,
46+
self._config.custom_hook_policy,
47+
self._config.logging_policy,
48+
policies.DistributedTracingPolicy(**kwargs),
49+
policies.SensitiveHeaderCleanupPolicy(**kwargs) if self._config.redirect_policy else None,
50+
self._config.http_logging_policy,
51+
]
52+
self._client: PipelineClient = PipelineClient(base_url=_endpoint, policies=_policies, **kwargs)
53+
54+
client_models = {k: v for k, v in _models.__dict__.items() if isinstance(v, type)}
55+
self._serialize = Serializer(client_models)
56+
self._deserialize = Deserializer(client_models)
57+
self._serialize.client_side_validation = False
58+
59+
def send_request(self, request: HttpRequest, *, stream: bool = False, **kwargs: Any) -> HttpResponse:
60+
"""Runs the network request through the client's chained policies.
61+
62+
>>> from azure.core.rest import HttpRequest
63+
>>> request = HttpRequest("GET", "https://www.example.org/")
64+
<HttpRequest [GET], url: 'https://www.example.org/'>
65+
>>> response = client.send_request(request)
66+
<HttpResponse: 200 OK>
67+
68+
For more information on this code flow, see https://aka.ms/azsdk/dpcodegen/python/send_request
69+
70+
:param request: The network request you want to make. Required.
71+
:type request: ~azure.core.rest.HttpRequest
72+
:keyword bool stream: Whether the response payload will be streamed. Defaults to False.
73+
:return: The response of your network call. Does not do error handling on your response.
74+
:rtype: ~azure.core.rest.HttpResponse
75+
"""
76+
77+
request_copy = deepcopy(request)
78+
path_format_arguments = {
79+
"Host": self._serialize.url("self._config.host", self._config.host, "str", skip_quote=True),
80+
}
81+
82+
request_copy.url = self._client.format_url(request_copy.url, **path_format_arguments)
83+
return self._client.send_request(request_copy, stream=stream, **kwargs) # type: ignore
84+
85+
def close(self) -> None:
86+
self._client.close()
87+
88+
def __enter__(self) -> "QuickpulseClient":
89+
self._client.__enter__()
90+
return self
91+
92+
def __exit__(self, *exc_details: Any) -> None:
93+
self._client.__exit__(*exc_details)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# coding=utf-8
2+
# --------------------------------------------------------------------------
3+
# Copyright (c) Microsoft Corporation. All rights reserved.
4+
# Licensed under the MIT License. See License.txt in the project root for license information.
5+
# Code generated by Microsoft (R) AutoRest Code Generator.
6+
# Changes may cause incorrect behavior and will be lost if the code is regenerated.
7+
# --------------------------------------------------------------------------
8+
9+
from typing import Any
10+
11+
from azure.core.pipeline import policies
12+
13+
VERSION = "unknown"
14+
15+
16+
class QuickpulseClientConfiguration: # pylint: disable=too-many-instance-attributes,name-too-long
17+
"""Configuration for QuickpulseClient.
18+
19+
Note that all parameters used to create this instance are saved as instance
20+
attributes.
21+
22+
:param host: QuickPulse endpoint: https://rt.services.visualstudio.com. Default value is
23+
"https://rt.services.visualstudio.com".
24+
:type host: str
25+
"""
26+
27+
def __init__(self, host: str = "https://rt.services.visualstudio.com", **kwargs: Any) -> None:
28+
if host is None:
29+
raise ValueError("Parameter 'host' must not be None.")
30+
31+
self.host = host
32+
kwargs.setdefault("sdk_moniker", "quickpulseclient/{}".format(VERSION))
33+
self.polling_interval = kwargs.get("polling_interval", 30)
34+
self._configure(**kwargs)
35+
36+
def _configure(self, **kwargs: Any) -> None:
37+
self.user_agent_policy = kwargs.get("user_agent_policy") or policies.UserAgentPolicy(**kwargs)
38+
self.headers_policy = kwargs.get("headers_policy") or policies.HeadersPolicy(**kwargs)
39+
self.proxy_policy = kwargs.get("proxy_policy") or policies.ProxyPolicy(**kwargs)
40+
self.logging_policy = kwargs.get("logging_policy") or policies.NetworkTraceLoggingPolicy(**kwargs)
41+
self.http_logging_policy = kwargs.get("http_logging_policy") or policies.HttpLoggingPolicy(**kwargs)
42+
self.custom_hook_policy = kwargs.get("custom_hook_policy") or policies.CustomHookPolicy(**kwargs)
43+
self.redirect_policy = kwargs.get("redirect_policy") or policies.RedirectPolicy(**kwargs)
44+
self.retry_policy = kwargs.get("retry_policy") or policies.RetryPolicy(**kwargs)
45+
self.authentication_policy = kwargs.get("authentication_policy")

0 commit comments

Comments
 (0)