polaris-python-ai 0.0.1.dev2__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_ai/__init__.py +34 -0
- polaris_ai/_async.py +39 -0
- polaris_ai/_version.py +18 -0
- polaris_ai/a2a/__init__.py +31 -0
- polaris_ai/a2a/_card.py +44 -0
- polaris_ai/a2a/registry.py +240 -0
- polaris_ai/a2a/resolver.py +198 -0
- polaris_ai/a2a/server.py +92 -0
- polaris_ai/agentscope/__init__.py +76 -0
- polaris_ai/agentscope/a2a.py +276 -0
- polaris_ai/agentscope/cards.py +106 -0
- polaris_ai/agentscope/config.py +221 -0
- polaris_ai/agentscope/context.py +195 -0
- polaris_ai/agentscope/mcp.py +598 -0
- polaris_ai/agentscope/middleware.py +103 -0
- polaris_ai/agentscope/runtime.py +291 -0
- polaris_ai/agentscope/skill.py +464 -0
- polaris_ai/langgraph/__init__.py +50 -0
- polaris_ai/langgraph/config.py +160 -0
- polaris_ai/langgraph/context.py +152 -0
- polaris_ai/langgraph/registry.py +258 -0
- polaris_ai/langgraph/remote.py +315 -0
- polaris_ai/langgraph/resolver.py +226 -0
- polaris_ai/log.py +408 -0
- polaris_ai/metrics.py +154 -0
- polaris_python_ai-0.0.1.dev2.dist-info/METADATA +351 -0
- polaris_python_ai-0.0.1.dev2.dist-info/RECORD +30 -0
- polaris_python_ai-0.0.1.dev2.dist-info/WHEEL +5 -0
- polaris_python_ai-0.0.1.dev2.dist-info/licenses/LICENSE +27 -0
- polaris_python_ai-0.0.1.dev2.dist-info/top_level.txt +1 -0
polaris_ai/log.py
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
# Tencent is pleased to support the open source community by making polaris-python-ai available.
|
|
2
|
+
#
|
|
3
|
+
# Copyright (C) 2026 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
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
"""Optional Polaris-managed logging for AI adapters."""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import asyncio
|
|
22
|
+
import logging
|
|
23
|
+
import os
|
|
24
|
+
import time
|
|
25
|
+
from contextlib import contextmanager
|
|
26
|
+
from contextvars import ContextVar
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from threading import RLock
|
|
30
|
+
|
|
31
|
+
from polaris.api._log_file import (
|
|
32
|
+
DEFAULT_BACKUP_COUNT,
|
|
33
|
+
DEFAULT_MAX_AGE_DAYS,
|
|
34
|
+
DEFAULT_MAX_BYTES,
|
|
35
|
+
ExclusiveRotatingFileHandler,
|
|
36
|
+
LogRetention,
|
|
37
|
+
reserve_log_files,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
from polaris_ai._async import await_completion
|
|
41
|
+
|
|
42
|
+
LOG_CATEGORIES = ("base", "agentscope", "skill", "a2a", "mcp", "langgraph", "metrics")
|
|
43
|
+
DEFAULT_AI_LOG_DIR = "./polaris/log/ai"
|
|
44
|
+
DEFAULT_WARNING_THROTTLE_SECONDS = 30.0
|
|
45
|
+
|
|
46
|
+
_LEVELS = {
|
|
47
|
+
"debug": logging.DEBUG,
|
|
48
|
+
"info": logging.INFO,
|
|
49
|
+
"warn": logging.WARN,
|
|
50
|
+
"warning": logging.WARNING,
|
|
51
|
+
"error": logging.ERROR,
|
|
52
|
+
}
|
|
53
|
+
_bound_logger: ContextVar["PolarisAILogger | None"] = ContextVar(
|
|
54
|
+
"polaris_ai_logger", default=None,
|
|
55
|
+
)
|
|
56
|
+
_agentscope_owner_lock = RLock()
|
|
57
|
+
_agentscope_owner: "PolarisAILogger | None" = None
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class PolarisAILogOptions:
|
|
60
|
+
"""Code-first options for Polaris AI extension logs.
|
|
61
|
+
|
|
62
|
+
Passing options enables Polaris-managed files. ``None`` leaves logging to
|
|
63
|
+
the host application's Python logging configuration.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
level: str | None = None
|
|
67
|
+
file_path: str | Path | None = None
|
|
68
|
+
log_dir: str | Path | None = None
|
|
69
|
+
max_bytes: int = DEFAULT_MAX_BYTES
|
|
70
|
+
backup_count: int = DEFAULT_BACKUP_COUNT
|
|
71
|
+
max_age_days: int = DEFAULT_MAX_AGE_DAYS
|
|
72
|
+
capture_agentscope: bool | None = None
|
|
73
|
+
warning_throttle_seconds: float = DEFAULT_WARNING_THROTTLE_SECONDS
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _boolean(value: str | None, default: bool) -> bool:
|
|
77
|
+
if value is None or not value.strip():
|
|
78
|
+
return default
|
|
79
|
+
lowered = value.strip().lower()
|
|
80
|
+
if lowered in ("1", "true", "yes", "on"):
|
|
81
|
+
return True
|
|
82
|
+
if lowered in ("0", "false", "no", "off"):
|
|
83
|
+
return False
|
|
84
|
+
raise ValueError(f"Invalid boolean value: {value!r}")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _validate_category(category: str):
|
|
88
|
+
if category not in LOG_CATEGORIES:
|
|
89
|
+
raise ValueError(f"Unknown AI log category: {category}")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _resolved_level(options: PolarisAILogOptions) -> tuple[str, int]:
|
|
93
|
+
level = options.level
|
|
94
|
+
if level is None:
|
|
95
|
+
level = os.getenv("POLARIS_AI_LOG_LEVEL", "info")
|
|
96
|
+
if not isinstance(level, str) or level.lower() not in _LEVELS:
|
|
97
|
+
raise ValueError(f"Invalid AI log level: {level!r}")
|
|
98
|
+
level = level.lower()
|
|
99
|
+
return level, _LEVELS[level]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _resolve_paths(
|
|
103
|
+
options: PolarisAILogOptions,
|
|
104
|
+
client_id: str,
|
|
105
|
+
) -> tuple[dict[str, Path], tuple[Path, ...]]:
|
|
106
|
+
file_path = options.file_path
|
|
107
|
+
log_dir = options.log_dir
|
|
108
|
+
if file_path is not None and log_dir is not None:
|
|
109
|
+
raise ValueError("Specify either log_dir or file_path")
|
|
110
|
+
|
|
111
|
+
if file_path is None and log_dir is None:
|
|
112
|
+
configured = os.getenv("POLARIS_AI_LOG_PATH")
|
|
113
|
+
if configured:
|
|
114
|
+
candidate = Path(configured).expanduser()
|
|
115
|
+
if (
|
|
116
|
+
candidate.is_dir()
|
|
117
|
+
or not candidate.suffix
|
|
118
|
+
or configured.endswith(("/", "\\"))
|
|
119
|
+
):
|
|
120
|
+
log_dir = candidate
|
|
121
|
+
else:
|
|
122
|
+
file_path = candidate
|
|
123
|
+
else:
|
|
124
|
+
log_dir = DEFAULT_AI_LOG_DIR
|
|
125
|
+
|
|
126
|
+
if log_dir is not None:
|
|
127
|
+
root = Path(log_dir).expanduser().resolve()
|
|
128
|
+
paths = {
|
|
129
|
+
category: (
|
|
130
|
+
root / category / f"polaris-ai-{category}-{client_id}.log"
|
|
131
|
+
).resolve()
|
|
132
|
+
for category in LOG_CATEGORIES
|
|
133
|
+
}
|
|
134
|
+
families = tuple(
|
|
135
|
+
(root / category / f"polaris-ai-{category}.log").resolve()
|
|
136
|
+
for category in LOG_CATEGORIES
|
|
137
|
+
)
|
|
138
|
+
return paths, families
|
|
139
|
+
|
|
140
|
+
base = Path(file_path).expanduser().resolve()
|
|
141
|
+
root = base.parent
|
|
142
|
+
stem = base.stem
|
|
143
|
+
suffix = base.suffix or ".log"
|
|
144
|
+
paths = {
|
|
145
|
+
category: (root / category / f"{stem}-{category}{suffix}").resolve()
|
|
146
|
+
for category in LOG_CATEGORIES
|
|
147
|
+
}
|
|
148
|
+
paths["base"] = base
|
|
149
|
+
return paths, ()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class PolarisAILogger:
|
|
153
|
+
"""Own the managed files and handlers for one AI runtime context."""
|
|
154
|
+
|
|
155
|
+
def __init__(
|
|
156
|
+
self,
|
|
157
|
+
client_id: str,
|
|
158
|
+
options: PolarisAILogOptions | None = None,
|
|
159
|
+
):
|
|
160
|
+
if not isinstance(client_id, str) or not client_id.strip():
|
|
161
|
+
raise ValueError("A non-empty client_id is required")
|
|
162
|
+
if options is None:
|
|
163
|
+
options = PolarisAILogOptions()
|
|
164
|
+
if not isinstance(options, PolarisAILogOptions):
|
|
165
|
+
raise TypeError("options must be PolarisAILogOptions")
|
|
166
|
+
self.client_id = client_id
|
|
167
|
+
self.options = options
|
|
168
|
+
self.level, self._level = _resolved_level(options)
|
|
169
|
+
if (
|
|
170
|
+
type(options.max_bytes) is not int
|
|
171
|
+
or options.max_bytes <= 0
|
|
172
|
+
or type(options.backup_count) is not int
|
|
173
|
+
or options.backup_count < 0
|
|
174
|
+
or type(options.max_age_days) is not int
|
|
175
|
+
or options.max_age_days < 0
|
|
176
|
+
or isinstance(options.warning_throttle_seconds, bool)
|
|
177
|
+
or not isinstance(options.warning_throttle_seconds, (int, float))
|
|
178
|
+
or options.warning_throttle_seconds < 0
|
|
179
|
+
):
|
|
180
|
+
raise ValueError("Invalid AI log rotation options")
|
|
181
|
+
self._paths, self._family_paths = _resolve_paths(options, self.client_id)
|
|
182
|
+
self._loggers: dict[str, logging.Logger] = {}
|
|
183
|
+
self._handlers: dict[str, logging.Handler] = {}
|
|
184
|
+
self._agentscope_handler: logging.Handler | None = None
|
|
185
|
+
self._lock = RLock()
|
|
186
|
+
self._closed = False
|
|
187
|
+
try:
|
|
188
|
+
self._locks = reserve_log_files(self._paths.values())
|
|
189
|
+
self._retention = LogRetention(
|
|
190
|
+
self._locks,
|
|
191
|
+
options.max_age_days,
|
|
192
|
+
options.backup_count,
|
|
193
|
+
self._family_paths,
|
|
194
|
+
)
|
|
195
|
+
capture = (
|
|
196
|
+
options.capture_agentscope
|
|
197
|
+
if options.capture_agentscope is not None
|
|
198
|
+
else _boolean(os.getenv("POLARIS_AI_LOG_CAPTURE_AGENTSCOPE"), False)
|
|
199
|
+
)
|
|
200
|
+
if capture:
|
|
201
|
+
self._attach_agentscope()
|
|
202
|
+
self._retention.start()
|
|
203
|
+
except BaseException:
|
|
204
|
+
self.close()
|
|
205
|
+
raise
|
|
206
|
+
|
|
207
|
+
def _create_category(self, category: str):
|
|
208
|
+
path = self._paths[category]
|
|
209
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
210
|
+
handler = ExclusiveRotatingFileHandler(
|
|
211
|
+
path,
|
|
212
|
+
"a",
|
|
213
|
+
self.options.max_bytes,
|
|
214
|
+
self.options.backup_count,
|
|
215
|
+
encoding="utf-8",
|
|
216
|
+
max_age_days=self.options.max_age_days,
|
|
217
|
+
)
|
|
218
|
+
handler.setLevel(self._level)
|
|
219
|
+
handler.setFormatter(logging.Formatter(
|
|
220
|
+
f"[%(asctime)s][%(levelname)s][%(process)d]"
|
|
221
|
+
f"[clientId={self.client_id}][%(name)s]: %(message)s",
|
|
222
|
+
))
|
|
223
|
+
logger = logging.Logger(
|
|
224
|
+
f"polaris_ai.{self.client_id}.{category}",
|
|
225
|
+
level=self._level,
|
|
226
|
+
)
|
|
227
|
+
logger.addHandler(handler)
|
|
228
|
+
logger.propagate = False
|
|
229
|
+
with self._retention.lock:
|
|
230
|
+
self._retention.add_handler(handler)
|
|
231
|
+
self._handlers[category] = handler
|
|
232
|
+
self._loggers[category] = logger
|
|
233
|
+
return logger
|
|
234
|
+
|
|
235
|
+
def _logger_for(self, category):
|
|
236
|
+
_validate_category(category)
|
|
237
|
+
if self._closed:
|
|
238
|
+
raise RuntimeError("PolarisAI logger is closed")
|
|
239
|
+
with self._lock:
|
|
240
|
+
if category not in self._loggers:
|
|
241
|
+
return self._create_category(category)
|
|
242
|
+
return self._loggers[category]
|
|
243
|
+
|
|
244
|
+
def _attach_agentscope(self):
|
|
245
|
+
global _agentscope_owner
|
|
246
|
+
with _agentscope_owner_lock:
|
|
247
|
+
if _agentscope_owner is not None and _agentscope_owner is not self:
|
|
248
|
+
raise ValueError("AgentScope logging is already captured by another AI runtime")
|
|
249
|
+
handler = self._create_category("agentscope").handlers[0]
|
|
250
|
+
agentscope_logger = logging.getLogger("as")
|
|
251
|
+
if handler not in agentscope_logger.handlers:
|
|
252
|
+
agentscope_logger.addHandler(handler)
|
|
253
|
+
self._agentscope_handler = handler
|
|
254
|
+
_agentscope_owner = self
|
|
255
|
+
|
|
256
|
+
def get_logger(self, category="base"):
|
|
257
|
+
"""Return a lazy category logger for this runtime."""
|
|
258
|
+
_validate_category(category)
|
|
259
|
+
return _CategoryLogger(
|
|
260
|
+
self,
|
|
261
|
+
category,
|
|
262
|
+
self.options.warning_throttle_seconds,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
def close(self):
|
|
266
|
+
"""Close owned handlers and release every reserved path."""
|
|
267
|
+
global _agentscope_owner
|
|
268
|
+
with self._lock:
|
|
269
|
+
if self._closed:
|
|
270
|
+
return
|
|
271
|
+
self._closed = True
|
|
272
|
+
try:
|
|
273
|
+
with _agentscope_owner_lock:
|
|
274
|
+
if self._agentscope_handler is not None:
|
|
275
|
+
logging.getLogger("as").removeHandler(self._agentscope_handler)
|
|
276
|
+
self._agentscope_handler = None
|
|
277
|
+
if _agentscope_owner is self:
|
|
278
|
+
_agentscope_owner = None
|
|
279
|
+
finally:
|
|
280
|
+
try:
|
|
281
|
+
if hasattr(self, "_retention"):
|
|
282
|
+
self._retention.close()
|
|
283
|
+
finally:
|
|
284
|
+
for category, logger in self._loggers.items():
|
|
285
|
+
handler = self._handlers.get(category)
|
|
286
|
+
if handler is not None:
|
|
287
|
+
logger.removeHandler(handler)
|
|
288
|
+
handler.close()
|
|
289
|
+
self._handlers.clear()
|
|
290
|
+
self._loggers.clear()
|
|
291
|
+
for lock in getattr(self, "_locks", {}).values():
|
|
292
|
+
lock.close()
|
|
293
|
+
|
|
294
|
+
async def async_close(self):
|
|
295
|
+
await await_completion(asyncio.to_thread(self.close))
|
|
296
|
+
|
|
297
|
+
def __enter__(self):
|
|
298
|
+
return self
|
|
299
|
+
|
|
300
|
+
def __exit__(self, *_):
|
|
301
|
+
self.close()
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
class _ClosedLogger:
|
|
305
|
+
"""Swallow late diagnostics after a managed runtime has closed."""
|
|
306
|
+
|
|
307
|
+
@staticmethod
|
|
308
|
+
def debug(*_args, **_kwargs):
|
|
309
|
+
return None
|
|
310
|
+
|
|
311
|
+
@staticmethod
|
|
312
|
+
def info(*_args, **_kwargs):
|
|
313
|
+
return None
|
|
314
|
+
|
|
315
|
+
@staticmethod
|
|
316
|
+
def warning(*_args, **_kwargs):
|
|
317
|
+
return None
|
|
318
|
+
|
|
319
|
+
@staticmethod
|
|
320
|
+
def error(*_args, **_kwargs):
|
|
321
|
+
return None
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
_CLOSED_LOGGER = _ClosedLogger()
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
class _CategoryLogger:
|
|
328
|
+
"""Resolve a category logger at emission time without changing std logging."""
|
|
329
|
+
|
|
330
|
+
def __init__(
|
|
331
|
+
self,
|
|
332
|
+
manager: PolarisAILogger | None,
|
|
333
|
+
category: str,
|
|
334
|
+
warning_throttle_seconds: float = DEFAULT_WARNING_THROTTLE_SECONDS,
|
|
335
|
+
):
|
|
336
|
+
_validate_category(category)
|
|
337
|
+
self._manager = manager
|
|
338
|
+
self.category = category
|
|
339
|
+
self._warning_interval = warning_throttle_seconds
|
|
340
|
+
self._warning_lock = RLock()
|
|
341
|
+
self._warning_state: dict[object, tuple[float, int]] = {}
|
|
342
|
+
|
|
343
|
+
def _target(self):
|
|
344
|
+
manager = self._manager
|
|
345
|
+
if manager is not None:
|
|
346
|
+
try:
|
|
347
|
+
return manager._logger_for(self.category)
|
|
348
|
+
except RuntimeError:
|
|
349
|
+
if manager._closed:
|
|
350
|
+
return _CLOSED_LOGGER
|
|
351
|
+
raise
|
|
352
|
+
bound = _bound_logger.get()
|
|
353
|
+
if bound is not None:
|
|
354
|
+
try:
|
|
355
|
+
return bound._logger_for(self.category)
|
|
356
|
+
except RuntimeError:
|
|
357
|
+
if bound._closed:
|
|
358
|
+
return _CLOSED_LOGGER
|
|
359
|
+
raise
|
|
360
|
+
return logging.getLogger(f"polaris_ai.{self.category}")
|
|
361
|
+
|
|
362
|
+
def debug(self, msg, *args, **kwargs):
|
|
363
|
+
self._target().debug(msg, *args, **kwargs)
|
|
364
|
+
|
|
365
|
+
def info(self, msg, *args, **kwargs):
|
|
366
|
+
self._target().info(msg, *args, **kwargs)
|
|
367
|
+
|
|
368
|
+
def warning(self, msg, *args, **kwargs):
|
|
369
|
+
self._target().warning(msg, *args, **kwargs)
|
|
370
|
+
|
|
371
|
+
def warning_throttled(self, key, msg, *args, **kwargs):
|
|
372
|
+
"""Emit a warning at most once per interval and report suppressed count."""
|
|
373
|
+
now = time.monotonic()
|
|
374
|
+
with self._warning_lock:
|
|
375
|
+
last, suppressed = self._warning_state.get(key, (0.0, 0))
|
|
376
|
+
if last and now - last < self._warning_interval:
|
|
377
|
+
self._warning_state[key] = (last, suppressed + 1)
|
|
378
|
+
return
|
|
379
|
+
self._warning_state[key] = (now, 0)
|
|
380
|
+
if suppressed:
|
|
381
|
+
msg = f"{msg} [suppressed {suppressed} similar warnings]"
|
|
382
|
+
self._target().warning(msg, *args, **kwargs)
|
|
383
|
+
|
|
384
|
+
def error(self, msg, *args, **kwargs):
|
|
385
|
+
self._target().error(msg, *args, **kwargs)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def get_ai_logger(category="base"):
|
|
389
|
+
"""Return a category logger that uses managed AI logging when configured."""
|
|
390
|
+
return _CategoryLogger(None, category)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def configure_ai_logging(
|
|
394
|
+
client_id: str,
|
|
395
|
+
options: PolarisAILogOptions | None = None,
|
|
396
|
+
) -> PolarisAILogger:
|
|
397
|
+
"""Create an owned AI log manager for a runtime or application."""
|
|
398
|
+
return PolarisAILogger(client_id, options)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
@contextmanager
|
|
402
|
+
def use_ai_logger(instance: PolarisAILogger | None):
|
|
403
|
+
"""Bind a managed logger to the current async task or thread."""
|
|
404
|
+
token = _bound_logger.set(instance)
|
|
405
|
+
try:
|
|
406
|
+
yield
|
|
407
|
+
finally:
|
|
408
|
+
_bound_logger.reset(token)
|
polaris_ai/metrics.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Tencent is pleased to support the open source community by making polaris-python-ai available.
|
|
2
|
+
#
|
|
3
|
+
# Copyright (C) 2026 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
|
+
"""Framework-independent call result reporting for AI adapters."""
|
|
17
|
+
|
|
18
|
+
import time
|
|
19
|
+
|
|
20
|
+
from polaris.pkg.model.service import (
|
|
21
|
+
RetStatus,
|
|
22
|
+
ServiceCallResult,
|
|
23
|
+
ServiceInfo,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
from polaris_ai.log import get_ai_logger
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class PolarisCallReporter:
|
|
30
|
+
"""Report one logical AI call without changing its business result."""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
consumer,
|
|
35
|
+
namespace: str,
|
|
36
|
+
*,
|
|
37
|
+
caller_service: str = "",
|
|
38
|
+
labels: str = "",
|
|
39
|
+
caller_ip: str = "",
|
|
40
|
+
logger=None,
|
|
41
|
+
):
|
|
42
|
+
if consumer is None:
|
|
43
|
+
raise ValueError("A ConsumerAPI is required")
|
|
44
|
+
self.consumer = consumer
|
|
45
|
+
self.namespace = namespace.strip() or "default"
|
|
46
|
+
self.caller_service = caller_service.strip()
|
|
47
|
+
self.labels = labels
|
|
48
|
+
self.caller_ip = caller_ip
|
|
49
|
+
self.logger = logger if logger is not None else get_ai_logger("metrics")
|
|
50
|
+
|
|
51
|
+
def _build_result(
|
|
52
|
+
self,
|
|
53
|
+
*,
|
|
54
|
+
service: str,
|
|
55
|
+
host: str,
|
|
56
|
+
port: int,
|
|
57
|
+
method: str,
|
|
58
|
+
instance_type: str,
|
|
59
|
+
started_at: float,
|
|
60
|
+
success: bool,
|
|
61
|
+
ret_code: int | None = None,
|
|
62
|
+
timeout: bool = False,
|
|
63
|
+
):
|
|
64
|
+
if not service or not host:
|
|
65
|
+
return None
|
|
66
|
+
status = RetStatus.RetSuccess if success else (
|
|
67
|
+
RetStatus.RetTimeout if timeout else RetStatus.RetFail
|
|
68
|
+
)
|
|
69
|
+
return ServiceCallResult(
|
|
70
|
+
ret_status=status,
|
|
71
|
+
ret_code=ret_code,
|
|
72
|
+
delay=max(0, int((time.monotonic() - started_at) * 1000)),
|
|
73
|
+
method=method,
|
|
74
|
+
caller_service=(
|
|
75
|
+
ServiceInfo(namespace=self.namespace, service=self.caller_service)
|
|
76
|
+
if self.caller_service else None
|
|
77
|
+
),
|
|
78
|
+
caller_ip=self.caller_ip or None,
|
|
79
|
+
labels=self.labels or None,
|
|
80
|
+
instance_type=instance_type,
|
|
81
|
+
namespace=self.namespace,
|
|
82
|
+
service=service,
|
|
83
|
+
host=host,
|
|
84
|
+
port=port,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
async def report(
|
|
88
|
+
self,
|
|
89
|
+
*,
|
|
90
|
+
service: str,
|
|
91
|
+
host: str,
|
|
92
|
+
port: int,
|
|
93
|
+
method: str,
|
|
94
|
+
instance_type: str,
|
|
95
|
+
started_at: float,
|
|
96
|
+
success: bool,
|
|
97
|
+
ret_code: int | None = None,
|
|
98
|
+
timeout: bool = False,
|
|
99
|
+
):
|
|
100
|
+
result = self._build_result(
|
|
101
|
+
service=service,
|
|
102
|
+
host=host,
|
|
103
|
+
port=port,
|
|
104
|
+
method=method,
|
|
105
|
+
instance_type=instance_type,
|
|
106
|
+
started_at=started_at,
|
|
107
|
+
success=success,
|
|
108
|
+
ret_code=ret_code,
|
|
109
|
+
timeout=timeout,
|
|
110
|
+
)
|
|
111
|
+
if result is None:
|
|
112
|
+
return
|
|
113
|
+
try:
|
|
114
|
+
await self.consumer.async_update_service_call_result(result)
|
|
115
|
+
except Exception as error:
|
|
116
|
+
self.logger.warning_throttled(
|
|
117
|
+
("report", instance_type, service),
|
|
118
|
+
"Failed to report %s call for %s: %s", instance_type, service, error,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def report_sync(
|
|
122
|
+
self,
|
|
123
|
+
*,
|
|
124
|
+
service: str,
|
|
125
|
+
host: str,
|
|
126
|
+
port: int,
|
|
127
|
+
method: str,
|
|
128
|
+
instance_type: str,
|
|
129
|
+
started_at: float,
|
|
130
|
+
success: bool,
|
|
131
|
+
ret_code: int | None = None,
|
|
132
|
+
timeout: bool = False,
|
|
133
|
+
):
|
|
134
|
+
"""Synchronously report one synchronous framework call."""
|
|
135
|
+
result = self._build_result(
|
|
136
|
+
service=service,
|
|
137
|
+
host=host,
|
|
138
|
+
port=port,
|
|
139
|
+
method=method,
|
|
140
|
+
instance_type=instance_type,
|
|
141
|
+
started_at=started_at,
|
|
142
|
+
success=success,
|
|
143
|
+
ret_code=ret_code,
|
|
144
|
+
timeout=timeout,
|
|
145
|
+
)
|
|
146
|
+
if result is None:
|
|
147
|
+
return
|
|
148
|
+
try:
|
|
149
|
+
self.consumer.sync_update_service_call_result(result)
|
|
150
|
+
except Exception as error:
|
|
151
|
+
self.logger.warning_throttled(
|
|
152
|
+
("report-sync", instance_type, service),
|
|
153
|
+
"Failed to report %s call for %s: %s", instance_type, service, error,
|
|
154
|
+
)
|