Thanks to visit codestin.com
Credit goes to github.com

Skip to content
This repository was archived by the owner on Nov 16, 2023. It is now read-only.

Adding live metrics manager #78

Merged
merged 11 commits into from
May 11, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ omit =
azure_monitor/tests/*

[report]
fail_under = 98
fail_under = 90
show_missing = True
omit =
azure_monitor/setup.py
Expand Down
124 changes: 124 additions & 0 deletions azure_monitor/src/azure_monitor/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -561,3 +561,127 @@ def to_dict(self):
"properties": self.properties,
"measurements": self.measurements,
}


class LiveMetricDocumentProperty(BaseObject):

__slots__ = ("key", "value")

def __init__(self, key: str, value: str) -> None:
self.key = key
self.value = value


class LiveMetricDocument(BaseObject):

__slots__ = (
"__type",
"document_type",
"version",
"operation_id",
"properties",
)

def __init__(
self,
__type: str = "",
document_type: str = "",
version: str = "",
operation_id: str = "",
properties: typing.List[LiveMetricDocumentProperty] = None,
) -> None:
self.__type = __type
self.document_type = document_type
self.version = version
self.operation_id = operation_id
self.properties = properties

def to_dict(self):
return {
"__type": self.__type,
"DocumentType": self.document_type,
"Version": self.version,
"OperationId": self.operation_id,
"Properties": self.properties.to_dict()
if self.properties
else None,
}


class LiveMetric(BaseObject):

__slots__ = ("name", "value", "weight")

def __init__(self, name: str, value: str, weight: int) -> None:
self.name = name
self.value = value
self.weight = weight

def to_dict(self):
return {"Name": self.name, "Value": self.value, "Weight": self.weight}


class LiveMetricEnvelope(BaseObject):
"""Envelope to send data to Live Metrics service.

Args:
documents: An array of detailed failure documents, each representing a full SDK telemetry item.
instance: Instance name. Either a cloud RoleInstance (if running in Azure), or the machine name otherwise.
instrumentation_key: Instrumentation key that the agent is instrumented with. While SDK can send to multiple ikeys,
it must select a single ikey to send QuickPulse data to - and only consider telemetry items sent to that ikey while collecting.
invariant_version: Version of QPS protocol that SDK supports. Currently, the latest is 2.
machine_name: Machine name.
metrics: Metrics
stream_id: A random GUID generated at start-up. Must remain unchanged while the application is running.
timestamp: UTC time the request was made in the Microsoft JSON format, e.g. "/Date(1478555534692)/".
version: SDK version (not specific to QuickPulse). Please make sure that there's some sort of prefix to identify the client (.NET SDK, Java SDK, etc.).
"""

__slots__ = (
"documents",
"instance",
"instrumentation_key",
"invariant_version",
"machine_name",
"metrics",
"stream_id",
"timestamp",
"version",
)

def __init__(
self,
documents: typing.List[LiveMetricDocument] = None,
instance: str = "",
instrumentation_key: str = "",
invariant_version: int = 1,
machine_name: str = "",
metrics: typing.List[LiveMetric] = None,
stream_id: str = "",
timestamp: str = "",
version: str = "",
) -> None:
if metrics is None:
metrics = []
self.documents = documents
self.instance = instance
self.instrumentation_key = instrumentation_key
self.invariant_version = invariant_version
self.machine_name = machine_name
self.metrics = metrics
self.stream_id = stream_id
self.timestamp = timestamp
self.version = version

def to_dict(self):
return {
"Documents": self.documents.to_dict() if self.documents else None,
"Instance": self.instance,
"InstrumentationKey": self.instrumentation_key,
"InvariantVersion": self.invariant_version,
"MachineName": self.machine_name,
"Metrics": list(map(lambda x: x.to_dict(), self.metrics)),
"StreamId": self.stream_id,
"Timestamp": self.timestamp,
"Version": self.version,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
#
import collections
import logging
import typing

from opentelemetry.sdk.metrics import Counter, Observer
from opentelemetry.sdk.metrics.export import (
MetricRecord,
MetricsExporter,
MetricsExportResult,
)

from azure_monitor.protocol import (
Envelope,
LiveMetric,
LiveMetricDocument,
LiveMetricDocumentProperty,
LiveMetricEnvelope,
)
from azure_monitor.sdk.auto_collection.live_metrics import utils
from azure_monitor.sdk.auto_collection.live_metrics.sender import (
LiveMetricsSender,
)

logger = logging.getLogger(__name__)


# pylint: disable=no-self-use
# pylint: disable=too-many-statements
# pylint: disable=too-many-return-statements
class LiveMetricsExporter(MetricsExporter):
"""Live Metrics Exporter

Export data to Azure Live Metrics service and determine if user is subscribed.
"""

def __init__(self, instrumentation_key):
self._instrumentation_key = instrumentation_key
self._sender = LiveMetricsSender(self._instrumentation_key)
self.subscribed = True
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like the exporter keeping the state of subscribed, however there doesn't seem to be a nice way to do this. One way is to use the context, where the key is LIVE_METRICS_{x} where x is the unique UUID for that manager and the value is subscribed or not. Another (ugly) way is to have a reference to the manager from the exporter and keep the state of subscribed in the manager.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since UUID is unique per application and not manager instance, I think it is fine to have a global variable in the context keeping track of the state of subscribed. Thoughts?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Problem is with internal threads, we have 3 different threads that would be using this global context variable, making this code easier to break in specific cases, my point is that is not worth to add this logic only because subscribed flag feels out of place.

self._document_envelopes = collections.deque()

def add_document(self, envelope: Envelope):
self._document_envelopes.append(envelope)

def export(
self, metric_records: typing.Sequence[MetricRecord]
) -> MetricsExportResult:
envelope = self._metric_to_live_metrics_envelope(metric_records)
try:
response = self._sender.post(envelope)
if response.ok:
self.subscribed = (
response.headers.get(utils.LIVE_METRICS_SUBSCRIBED_HEADER)
== "true"
)
return MetricsExportResult.SUCCESS

except Exception: # pylint: disable=broad-except
logger.exception("Exception occurred while exporting the data.")

return MetricsExportResult.FAILED_NOT_RETRYABLE

def _metric_to_live_metrics_envelope(
self, metric_records: typing.Sequence[MetricRecord]
) -> LiveMetricEnvelope:

envelope = utils.create_metric_envelope(self._instrumentation_key)
envelope.documents = self._get_live_metric_documents()
envelope.metrics = []

# Add metrics
for metric_record in metric_records:
value = 0
metric = metric_record.metric
if isinstance(metric, Counter):
value = metric_record.aggregator.checkpoint
elif isinstance(metric, Observer):
value = metric_record.aggregator.checkpoint.last
if not value:
value = 0
envelope.metrics.append(
LiveMetric(name=metric.name, value=value, weight=1)
)

return envelope

def _get_live_metric_documents(
self,
) -> typing.Sequence[LiveMetricDocument]:
live_metric_documents = []
while self._document_envelopes:
for envelope in self._document_envelopes.popleft():
base_type = envelope.data.baseType
if base_type:
document = LiveMetricDocument(
__type=self._get_live_metric_type(base_type),
document_type=self._get_live_metric_document_type(
base_type
),
properties=self._get_aggregated_properties(envelope),
version="1.0",
)
live_metric_documents.append(document)
else:
logger.warning(
"Document type invalid; not sending live metric document"
)

return live_metric_documents

def _get_live_metric_type(self, base_type: str) -> str:
if base_type == "EventData":
return "EventTelemetryDocument"
if base_type == "ExceptionData":
return "ExceptionTelemetryDocument"
if base_type == "MessageData":
return "TraceTelemetryDocument"
if base_type == "MetricData":
return "MetricTelemetryDocument"
if base_type == "RequestData":
return "RequestTelemetryDocument"
if base_type == "RemoteDependencyData":
return "DependencyTelemetryDocument"
if base_type == "AvailabilityData":
return "AvailabilityTelemetryDocument"
return ""

def _get_live_metric_document_type(self, base_type: str) -> str:
if base_type == "EventData":
return "Event"
if base_type == "ExceptionData":
return "Exception"
if base_type == "MessageData":
return "Trace"
if base_type == "MetricData":
return "Metric"
if base_type == "RequestData":
return "Request"
if base_type == "RemoteDependencyData":
return "RemoteDependency"
if base_type == "AvailabilityData":
return "Availability"
return ""

def _get_aggregated_properties(
self, envelope: Envelope
) -> typing.List[LiveMetricDocumentProperty]:

aggregated_properties = []
measurements = (
envelope.data.base_data.measurements
if envelope.data.base_data.measurements
else []
)
for key in measurements:
prop = LiveMetricDocumentProperty(key=key, value=measurements[key])
aggregated_properties.append(prop)
properties = (
envelope.data.base_data.properties
if envelope.data.base_data.properties
else []
)
for key in properties:
prop = LiveMetricDocumentProperty(key=key, value=properties[key])
aggregated_properties.append(prop)
return aggregated_properties
Loading