6
6
metrics that track the usage and performance of the Azure Monitor OpenTelemetry Exporter.
7
7
"""
8
8
9
- from typing import List , Dict , Any , Iterable
9
+ from typing import List , Dict , Any , Iterable , Optional
10
10
import os
11
11
12
12
from opentelemetry .metrics import CallbackOptions , Observation
17
17
_APPLICATIONINSIGHTS_STATSBEAT_ENABLED_PREVIEW ,
18
18
_DEFAULT_STATS_SHORT_EXPORT_INTERVAL ,
19
19
CustomerStatsbeatProperties ,
20
- #DropCode,
20
+ DropCode ,
21
+ DropCodeType ,
21
22
#RetryCode,
22
23
CustomerStatsbeatMetricName ,
23
24
_CUSTOMER_STATSBEAT_LANGUAGE ,
29
30
get_compute_type ,
30
31
)
31
32
33
+ from azure .monitor .opentelemetry .exporter .statsbeat ._utils import (
34
+ categorize_status_code ,
35
+ )
32
36
from azure .monitor .opentelemetry .exporter import VERSION
33
37
34
38
class _CustomerStatsbeatTelemetryCounters :
35
39
def __init__ (self ):
36
40
self .total_item_success_count : Dict [str , Any ] = {}
41
+ self .total_item_drop_count : Dict [str , Dict [DropCodeType , Dict [str , int ]]] = {}
37
42
38
43
class CustomerStatsbeatMetrics (metaclass = Singleton ):
39
44
def __init__ (self , options ):
@@ -66,20 +71,51 @@ def __init__(self, options):
66
71
description = "Tracks successful telemetry items sent to Azure Monitor" ,
67
72
callbacks = [self ._item_success_callback ]
68
73
)
74
+ self ._dropped_gauge = self ._customer_statsbeat_meter .create_observable_gauge (
75
+ name = CustomerStatsbeatMetricName .ITEM_DROP_COUNT .value ,
76
+ description = "Tracks dropped telemetry items sent to Azure Monitor" ,
77
+ callbacks = [self ._item_drop_callback ]
78
+ )
69
79
70
80
def count_successful_items (self , count : int , telemetry_type : str ) -> None :
71
81
if not self ._is_enabled or count <= 0 :
72
82
return
83
+
73
84
if telemetry_type in self ._counters .total_item_success_count :
74
85
self ._counters .total_item_success_count [telemetry_type ] += count
75
86
else :
76
87
self ._counters .total_item_success_count [telemetry_type ] = count
77
88
89
+ def count_dropped_items (
90
+ self , count : int , telemetry_type : str , drop_code : DropCodeType ,
91
+ exception_message : Optional [str ] = None
92
+ ) -> None :
93
+ if not self ._is_enabled or count <= 0 :
94
+ return
95
+
96
+ # Get or create the drop_code map for this telemetry_type
97
+ if telemetry_type not in self ._counters .total_item_drop_count :
98
+ self ._counters .total_item_drop_count [telemetry_type ] = {}
99
+ drop_code_map = self ._counters .total_item_drop_count [telemetry_type ]
100
+
101
+ # Get or create the reason map for this drop_code
102
+ if drop_code not in drop_code_map :
103
+ drop_code_map [drop_code ] = {}
104
+ reason_map = drop_code_map [drop_code ]
105
+
106
+ # Generate a low-cardinality, informative reason description
107
+ reason = self ._get_drop_reason (drop_code , exception_message )
108
+
109
+ # Update the count for this reason
110
+ current_count = reason_map .get (reason , 0 )
111
+ reason_map [reason ] = current_count + count
112
+
78
113
def _item_success_callback (self , options : CallbackOptions ) -> Iterable [Observation ]: # pylint: disable=unused-argument
79
114
if not getattr (self , "_is_enabled" , False ):
80
115
return []
81
116
82
117
observations : List [Observation ] = []
118
+
83
119
for telemetry_type , count in self ._counters .total_item_success_count .items ():
84
120
attributes = {
85
121
"language" : self ._customer_properties .language ,
@@ -90,3 +126,37 @@ def _item_success_callback(self, options: CallbackOptions) -> Iterable[Observati
90
126
observations .append (Observation (count , dict (attributes )))
91
127
92
128
return observations
129
+
130
+ def _item_drop_callback (self , options : CallbackOptions ) -> Iterable [Observation ]: # pylint: disable=unused-argument
131
+ if not getattr (self , "_is_enabled" , False ):
132
+ return []
133
+ observations : List [Observation ] = []
134
+ for telemetry_type , drop_code_map in self ._counters .total_item_drop_count .items ():
135
+ for drop_code , reason_map in drop_code_map .items ():
136
+ for reason , count in reason_map .items ():
137
+ attributes = {
138
+ "language" : self ._customer_properties .language ,
139
+ "version" : self ._customer_properties .version ,
140
+ "compute_type" : self ._customer_properties .compute_type ,
141
+ "drop.code" : drop_code ,
142
+ "drop.reason" : reason ,
143
+ "telemetry_type" : telemetry_type
144
+ }
145
+ observations .append (Observation (count , dict (attributes )))
146
+
147
+ return observations
148
+
149
+ def _get_drop_reason (self , drop_code : DropCodeType , exception_message : Optional [str ] = None ) -> str :
150
+ if isinstance (drop_code , int ):
151
+ return categorize_status_code (drop_code )
152
+
153
+ if drop_code == DropCode .CLIENT_EXCEPTION :
154
+ return exception_message if exception_message else "unknown_exception"
155
+
156
+ drop_code_reasons = {
157
+ DropCode .CLIENT_READONLY : "readonly_mode" ,
158
+ DropCode .CLIENT_STALE_DATA : "stale_data" ,
159
+ DropCode .CLIENT_PERSISTENCE_CAPACITY : "persistence_full" ,
160
+ }
161
+
162
+ return drop_code_reasons .get (drop_code , "unknown_reason" )
0 commit comments