polaris-python 0.0.1.dev3__py3-none-any.whl
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.
- polaris/__init__.py +74 -0
- polaris/_version.py +16 -0
- polaris/api/__init__.py +55 -0
- polaris/api/_log_file.py +232 -0
- polaris/api/config.py +145 -0
- polaris/api/consumer.py +240 -0
- polaris/api/log.py +414 -0
- polaris/api/lossless.py +73 -0
- polaris/api/provider.py +152 -0
- polaris/api/skill.py +162 -0
- polaris/pkg/__init__.py +15 -0
- polaris/pkg/common/__init__.py +15 -0
- polaris/pkg/common/plugin.py +33 -0
- polaris/pkg/config/__init__.py +15 -0
- polaris/pkg/config/api.py +1205 -0
- polaris/pkg/config/default.py +92 -0
- polaris/pkg/config/event.py +76 -0
- polaris/pkg/config/loader.py +392 -0
- polaris/pkg/config/stat_reporter.py +100 -0
- polaris/pkg/flow/__init__.py +15 -0
- polaris/pkg/flow/admin_server.py +104 -0
- polaris/pkg/flow/async_flow.py +55 -0
- polaris/pkg/flow/base_flow.py +111 -0
- polaris/pkg/flow/impl.py +1277 -0
- polaris/pkg/flow/lossless.py +605 -0
- polaris/pkg/flow/notify.py +46 -0
- polaris/pkg/flow/register.py +210 -0
- polaris/pkg/flow/skill.py +110 -0
- polaris/pkg/flow/sync_flow.py +20 -0
- polaris/pkg/model/__init__.py +15 -0
- polaris/pkg/model/context.py +53 -0
- polaris/pkg/model/contract.py +89 -0
- polaris/pkg/model/core.py +51 -0
- polaris/pkg/model/engine.py +83 -0
- polaris/pkg/model/error.py +94 -0
- polaris/pkg/model/event.py +159 -0
- polaris/pkg/model/lossless.py +102 -0
- polaris/pkg/model/rule.py +152 -0
- polaris/pkg/model/server.py +288 -0
- polaris/pkg/model/service.py +820 -0
- polaris/pkg/model/skill.py +179 -0
- polaris/pkg/model/stat.py +168 -0
- polaris/pkg/model/util.py +58 -0
- polaris/pkg/version/__init__.py +18 -0
- polaris/plugin/__init__.py +32 -0
- polaris/plugin/circuit_breaker/__init__.py +19 -0
- polaris/plugin/circuit_breaker/base.py +54 -0
- polaris/plugin/circuit_breaker/composite.py +265 -0
- polaris/plugin/circuit_breaker/config.py +36 -0
- polaris/plugin/event/__init__.py +21 -0
- polaris/plugin/event/pushgateway.py +202 -0
- polaris/plugin/load_balancer/__init__.py +24 -0
- polaris/plugin/load_balancer/base.py +30 -0
- polaris/plugin/load_balancer/random.py +39 -0
- polaris/plugin/load_balancer/weighted_random.py +50 -0
- polaris/plugin/local_registry/__init__.py +18 -0
- polaris/plugin/local_registry/base.py +47 -0
- polaris/plugin/local_registry/file_cache.py +335 -0
- polaris/plugin/local_registry/local_cache.py +433 -0
- polaris/plugin/server_connector/__init__.py +20 -0
- polaris/plugin/server_connector/base.py +104 -0
- polaris/plugin/server_connector/convert.py +149 -0
- polaris/plugin/server_connector/grpc_connector/__init__.py +21 -0
- polaris/plugin/server_connector/grpc_connector/connection.py +401 -0
- polaris/plugin/server_connector/grpc_connector/grpc_client.py +384 -0
- polaris/plugin/server_connector/grpc_connector/tls.py +45 -0
- polaris/plugin/service_router/__init__.py +28 -0
- polaris/plugin/service_router/base.py +105 -0
- polaris/plugin/service_router/isolated.py +35 -0
- polaris/plugin/service_router/metadata.py +84 -0
- polaris/plugin/service_router/recover.py +62 -0
- polaris/plugin/service_router/util.py +54 -0
- polaris/plugin/skill/__init__.py +16 -0
- polaris/plugin/skill/connector.py +127 -0
- polaris/plugin/skill/persistence.py +94 -0
- polaris/plugin/stat_reporter/__init__.py +16 -0
- polaris/plugin/stat_reporter/base.py +34 -0
- polaris/plugin/stat_reporter/prometheus.py +375 -0
- polaris/plugin_manager.py +146 -0
- polaris_python-0.0.1.dev3.dist-info/METADATA +213 -0
- polaris_python-0.0.1.dev3.dist-info/RECORD +84 -0
- polaris_python-0.0.1.dev3.dist-info/WHEEL +5 -0
- polaris_python-0.0.1.dev3.dist-info/licenses/LICENSE +18 -0
- polaris_python-0.0.1.dev3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
# Tencent is pleased to support the open source community by making polaris-python available.
|
|
2
|
+
#
|
|
3
|
+
# Copyright (C) 2021 Tencent. All rights reserved.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the BSD 3-Clause License (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# https://opensource.org/licenses/BSD-3-Clause
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software distributed
|
|
12
|
+
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
|
13
|
+
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
|
14
|
+
# specific language governing permissions and limitations under the License.
|
|
15
|
+
|
|
16
|
+
"""Java-compatible period gauges and Prometheus pull / Pushgateway export."""
|
|
17
|
+
|
|
18
|
+
import asyncio
|
|
19
|
+
import gzip
|
|
20
|
+
import random
|
|
21
|
+
import re
|
|
22
|
+
import time
|
|
23
|
+
from threading import RLock
|
|
24
|
+
from urllib.parse import quote, urlsplit, urlunsplit
|
|
25
|
+
|
|
26
|
+
from aiohttp import ClientSession, ClientTimeout, web
|
|
27
|
+
from prometheus_client import CollectorRegistry, Gauge, generate_latest
|
|
28
|
+
from prometheus_client.exposition import CONTENT_TYPE_LATEST
|
|
29
|
+
|
|
30
|
+
from polaris.pkg.model.error import ErrorCode, SDKError
|
|
31
|
+
from polaris.pkg.config import stat_reporter as stat_reporter_config
|
|
32
|
+
from polaris.pkg.model.service import GetOneInstanceRequest, RetStatus
|
|
33
|
+
from polaris.plugin_manager import PluginType, register_plugin
|
|
34
|
+
from .base import BaseStatReporter
|
|
35
|
+
|
|
36
|
+
LABELS = ("callee_namespace", "callee_service", "callee_method", "callee_instance",
|
|
37
|
+
"callee_instance_type", "callee_result_code", "callee_result", "caller_labels",
|
|
38
|
+
"caller_namespace", "caller_service", "caller_ip", "metric_name")
|
|
39
|
+
METRICS = {
|
|
40
|
+
"upstream_rq_total": "total of request per period",
|
|
41
|
+
"upstream_rq_success": "total of success request per period",
|
|
42
|
+
"upstream_rq_timeout": "total of request delay per period",
|
|
43
|
+
"upstream_rq_max_timeout": "maximum request delay per period",
|
|
44
|
+
"upstream_rq_min_timeout": "minimum request delay per period",
|
|
45
|
+
}
|
|
46
|
+
PULL_INTERVAL_SECONDS = 30
|
|
47
|
+
REVISION_LOG_INTERVAL_SECONDS = 30 * 60
|
|
48
|
+
PUSH_ERROR_LOG_INTERVAL_SECONDS = 30
|
|
49
|
+
NULL = "__NULL__"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@register_plugin(PluginType.STAT_REPORTER, "prometheus")
|
|
53
|
+
class PrometheusReporter(BaseStatReporter):
|
|
54
|
+
"""Own independent metrics, HTTP listener and push task for one context."""
|
|
55
|
+
|
|
56
|
+
def __init__(self, init_ctx):
|
|
57
|
+
self.global_config = init_ctx.config.get_global()
|
|
58
|
+
self.config = self.global_config.get_stat_reporter().get_prometheus()
|
|
59
|
+
self.enabled = self.global_config.get_stat_reporter().is_enable()
|
|
60
|
+
self.client_id = init_ctx.value_ctx.get_client_id()
|
|
61
|
+
from polaris.api.log import get_logger
|
|
62
|
+
self.logger = init_ctx.logger.get_logger("statReport") if init_ctx.logger else get_logger("statReport")
|
|
63
|
+
self.registry = CollectorRegistry()
|
|
64
|
+
self.gauges = {name: Gauge(name, description, LABELS, registry=self.registry)
|
|
65
|
+
for name, description in METRICS.items()}
|
|
66
|
+
self.lock = RLock()
|
|
67
|
+
self.period = 0
|
|
68
|
+
self.values = {}
|
|
69
|
+
self.task = None
|
|
70
|
+
self.runner = None
|
|
71
|
+
self._admin_server = None
|
|
72
|
+
self.session = None
|
|
73
|
+
self.push_count = 0
|
|
74
|
+
self._port = None
|
|
75
|
+
self._reported_port = None
|
|
76
|
+
self.engine = None
|
|
77
|
+
self.closed = False
|
|
78
|
+
self.started = False
|
|
79
|
+
self.random = random.Random()
|
|
80
|
+
self.push_lock = asyncio.Lock()
|
|
81
|
+
self.pushing = False
|
|
82
|
+
self._last_pull_revision_log = None
|
|
83
|
+
self._last_push_revision_log = None
|
|
84
|
+
self._last_push_error = ""
|
|
85
|
+
self._last_push_error_log = 0.0
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def port(self):
|
|
89
|
+
"""Return the shared admin port when mounted, otherwise the private one."""
|
|
90
|
+
return self._admin_server.port if self._admin_server is not None else self._port
|
|
91
|
+
|
|
92
|
+
def report(self, result):
|
|
93
|
+
"""Record Java label values and period count / delay aggregations."""
|
|
94
|
+
if not self.enabled or self.closed:
|
|
95
|
+
return
|
|
96
|
+
method = result.method
|
|
97
|
+
if method:
|
|
98
|
+
for pattern in self.config.path_regex_list:
|
|
99
|
+
if re.fullmatch(pattern, method):
|
|
100
|
+
method = pattern
|
|
101
|
+
break
|
|
102
|
+
caller = result.caller_service
|
|
103
|
+
ip = result.caller_ip
|
|
104
|
+
if ip is None:
|
|
105
|
+
ip = self.global_config.get_api().get_bind_ip()
|
|
106
|
+
labels = (result.get_namespace(), result.get_service(), method,
|
|
107
|
+
f"{result.get_host()}:{result.get_port()}", result.instance_type,
|
|
108
|
+
result.ret_code, RetStatus.descriptions[result.ret_status], result.labels,
|
|
109
|
+
caller.get_namespace() if caller else None,
|
|
110
|
+
caller.get_service() if caller else None, ip)
|
|
111
|
+
key = tuple(NULL if item is None else str(item) for item in labels)
|
|
112
|
+
delay = result.delay or 0
|
|
113
|
+
success = int(result.ret_status == RetStatus.RetSuccess)
|
|
114
|
+
with self.lock:
|
|
115
|
+
period = self.period + int(self.pushing)
|
|
116
|
+
existing = self.values.get(key)
|
|
117
|
+
if existing is None or existing[0] != period:
|
|
118
|
+
self.values[key] = (period, [1, success, delay, delay, delay])
|
|
119
|
+
else:
|
|
120
|
+
values = existing[1]
|
|
121
|
+
values[0] += 1
|
|
122
|
+
values[1] += success
|
|
123
|
+
if result.delay is not None:
|
|
124
|
+
values[2] += delay
|
|
125
|
+
values[3] = max(values[3], delay)
|
|
126
|
+
values[4] = min(values[4], delay)
|
|
127
|
+
|
|
128
|
+
def aggregate(self, advance=True):
|
|
129
|
+
"""Publish this period, zero stale series, remove after two idle revisions."""
|
|
130
|
+
with self.lock:
|
|
131
|
+
active_series = sum(
|
|
132
|
+
1 for revision, values in self.values.values()
|
|
133
|
+
if revision == self.period and values[0] > 0
|
|
134
|
+
)
|
|
135
|
+
request_count = sum(
|
|
136
|
+
values[0] for revision, values in self.values.values() if revision == self.period
|
|
137
|
+
)
|
|
138
|
+
for key, (revision, values) in list(self.values.items()):
|
|
139
|
+
for name, value in zip(METRICS, values):
|
|
140
|
+
labels = (*key, NULL)
|
|
141
|
+
if revision < self.period - 2:
|
|
142
|
+
self.gauges[name].remove(*labels)
|
|
143
|
+
else:
|
|
144
|
+
self.gauges[name].labels(*labels).set(value if revision == self.period else 0)
|
|
145
|
+
if revision < self.period - 2:
|
|
146
|
+
del self.values[key]
|
|
147
|
+
if advance:
|
|
148
|
+
self.period += 1
|
|
149
|
+
if not advance:
|
|
150
|
+
return
|
|
151
|
+
self.logger.debug(
|
|
152
|
+
"[metrics][%s] aggregate completed period=%d activeSeries=%d requests=%d retainedSeries=%d",
|
|
153
|
+
self.config.type, self.period, active_series, request_count, len(self.values),
|
|
154
|
+
)
|
|
155
|
+
now = time.monotonic()
|
|
156
|
+
if (self._last_pull_revision_log is None
|
|
157
|
+
or now - self._last_pull_revision_log >= REVISION_LOG_INTERVAL_SECONDS):
|
|
158
|
+
self.logger.info(
|
|
159
|
+
"[metrics][pull] revision collector inc current revision to %d", self.period,
|
|
160
|
+
)
|
|
161
|
+
self._last_pull_revision_log = now
|
|
162
|
+
|
|
163
|
+
def render(self):
|
|
164
|
+
"""Return the current exported Prometheus text without advancing the period."""
|
|
165
|
+
with self.lock:
|
|
166
|
+
data = generate_latest(self.registry)
|
|
167
|
+
self.logger.debug(
|
|
168
|
+
"[metrics][%s] render completed period=%d series=%d bytes=%d",
|
|
169
|
+
self.config.type, self.period, len(self.values), len(data),
|
|
170
|
+
)
|
|
171
|
+
return data
|
|
172
|
+
|
|
173
|
+
def meta_info(self):
|
|
174
|
+
"""Advertise the pull endpoint in the SDK client report, never in push mode."""
|
|
175
|
+
if not self.enabled or self.closed or self.port is None or self.config.type != "pull":
|
|
176
|
+
return None
|
|
177
|
+
port = self.port
|
|
178
|
+
if port != self._reported_port:
|
|
179
|
+
self._reported_port = port
|
|
180
|
+
self.logger.info(
|
|
181
|
+
"[metrics][pull] endpoint ready port=%s path=%s", port, self.config.path,
|
|
182
|
+
)
|
|
183
|
+
return {"target": "prometheus", "port": port, "path": self.config.path, "protocol": "http"}
|
|
184
|
+
|
|
185
|
+
async def start(self, engine):
|
|
186
|
+
"""Start pull or push resources after the SDK address is resolved."""
|
|
187
|
+
self.engine = engine
|
|
188
|
+
if not self.enabled:
|
|
189
|
+
self.logger.info("[metrics] reporter disabled by configuration")
|
|
190
|
+
return
|
|
191
|
+
admin = self.global_config.get_admin()
|
|
192
|
+
if self.config.type == "pull":
|
|
193
|
+
if admin.port < 0:
|
|
194
|
+
self.logger.warning(
|
|
195
|
+
"[metrics][pull] reporter disabled because global.admin.port < 0")
|
|
196
|
+
self.enabled = False
|
|
197
|
+
return
|
|
198
|
+
shared = getattr(engine, "admin_server", None) if engine is not None else None
|
|
199
|
+
if shared is not None:
|
|
200
|
+
shared.add_route("GET", self.config.path, self._metrics)
|
|
201
|
+
if self.config.path != "/-/healthy":
|
|
202
|
+
shared.add_route("GET", "/-/healthy", self._metrics)
|
|
203
|
+
self._admin_server = shared
|
|
204
|
+
else:
|
|
205
|
+
app = web.Application()
|
|
206
|
+
app.router.add_get(self.config.path, self._metrics)
|
|
207
|
+
if self.config.path != "/-/healthy":
|
|
208
|
+
app.router.add_get("/-/healthy", self._metrics)
|
|
209
|
+
self.runner = web.AppRunner(app, access_log=None)
|
|
210
|
+
await self.runner.setup()
|
|
211
|
+
listen_port = (admin.port if admin.port is not None
|
|
212
|
+
else stat_reporter_config.DEFAULT_ADMIN_PORT)
|
|
213
|
+
site = web.TCPSite(self.runner, admin.host, listen_port)
|
|
214
|
+
try:
|
|
215
|
+
await site.start()
|
|
216
|
+
except OSError as error:
|
|
217
|
+
await self.runner.cleanup()
|
|
218
|
+
self.runner = None
|
|
219
|
+
raise SDKError(ErrorCode.ErrCodeAPIInvalidConfig,
|
|
220
|
+
f"Cannot bind metrics listener {admin.host}:{admin.port}: {error}") from error
|
|
221
|
+
self._port = site._server.sockets[0].getsockname()[1]
|
|
222
|
+
else:
|
|
223
|
+
self.session = ClientSession(timeout=ClientTimeout(total=self.global_config.get_api().timeout / 1000))
|
|
224
|
+
self.task = asyncio.create_task(self._run(), name="polaris-prometheus")
|
|
225
|
+
self.started = True
|
|
226
|
+
interval = self.config.push_interval / 1000 if self.config.type == "push" else PULL_INTERVAL_SECONDS
|
|
227
|
+
port = self.port if self.port is not None else admin.port
|
|
228
|
+
self.logger.info(
|
|
229
|
+
"[metrics][%s] reporter started path=%s interval=%.3fs admin=%s:%s",
|
|
230
|
+
self.config.type, self.config.path, interval, admin.host, port,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
async def _metrics(self, request):
|
|
234
|
+
data = self.render()
|
|
235
|
+
self.logger.debug(
|
|
236
|
+
"[metrics][pull] served path=%s port=%s series=%d bytes=%d",
|
|
237
|
+
request.path, self.port, len(self.values), len(data),
|
|
238
|
+
)
|
|
239
|
+
return web.Response(body=data, headers={"Content-Type": CONTENT_TYPE_LATEST})
|
|
240
|
+
|
|
241
|
+
async def _address(self):
|
|
242
|
+
if self.config.address:
|
|
243
|
+
return self.random.choice(self.config.address)
|
|
244
|
+
if self.config.namespace and self.config.service:
|
|
245
|
+
namespace = self.config.namespace
|
|
246
|
+
service = self.config.service
|
|
247
|
+
else:
|
|
248
|
+
monitor = self.global_config.get_system().get_monitor_cluster()
|
|
249
|
+
if not monitor.get_namespace() or not monitor.get_service():
|
|
250
|
+
raise SDKError(
|
|
251
|
+
ErrorCode.ErrCodeAPIInvalidConfig,
|
|
252
|
+
"Prometheus push requires address or namespace/service from "
|
|
253
|
+
"plugin.prometheus or global.system.monitorCluster",
|
|
254
|
+
)
|
|
255
|
+
namespace = monitor.get_namespace()
|
|
256
|
+
service = monitor.get_service()
|
|
257
|
+
node = await self.engine._async_get_one_instance(GetOneInstanceRequest(
|
|
258
|
+
namespace=namespace, service=service))
|
|
259
|
+
if node is None:
|
|
260
|
+
raise SDKError(ErrorCode.ErrCodeAPIInstanceNotFound, "No Pushgateway instance available")
|
|
261
|
+
host = node.get_host()
|
|
262
|
+
return f"[{host}]:{node.get_port()}" if ":" in host else f"{host}:{node.get_port()}"
|
|
263
|
+
|
|
264
|
+
async def push(self):
|
|
265
|
+
"""POST gauges using Java job / grouping keys; failures retain the current period."""
|
|
266
|
+
async with self.push_lock:
|
|
267
|
+
await self._push()
|
|
268
|
+
|
|
269
|
+
async def _push(self):
|
|
270
|
+
address = await self._address()
|
|
271
|
+
url = urlsplit(address if "://" in address else f"http://{address}")
|
|
272
|
+
if self.config.override_host:
|
|
273
|
+
host = self.config.override_host
|
|
274
|
+
netloc = f"[{host}]" if ":" in host else host
|
|
275
|
+
if url.port:
|
|
276
|
+
netloc += f":{url.port}"
|
|
277
|
+
url = url._replace(netloc=netloc)
|
|
278
|
+
target = urlunsplit(url._replace(path=url.path.rstrip("/") +
|
|
279
|
+
f"/metrics/job/polaris-client/instance/{quote(self.client_id, safe='')}", query=""))
|
|
280
|
+
with self.lock:
|
|
281
|
+
self.aggregate(advance=False)
|
|
282
|
+
data = self.render()
|
|
283
|
+
snapshot = {key: (revision, list(values)) for key, (revision, values) in self.values.items()}
|
|
284
|
+
self.pushing = True
|
|
285
|
+
headers = {"Content-Type": CONTENT_TYPE_LATEST}
|
|
286
|
+
if self.config.open_gzip:
|
|
287
|
+
data = gzip.compress(data)
|
|
288
|
+
headers["Content-Encoding"] = "gzip"
|
|
289
|
+
try:
|
|
290
|
+
async with self.session.post(target, data=data, headers=headers) as response:
|
|
291
|
+
await response.read()
|
|
292
|
+
if not 200 <= response.status < 300:
|
|
293
|
+
raise SDKError(ErrorCode.ErrCodeNetworkError, f"Pushgateway returned HTTP {response.status}")
|
|
294
|
+
self.push_count += 1
|
|
295
|
+
except BaseException:
|
|
296
|
+
with self.lock:
|
|
297
|
+
for key, (revision, values) in list(self.values.items()):
|
|
298
|
+
if revision != self.period + 1:
|
|
299
|
+
continue
|
|
300
|
+
previous = snapshot.get(key)
|
|
301
|
+
if previous is not None and previous[0] == self.period:
|
|
302
|
+
old = previous[1]
|
|
303
|
+
values = [old[0] + values[0], old[1] + values[1], old[2] + values[2],
|
|
304
|
+
max(old[3], values[3]), min(old[4], values[4])]
|
|
305
|
+
self.values[key] = (self.period, values)
|
|
306
|
+
raise
|
|
307
|
+
else:
|
|
308
|
+
with self.lock:
|
|
309
|
+
self.period += 1
|
|
310
|
+
if self._last_push_error:
|
|
311
|
+
self.logger.info(
|
|
312
|
+
"[metrics][push] push metrics to pushgateway recovered (was failing: %s)",
|
|
313
|
+
self._last_push_error,
|
|
314
|
+
)
|
|
315
|
+
self._last_push_error = ""
|
|
316
|
+
self.logger.debug("[metrics][push] exported period=%d series=%d target=%s",
|
|
317
|
+
self.period, len(snapshot), target)
|
|
318
|
+
now = time.monotonic()
|
|
319
|
+
if (self._last_push_revision_log is None
|
|
320
|
+
or now - self._last_push_revision_log >= REVISION_LOG_INTERVAL_SECONDS):
|
|
321
|
+
self.logger.info(
|
|
322
|
+
"[metrics][push] revision collector inc current revision to %d", self.period,
|
|
323
|
+
)
|
|
324
|
+
self._last_push_revision_log = now
|
|
325
|
+
finally:
|
|
326
|
+
with self.lock:
|
|
327
|
+
self.pushing = False
|
|
328
|
+
|
|
329
|
+
async def _run(self):
|
|
330
|
+
interval = self.config.push_interval / 1000 if self.config.type == "push" else PULL_INTERVAL_SECONDS
|
|
331
|
+
while True:
|
|
332
|
+
await asyncio.sleep(interval)
|
|
333
|
+
try:
|
|
334
|
+
if self.config.type == "push":
|
|
335
|
+
await self.push()
|
|
336
|
+
else:
|
|
337
|
+
self.aggregate()
|
|
338
|
+
except Exception as error:
|
|
339
|
+
if self.config.type == "push":
|
|
340
|
+
self._log_push_error(error)
|
|
341
|
+
else:
|
|
342
|
+
self.logger.warning("Metrics export failed; retaining current period: %s", error)
|
|
343
|
+
|
|
344
|
+
def _log_push_error(self, error):
|
|
345
|
+
error_text = str(error)
|
|
346
|
+
now = time.monotonic()
|
|
347
|
+
if (error_text != self._last_push_error
|
|
348
|
+
or now - self._last_push_error_log >= PUSH_ERROR_LOG_INTERVAL_SECONDS):
|
|
349
|
+
if not self._last_push_error:
|
|
350
|
+
self.logger.error("push metrics to pushgateway fail: %s", error_text)
|
|
351
|
+
else:
|
|
352
|
+
self.logger.warning(
|
|
353
|
+
"push metrics to pushgateway still failing (last error unchanged for %.0fs): %s",
|
|
354
|
+
now - self._last_push_error_log, error_text,
|
|
355
|
+
)
|
|
356
|
+
self._last_push_error_log = now
|
|
357
|
+
self._last_push_error = error_text
|
|
358
|
+
|
|
359
|
+
async def close(self):
|
|
360
|
+
"""Cancel the task before closing its listener and outgoing HTTP session."""
|
|
361
|
+
self.closed = True
|
|
362
|
+
self._reported_port = None
|
|
363
|
+
if self.task is not None:
|
|
364
|
+
self.task.cancel()
|
|
365
|
+
await asyncio.gather(self.task, return_exceptions=True)
|
|
366
|
+
self.task = None
|
|
367
|
+
if self.runner is not None:
|
|
368
|
+
await self.runner.cleanup()
|
|
369
|
+
self.runner = None
|
|
370
|
+
if self.session is not None:
|
|
371
|
+
await self.session.close()
|
|
372
|
+
self.session = None
|
|
373
|
+
if self.started:
|
|
374
|
+
self.started = False
|
|
375
|
+
self.logger.info("[metrics][%s] reporter stopped", self.config.type)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Tencent is pleased to support the open source community by making polaris-python available.
|
|
2
|
+
#
|
|
3
|
+
# Copyright (C) 2021 Tencent. All rights reserved.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the BSD 3-Clause License (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# https://opensource.org/licenses/BSD-3-Clause
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software distributed
|
|
12
|
+
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
|
13
|
+
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
|
14
|
+
# specific language governing permissions and limitations under the License.
|
|
15
|
+
|
|
16
|
+
from threading import RLock
|
|
17
|
+
|
|
18
|
+
from polaris.pkg.model.context import ValueContext
|
|
19
|
+
from polaris.pkg.config.api import Configuration
|
|
20
|
+
from polaris.pkg.model.error import ErrorCode, SDKError
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PluginType:
|
|
24
|
+
SERVER_CONNECTOR = 2
|
|
25
|
+
SERVICE_ROUTER = 3
|
|
26
|
+
LOAD_BALANCE = 4
|
|
27
|
+
CIRCUIT_BREAKER = 5
|
|
28
|
+
LOCAL_CACHE = 6
|
|
29
|
+
STAT_REPORTER = 7
|
|
30
|
+
SKILL_CONNECTOR = 8
|
|
31
|
+
EVENT_REPORTER = 9
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class EventType:
|
|
35
|
+
ON_LOCALVALUE_CREATED = 1
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class PluginManager:
|
|
39
|
+
"""Own plugin instances and event subscribers for one SDK context."""
|
|
40
|
+
|
|
41
|
+
def __init__(self):
|
|
42
|
+
self.plugin_container = {}
|
|
43
|
+
self.plugin_instance = {}
|
|
44
|
+
self.event_handle_map = {}
|
|
45
|
+
self._lock = RLock()
|
|
46
|
+
|
|
47
|
+
def register(self, plugin_type: PluginType, plugin_name: str, plugin_cls):
|
|
48
|
+
"""Register a plugin constructor without creating a runtime instance."""
|
|
49
|
+
with self._lock:
|
|
50
|
+
self.plugin_container.setdefault(plugin_type, {})[plugin_name] = plugin_cls
|
|
51
|
+
|
|
52
|
+
def new_context_manager(self):
|
|
53
|
+
"""Copy registered constructors into an empty, independent runtime manager."""
|
|
54
|
+
manager = PluginManager()
|
|
55
|
+
with self._lock:
|
|
56
|
+
manager.plugin_container = {typ: dict(plugins) for typ, plugins in self.plugin_container.items()}
|
|
57
|
+
return manager
|
|
58
|
+
|
|
59
|
+
def get(self, plugin_type: PluginType, plugin_name: str):
|
|
60
|
+
"""Return a registered plugin constructor."""
|
|
61
|
+
if plugin_type in self.plugin_container:
|
|
62
|
+
return self.plugin_container[plugin_type][plugin_name]
|
|
63
|
+
else:
|
|
64
|
+
return ""
|
|
65
|
+
|
|
66
|
+
def find_instance(self, plugin_type: PluginType, plugin_name: str) -> bool:
|
|
67
|
+
"""Check whether this context has created a plugin instance."""
|
|
68
|
+
if plugin_type in self.plugin_instance and plugin_name in self.plugin_instance[plugin_type]:
|
|
69
|
+
return True
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
def get_instance(self, plugin_type: PluginType, plugin_name: str):
|
|
73
|
+
"""Return this context's plugin instance or a configuration diagnostic."""
|
|
74
|
+
try:
|
|
75
|
+
return self.plugin_instance[plugin_type][plugin_name]
|
|
76
|
+
except KeyError as error:
|
|
77
|
+
raise SDKError(ErrorCode.ErrCodePluginError,
|
|
78
|
+
f"Plugin is not available: {plugin_type}:{plugin_name}") from error
|
|
79
|
+
|
|
80
|
+
def set_instance(self, plugin_type: PluginType, plugin_name: str, instance):
|
|
81
|
+
"""Store an instance owned exclusively by this context."""
|
|
82
|
+
if plugin_type not in self.plugin_instance:
|
|
83
|
+
self.plugin_instance[plugin_type] = {}
|
|
84
|
+
|
|
85
|
+
self.plugin_instance[plugin_type][plugin_name] = instance
|
|
86
|
+
|
|
87
|
+
def clear_instances(self):
|
|
88
|
+
"""Forget this context's instances and their event subscribers."""
|
|
89
|
+
self.plugin_instance.clear()
|
|
90
|
+
self.event_handle_map.clear()
|
|
91
|
+
|
|
92
|
+
def destroy_plugins(self):
|
|
93
|
+
"""Release synchronous plugin resources after the owner loop has stopped."""
|
|
94
|
+
instances = [instance for plugins in self.plugin_instance.values() for instance in plugins.values()]
|
|
95
|
+
self.clear_instances()
|
|
96
|
+
first_error = None
|
|
97
|
+
for instance in reversed(instances):
|
|
98
|
+
destroy = getattr(instance, "destroy", None)
|
|
99
|
+
if destroy is not None:
|
|
100
|
+
try:
|
|
101
|
+
destroy()
|
|
102
|
+
except Exception as error:
|
|
103
|
+
if first_error is None:
|
|
104
|
+
first_error = error
|
|
105
|
+
if first_error is not None:
|
|
106
|
+
raise first_error
|
|
107
|
+
|
|
108
|
+
def register_event_handle(self, event_type, handler):
|
|
109
|
+
"""Subscribe a context-owned handler to a plugin event."""
|
|
110
|
+
self.event_handle_map.setdefault(event_type, []).append(handler)
|
|
111
|
+
|
|
112
|
+
def get_event_subscriber(self, event_type):
|
|
113
|
+
"""Return only this context's subscribers, or an empty tuple."""
|
|
114
|
+
return tuple(self.event_handle_map.get(event_type, ()))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def register_plugin(plugin_type: PluginType, plugin_name: str):
|
|
118
|
+
def decorator(cls):
|
|
119
|
+
DefaultPluginManager.register(plugin_type, plugin_name, cls)
|
|
120
|
+
return cls
|
|
121
|
+
|
|
122
|
+
return decorator
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# Process-wide registration stores constructors only, never SDK runtime state.
|
|
126
|
+
DefaultPluginManager = PluginManager()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class InitContext:
|
|
130
|
+
"""Provide configuration and context-owned dependencies during plugin creation."""
|
|
131
|
+
|
|
132
|
+
def __init__(self, config: Configuration = None, value_ctx: ValueContext = None,
|
|
133
|
+
plugins: PluginManager = None):
|
|
134
|
+
self.config = config
|
|
135
|
+
self.value_ctx = value_ctx
|
|
136
|
+
self.plugins = plugins if plugins is not None else PluginManager()
|
|
137
|
+
self.logger = getattr(value_ctx, "logger", None)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def setup(init_ctx: InitContext):
|
|
141
|
+
"""Create plugins in the supplied context manager, leaving the registry untouched."""
|
|
142
|
+
manager = init_ctx.plugins
|
|
143
|
+
for plugin_type, plugins in manager.plugin_container.items():
|
|
144
|
+
for plugin_name, plugin in plugins.items():
|
|
145
|
+
if not manager.find_instance(plugin_type, plugin_name):
|
|
146
|
+
manager.set_instance(plugin_type, plugin_name, plugin(init_ctx))
|