Skip to content
This repository was archived by the owner on Sep 17, 2025. It is now read-only.

Commit 7cbf82f

Browse files
authored
Shutdown statsbeat after failure threshold is met (#1127)
1 parent 94e49ec commit 7cbf82f

File tree

12 files changed

+337
-116
lines changed

12 files changed

+337
-116
lines changed

contrib/opencensus-ext-azure/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
## Unreleased
44

5+
- Shutdown Statsbeat when hitting error/exception threshold
6+
([#1127](https://github.com/census-instrumentation/opencensus-python/pull/1127))
7+
58
## 1.1.4
69
Released 2022-04-20
710

contrib/opencensus-ext-azure/opencensus/ext/azure/common/transport.py

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,15 @@
1414

1515
import json
1616
import logging
17-
import os
1817
import threading
1918
import time
2019

2120
import requests
2221
from azure.core.exceptions import ClientAuthenticationError
2322
from azure.identity._exceptions import CredentialUnavailableError
2423

24+
from opencensus.ext.azure.statsbeat import state
25+
2526
try:
2627
from urllib.parse import urlparse
2728
except ImportError:
@@ -34,13 +35,19 @@
3435
_MONITOR_OAUTH_SCOPE = "https://monitor.azure.com//.default"
3536
_requests_lock = threading.Lock()
3637
_requests_map = {}
38+
_REACHED_INGESTION_STATUS_CODES = (200, 206, 402, 408, 429, 439, 500)
3739

3840

3941
class TransportMixin(object):
4042

43+
# check to see if collecting requests information related to statsbeats
4144
def _check_stats_collection(self):
42-
return not os.environ.get("APPLICATIONINSIGHTS_STATSBEAT_DISABLED_ALL") and (not hasattr(self, '_is_stats') or not self._is_stats) # noqa: E501
45+
return state.is_statsbeat_enabled() and \
46+
not state.get_statsbeat_shutdown() and \
47+
not self._is_stats_exporter()
4348

49+
# check if the current exporter is a statsbeat metric exporter
50+
# only applies to metrics exporter
4451
def _is_stats_exporter(self):
4552
return hasattr(self, '_is_stats') and self._is_stats
4653

@@ -128,7 +135,13 @@ def _transmit(self, envelopes):
128135
_requests_map['retry'] = _requests_map.get('retry', 0) + 1 # noqa: E501
129136
else:
130137
_requests_map['exception'] = _requests_map.get('exception', 0) + 1 # noqa: E501
131-
138+
if self._is_stats_exporter() and \
139+
not state.get_statsbeat_shutdown() and \
140+
not state.get_statsbeat_initial_success():
141+
# If ingestion threshold during statsbeat initialization is
142+
# reached, return back code to shut it down
143+
if _statsbeat_failure_reached_threshold():
144+
return -2
132145
return exception
133146

134147
text = 'N/A'
@@ -143,6 +156,19 @@ def _transmit(self, envelopes):
143156
data = json.loads(text)
144157
except Exception:
145158
pass
159+
160+
if self._is_stats_exporter() and \
161+
not state.get_statsbeat_shutdown() and \
162+
not state.get_statsbeat_initial_success():
163+
# If statsbeat exporter, record initialization as success if
164+
# appropriate status code is returned
165+
if _reached_ingestion_status_code(response.status_code):
166+
state.set_statsbeat_initial_success(True)
167+
elif _statsbeat_failure_reached_threshold():
168+
# If ingestion threshold during statsbeat initialization is
169+
# reached, return back code to shut it down
170+
return -2
171+
146172
if response.status_code == 200:
147173
self._consecutive_redirects = 0
148174
if self._check_stats_collection():
@@ -271,3 +297,13 @@ def _transmit(self, envelopes):
271297
with _requests_lock:
272298
_requests_map['throttle'] = _requests_map.get('throttle', 0) + 1 # noqa: E501
273299
return -response.status_code
300+
301+
302+
def _reached_ingestion_status_code(status_code):
303+
return status_code in _REACHED_INGESTION_STATUS_CODES
304+
305+
306+
def _statsbeat_failure_reached_threshold():
307+
# increment failure counter for sending statsbeat if in initialization
308+
state.increment_statsbeat_initial_failure_count()
309+
return state.get_statsbeat_initial_failure_count() >= 3

contrib/opencensus-ext-azure/opencensus/ext/azure/log_exporter/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
)
3333
from opencensus.ext.azure.common.storage import LocalFileStorage
3434
from opencensus.ext.azure.common.transport import TransportMixin
35-
from opencensus.ext.azure.metrics_exporter import statsbeat_metrics
35+
from opencensus.ext.azure.statsbeat import statsbeat
3636
from opencensus.trace import execution_context
3737

3838
logger = logging.getLogger(__name__)
@@ -67,7 +67,7 @@ def __init__(self, **options):
6767
atexit.register(self.close, self.options.grace_period)
6868
# start statsbeat on exporter instantiation
6969
if not os.environ.get("APPLICATIONINSIGHTS_STATSBEAT_DISABLED_ALL"):
70-
statsbeat_metrics.collect_statsbeat_metrics(self.options)
70+
statsbeat.collect_statsbeat_metrics(self.options)
7171
# For redirects
7272
self._consecutive_redirects = 0 # To prevent circular redirects
7373

contrib/opencensus-ext-azure/opencensus/ext/azure/metrics_exporter/__init__.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ def export_metrics(self, metrics):
7171
for batch in batched_envelopes:
7272
batch = self.apply_telemetry_processors(batch)
7373
result = self._transmit(batch)
74+
# If statsbeat exporter and received signal to shutdown
75+
if self._is_stats_exporter() and result == -2:
76+
from opencensus.ext.azure.statsbeat import statsbeat
77+
statsbeat.shutdown_statsbeat_metrics()
78+
return
7479
# Only store files if local storage enabled
7580
if self.storage and result > 0:
7681
self.storage.put(batch, result)
@@ -144,10 +149,12 @@ def _create_envelope(self, data_point, timestamp, properties):
144149
return envelope
145150

146151
def shutdown(self):
147-
# Flush the exporter thread
148-
# Do not flush if metrics exporter for stats
149-
if self.exporter_thread and not self._is_stats:
150-
self.exporter_thread.close()
152+
if self.exporter_thread:
153+
# flush if metrics exporter is not for stats
154+
if not self._is_stats:
155+
self.exporter_thread.close()
156+
else:
157+
self.exporter_thread.cancel()
151158
# Shutsdown storage worker
152159
if self.storage:
153160
self.storage.close()
@@ -163,7 +170,7 @@ def new_metrics_exporter(**options):
163170
exporter,
164171
interval=exporter.options.export_interval)
165172
if not os.environ.get("APPLICATIONINSIGHTS_STATSBEAT_DISABLED_ALL"):
166-
from opencensus.ext.azure.metrics_exporter import statsbeat_metrics
173+
from opencensus.ext.azure.statsbeat import statsbeat
167174
# Stats will track the user's ikey
168-
statsbeat_metrics.collect_statsbeat_metrics(exporter.options)
175+
statsbeat.collect_statsbeat_metrics(exporter.options)
169176
return exporter
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Copyright 2020, OpenCensus Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import os
16+
import threading
17+
18+
_STATSBEAT_STATE = {
19+
"INITIAL_FAILURE_COUNT": 0,
20+
"INITIAL_SUCCESS": False,
21+
"SHUTDOWN": False,
22+
}
23+
_STATSBEAT_STATE_LOCK = threading.Lock()
24+
25+
26+
def is_statsbeat_enabled():
27+
return not os.environ.get("APPLICATIONINSIGHTS_STATSBEAT_DISABLED_ALL")
28+
29+
30+
def increment_statsbeat_initial_failure_count():
31+
with _STATSBEAT_STATE_LOCK:
32+
_STATSBEAT_STATE["INITIAL_FAILURE_COUNT"] += 1
33+
34+
35+
def get_statsbeat_initial_failure_count():
36+
return _STATSBEAT_STATE["INITIAL_FAILURE_COUNT"]
37+
38+
39+
def set_statsbeat_initial_success(success):
40+
with _STATSBEAT_STATE_LOCK:
41+
_STATSBEAT_STATE["INITIAL_SUCCESS"] = success
42+
43+
44+
def get_statsbeat_initial_success():
45+
return _STATSBEAT_STATE["INITIAL_SUCCESS"]
46+
47+
48+
def get_statsbeat_shutdown():
49+
return _STATSBEAT_STATE["SHUTDOWN"]

contrib/opencensus-ext-azure/opencensus/ext/azure/metrics_exporter/statsbeat_metrics/__init__.py renamed to contrib/opencensus-ext-azure/opencensus/ext/azure/statsbeat/statsbeat.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14-
1514
import threading
1615

1716
from opencensus.ext.azure.metrics_exporter import MetricsExporter
18-
from opencensus.ext.azure.metrics_exporter.statsbeat_metrics.statsbeat import (
17+
from opencensus.ext.azure.statsbeat.state import (
18+
_STATSBEAT_STATE,
19+
_STATSBEAT_STATE_LOCK,
20+
)
21+
from opencensus.ext.azure.statsbeat.statsbeat_metrics import (
1922
_STATS_SHORT_EXPORT_INTERVAL,
2023
_get_stats_connection_string,
2124
_StatsbeatMetrics,
@@ -55,6 +58,29 @@ def collect_statsbeat_metrics(options):
5558
exporter,
5659
exporter.options.export_interval)
5760
_STATSBEAT_EXPORTER = exporter
61+
with _STATSBEAT_STATE_LOCK:
62+
_STATSBEAT_STATE["INITIAL_FAILURE_COUNT"] = 0
63+
_STATSBEAT_STATE["INITIAL_SUCCESS"] = 0
64+
_STATSBEAT_STATE["SHUTDOWN"] = False
65+
66+
67+
def shutdown_statsbeat_metrics():
68+
# pylint: disable=global-statement
69+
global _STATSBEAT_METRICS
70+
global _STATSBEAT_EXPORTER
71+
shutdown_success = False
72+
if _STATSBEAT_METRICS is not None and _STATSBEAT_EXPORTER is not None and not _STATSBEAT_STATE["SHUTDOWN"]: # noqa: E501
73+
with _STATSBEAT_LOCK:
74+
try:
75+
_STATSBEAT_EXPORTER.shutdown()
76+
_STATSBEAT_EXPORTER = None
77+
_STATSBEAT_METRICS = None
78+
shutdown_success = True
79+
except: # pylint: disable=broad-except # noqa: E722
80+
pass
81+
if shutdown_success:
82+
with _STATSBEAT_STATE_LOCK:
83+
_STATSBEAT_STATE["SHUTDOWN"] = True
5884

5985

6086
class _AzureStatsbeatMetricsProducer(MetricProducer):

contrib/opencensus-ext-azure/opencensus/ext/azure/metrics_exporter/statsbeat_metrics/statsbeat.py renamed to contrib/opencensus-ext-azure/opencensus/ext/azure/statsbeat/statsbeat_metrics.py

File renamed without changes.

contrib/opencensus-ext-azure/opencensus/ext/azure/trace_exporter/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
)
3131
from opencensus.ext.azure.common.storage import LocalFileStorage
3232
from opencensus.ext.azure.common.transport import TransportMixin
33-
from opencensus.ext.azure.metrics_exporter import statsbeat_metrics
33+
from opencensus.ext.azure.statsbeat import statsbeat
3434
from opencensus.trace import attributes_helper
3535
from opencensus.trace.span import SpanKind
3636

@@ -76,7 +76,7 @@ def __init__(self, **options):
7676
atexit.register(self._stop, self.options.grace_period)
7777
# start statsbeat on exporter instantiation
7878
if not os.environ.get("APPLICATIONINSIGHTS_STATSBEAT_DISABLED_ALL"):
79-
statsbeat_metrics.collect_statsbeat_metrics(self.options)
79+
statsbeat.collect_statsbeat_metrics(self.options)
8080
# For redirects
8181
self._consecutive_redirects = 0 # To prevent circular redirects
8282

contrib/opencensus-ext-azure/tests/test_azure_metrics_exporter.py

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -206,11 +206,24 @@ def test_shutdown(self):
206206
mock_thread.close.assert_called_once()
207207
mock_storage.close.assert_called_once()
208208

209+
def test_shutdown_statsbeat(self):
210+
mock_thread = mock.Mock()
211+
mock_storage = mock.Mock()
212+
exporter = MetricsExporter(
213+
instrumentation_key='12345678-1234-5678-abcd-12345678abcd'
214+
)
215+
exporter.exporter_thread = mock_thread
216+
exporter._is_stats = True
217+
exporter.storage = mock_storage
218+
exporter.shutdown()
219+
mock_thread.cancel.assert_called_once()
220+
mock_storage.close.assert_called_once()
221+
209222
@mock.patch('opencensus.ext.azure.metrics_exporter'
210223
'.transport.get_exporter_thread')
211224
def test_new_metrics_exporter(self, exporter_mock):
212-
with mock.patch('opencensus.ext.azure.metrics_exporter'
213-
'.statsbeat_metrics.collect_statsbeat_metrics') as hb:
225+
with mock.patch('opencensus.ext.azure.statsbeat'
226+
'.statsbeat.collect_statsbeat_metrics') as hb:
214227
hb.return_value = None
215228
iKey = '12345678-1234-5678-abcd-12345678abcd'
216229
exporter = new_metrics_exporter(instrumentation_key=iKey)
@@ -227,8 +240,8 @@ def test_new_metrics_exporter(self, exporter_mock):
227240
@mock.patch('opencensus.ext.azure.metrics_exporter'
228241
'.transport.get_exporter_thread')
229242
def test_new_metrics_exporter_no_standard_metrics(self, exporter_mock):
230-
with mock.patch('opencensus.ext.azure.metrics_exporter'
231-
'.statsbeat_metrics.collect_statsbeat_metrics') as hb:
243+
with mock.patch('opencensus.ext.azure.statsbeat'
244+
'.statsbeat.collect_statsbeat_metrics') as hb:
232245
hb.return_value = None
233246
iKey = '12345678-1234-5678-abcd-12345678abcd'
234247
exporter = new_metrics_exporter(
@@ -240,18 +253,3 @@ def test_new_metrics_exporter_no_standard_metrics(self, exporter_mock):
240253
producer_class = standard_metrics.AzureStandardMetricsProducer
241254
self.assertFalse(isinstance(exporter_mock.call_args[0][0][0],
242255
producer_class))
243-
244-
@unittest.skip("Skip because disabling heartbeat metrics")
245-
@mock.patch('opencensus.ext.azure.metrics_exporter'
246-
'.transport.get_exporter_thread')
247-
def test_new_metrics_exporter_heartbeat(self, exporter_mock):
248-
with mock.patch('opencensus.ext.azure.metrics_exporter'
249-
'.statsbeat_metrics.collect_statsbeat_metrics') as hb:
250-
iKey = '12345678-1234-5678-abcd-12345678abcd'
251-
exporter = new_metrics_exporter(instrumentation_key=iKey)
252-
253-
self.assertEqual(exporter.options.instrumentation_key, iKey)
254-
self.assertEqual(len(hb.call_args_list), 1)
255-
self.assertEqual(len(hb.call_args[0]), 2)
256-
self.assertEqual(hb.call_args[0][0], None)
257-
self.assertEqual(hb.call_args[0][1], iKey)

0 commit comments

Comments
 (0)