flatmachines 1.0.0__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.
- flatmachines/__init__.py +136 -0
- flatmachines/actions.py +408 -0
- flatmachines/adapters/__init__.py +38 -0
- flatmachines/adapters/flatagent.py +86 -0
- flatmachines/adapters/pi_agent_bridge.py +127 -0
- flatmachines/adapters/pi_agent_runner.mjs +99 -0
- flatmachines/adapters/smolagents.py +125 -0
- flatmachines/agents.py +144 -0
- flatmachines/assets/MACHINES.md +141 -0
- flatmachines/assets/README.md +11 -0
- flatmachines/assets/__init__.py +0 -0
- flatmachines/assets/flatagent.d.ts +219 -0
- flatmachines/assets/flatagent.schema.json +271 -0
- flatmachines/assets/flatagent.slim.d.ts +58 -0
- flatmachines/assets/flatagents-runtime.d.ts +523 -0
- flatmachines/assets/flatagents-runtime.schema.json +281 -0
- flatmachines/assets/flatagents-runtime.slim.d.ts +187 -0
- flatmachines/assets/flatmachine.d.ts +403 -0
- flatmachines/assets/flatmachine.schema.json +620 -0
- flatmachines/assets/flatmachine.slim.d.ts +106 -0
- flatmachines/assets/profiles.d.ts +140 -0
- flatmachines/assets/profiles.schema.json +93 -0
- flatmachines/assets/profiles.slim.d.ts +26 -0
- flatmachines/backends.py +222 -0
- flatmachines/distributed.py +835 -0
- flatmachines/distributed_hooks.py +351 -0
- flatmachines/execution.py +638 -0
- flatmachines/expressions/__init__.py +60 -0
- flatmachines/expressions/cel.py +101 -0
- flatmachines/expressions/simple.py +166 -0
- flatmachines/flatmachine.py +1263 -0
- flatmachines/hooks.py +381 -0
- flatmachines/locking.py +69 -0
- flatmachines/monitoring.py +505 -0
- flatmachines/persistence.py +213 -0
- flatmachines/run.py +117 -0
- flatmachines/utils.py +166 -0
- flatmachines/validation.py +79 -0
- flatmachines-1.0.0.dist-info/METADATA +390 -0
- flatmachines-1.0.0.dist-info/RECORD +41 -0
- flatmachines-1.0.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Monitoring and observability utilities for FlatAgents.
|
|
3
|
+
|
|
4
|
+
Provides standardized logging configuration and OpenTelemetry-based metrics.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import time
|
|
12
|
+
from contextlib import contextmanager
|
|
13
|
+
from dataclasses import is_dataclass, fields
|
|
14
|
+
from enum import Enum
|
|
15
|
+
from typing import Any, Dict, Optional
|
|
16
|
+
|
|
17
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
# Logging Configuration
|
|
19
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
# Global logger registry
|
|
22
|
+
_loggers: Dict[str, logging.Logger] = {}
|
|
23
|
+
_logging_configured = False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class JSONFormatter(logging.Formatter):
|
|
27
|
+
"""
|
|
28
|
+
Production-ready JSON log formatter.
|
|
29
|
+
|
|
30
|
+
Properly escapes message content and includes exception info.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
34
|
+
log_entry = {
|
|
35
|
+
"time": self.formatTime(record, '%Y-%m-%dT%H:%M:%S'),
|
|
36
|
+
"name": record.name,
|
|
37
|
+
"level": record.levelname,
|
|
38
|
+
"message": record.getMessage(),
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Add exception info if present
|
|
42
|
+
if record.exc_info:
|
|
43
|
+
log_entry["exception"] = self.formatException(record.exc_info)
|
|
44
|
+
|
|
45
|
+
# Add extra fields if any
|
|
46
|
+
if hasattr(record, 'extra') and record.extra:
|
|
47
|
+
log_entry["extra"] = record.extra
|
|
48
|
+
|
|
49
|
+
return json.dumps(log_entry, ensure_ascii=False)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def setup_logging(
|
|
53
|
+
level: Optional[str] = None,
|
|
54
|
+
format: Optional[str] = None,
|
|
55
|
+
force: bool = False
|
|
56
|
+
) -> None:
|
|
57
|
+
"""
|
|
58
|
+
Configure SDK-wide logging with sensible defaults.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
level: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL).
|
|
62
|
+
Defaults to FLATAGENTS_LOG_LEVEL env var or INFO.
|
|
63
|
+
format: Log format style. Options:
|
|
64
|
+
- 'standard': Human-readable with timestamps
|
|
65
|
+
- 'json': Structured JSON logging
|
|
66
|
+
- 'simple': Just level and message
|
|
67
|
+
- Custom format string
|
|
68
|
+
Defaults to FLATAGENTS_LOG_FORMAT env var or 'standard'.
|
|
69
|
+
force: If True, reconfigure even if already configured.
|
|
70
|
+
|
|
71
|
+
Environment Variables:
|
|
72
|
+
FLATAGENTS_LOG_DIR: If set, logs will be written to files in this directory.
|
|
73
|
+
Each process gets a unique log file with PID and timestamp.
|
|
74
|
+
FLATAGENTS_LOG_LEVEL: Default log level (DEBUG, INFO, etc.)
|
|
75
|
+
FLATAGENTS_LOG_FORMAT: Log format style
|
|
76
|
+
|
|
77
|
+
Example:
|
|
78
|
+
>>> from flatagents import setup_logging
|
|
79
|
+
>>> setup_logging(level='DEBUG')
|
|
80
|
+
>>> # Or via environment:
|
|
81
|
+
>>> # export FLATAGENTS_LOG_LEVEL=DEBUG
|
|
82
|
+
>>> # export FLATAGENTS_LOG_FORMAT=json
|
|
83
|
+
>>> # export FLATAGENTS_LOG_DIR=/path/to/logs
|
|
84
|
+
"""
|
|
85
|
+
global _logging_configured
|
|
86
|
+
|
|
87
|
+
if _logging_configured and not force:
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
# Determine log level
|
|
91
|
+
if level is None:
|
|
92
|
+
level = os.getenv('FLATAGENTS_LOG_LEVEL', 'INFO').upper()
|
|
93
|
+
|
|
94
|
+
log_level = getattr(logging, level.upper(), logging.INFO)
|
|
95
|
+
|
|
96
|
+
# Determine format
|
|
97
|
+
if format is None:
|
|
98
|
+
format = os.getenv('FLATAGENTS_LOG_FORMAT', 'standard')
|
|
99
|
+
|
|
100
|
+
# Create appropriate formatter
|
|
101
|
+
if format == 'json':
|
|
102
|
+
formatter = JSONFormatter()
|
|
103
|
+
elif format == 'simple':
|
|
104
|
+
log_format = '%(levelname)s - %(message)s'
|
|
105
|
+
formatter = logging.Formatter(log_format, datefmt='%Y-%m-%d %H:%M:%S')
|
|
106
|
+
elif format == 'standard':
|
|
107
|
+
log_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
108
|
+
formatter = logging.Formatter(log_format, datefmt='%Y-%m-%d %H:%M:%S')
|
|
109
|
+
else:
|
|
110
|
+
# Custom format string
|
|
111
|
+
formatter = logging.Formatter(format, datefmt='%Y-%m-%d %H:%M:%S')
|
|
112
|
+
|
|
113
|
+
# Get root logger for flatagents
|
|
114
|
+
root_logger = logging.getLogger()
|
|
115
|
+
root_logger.setLevel(log_level)
|
|
116
|
+
|
|
117
|
+
# Clear existing handlers if force
|
|
118
|
+
if force:
|
|
119
|
+
root_logger.handlers.clear()
|
|
120
|
+
|
|
121
|
+
# Always add stdout handler
|
|
122
|
+
stdout_handler = logging.StreamHandler(sys.stdout)
|
|
123
|
+
stdout_handler.setFormatter(formatter)
|
|
124
|
+
root_logger.addHandler(stdout_handler)
|
|
125
|
+
|
|
126
|
+
# Add file handler if FLATAGENTS_LOG_DIR is set
|
|
127
|
+
log_dir = os.getenv('FLATAGENTS_LOG_DIR')
|
|
128
|
+
if log_dir:
|
|
129
|
+
from datetime import datetime
|
|
130
|
+
|
|
131
|
+
# Create log directory if it doesn't exist
|
|
132
|
+
os.makedirs(log_dir, exist_ok=True)
|
|
133
|
+
|
|
134
|
+
# Create unique log file per process (PID + timestamp)
|
|
135
|
+
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
136
|
+
pid = os.getpid()
|
|
137
|
+
log_file = os.path.join(log_dir, f"flatagents_{pid}_{timestamp}.log")
|
|
138
|
+
|
|
139
|
+
file_handler = logging.FileHandler(log_file, mode='a', encoding='utf-8')
|
|
140
|
+
file_handler.setFormatter(formatter)
|
|
141
|
+
root_logger.addHandler(file_handler)
|
|
142
|
+
|
|
143
|
+
# Log where we're writing to
|
|
144
|
+
root_logger.info(f"Logging to file: {log_file}")
|
|
145
|
+
|
|
146
|
+
_logging_configured = True
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def get_logger(name: str) -> logging.Logger:
|
|
150
|
+
"""
|
|
151
|
+
Get a properly configured logger for a module.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
name: Logger name (typically __name__ from the calling module)
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Configured logger instance
|
|
158
|
+
|
|
159
|
+
Example:
|
|
160
|
+
>>> from flatagents import get_logger
|
|
161
|
+
>>> logger = get_logger(__name__)
|
|
162
|
+
>>> logger.info("Agent started")
|
|
163
|
+
"""
|
|
164
|
+
if name not in _loggers:
|
|
165
|
+
# Ensure logging is configured
|
|
166
|
+
if not _logging_configured:
|
|
167
|
+
setup_logging()
|
|
168
|
+
|
|
169
|
+
logger = logging.getLogger(name)
|
|
170
|
+
_loggers[name] = logger
|
|
171
|
+
|
|
172
|
+
return _loggers[name]
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
176
|
+
# Metrics with OpenTelemetry
|
|
177
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
# Lazy imports for OpenTelemetry (optional dependency)
|
|
180
|
+
_otel_available = False
|
|
181
|
+
_meter = None
|
|
182
|
+
_metrics_enabled = False
|
|
183
|
+
_metrics_init_attempted = False # Prevent repeated init attempts
|
|
184
|
+
_cached_histograms: Dict[str, Any] = {} # Cache for histogram instruments
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
from opentelemetry import metrics
|
|
188
|
+
from opentelemetry.sdk.metrics import MeterProvider
|
|
189
|
+
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
|
|
190
|
+
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
|
|
191
|
+
from opentelemetry.sdk.resources import Resource, SERVICE_NAME
|
|
192
|
+
_otel_available = True
|
|
193
|
+
except ImportError:
|
|
194
|
+
_otel_available = False
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _init_metrics() -> None:
|
|
198
|
+
"""Initialize OpenTelemetry metrics if enabled and available."""
|
|
199
|
+
global _meter, _metrics_enabled, _metrics_init_attempted
|
|
200
|
+
|
|
201
|
+
_metrics_init_attempted = True
|
|
202
|
+
|
|
203
|
+
# Check if metrics should be enabled (default: true)
|
|
204
|
+
enabled = os.getenv('FLATAGENTS_METRICS_ENABLED', 'true').lower() not in ('false', '0', 'no')
|
|
205
|
+
|
|
206
|
+
if not enabled:
|
|
207
|
+
_metrics_enabled = False
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
if not _otel_available:
|
|
211
|
+
logger = get_logger(__name__)
|
|
212
|
+
logger.warning(
|
|
213
|
+
"Metrics enabled but OpenTelemetry not available. "
|
|
214
|
+
"Install with: pip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp"
|
|
215
|
+
)
|
|
216
|
+
_metrics_enabled = False
|
|
217
|
+
return
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
# Get service name from environment or use default
|
|
221
|
+
service_name = os.getenv('OTEL_SERVICE_NAME', 'flatagents')
|
|
222
|
+
|
|
223
|
+
# Create resource with service name
|
|
224
|
+
resource = Resource(attributes={
|
|
225
|
+
SERVICE_NAME: service_name
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
# Check which exporter to use (default to console so metrics are visible)
|
|
229
|
+
exporter_type = os.getenv('OTEL_METRICS_EXPORTER', 'console').lower()
|
|
230
|
+
|
|
231
|
+
if exporter_type == 'console':
|
|
232
|
+
# Use console exporter for testing/debugging
|
|
233
|
+
try:
|
|
234
|
+
from opentelemetry.sdk.metrics.export import (
|
|
235
|
+
ConsoleMetricExporter,
|
|
236
|
+
MetricExporter,
|
|
237
|
+
MetricExportResult,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
def _to_jsonable(value):
|
|
241
|
+
if value is None or isinstance(value, (str, int, float, bool)):
|
|
242
|
+
return value
|
|
243
|
+
if isinstance(value, Enum):
|
|
244
|
+
return value.value
|
|
245
|
+
if is_dataclass(value):
|
|
246
|
+
return {f.name: _to_jsonable(getattr(value, f.name)) for f in fields(value)}
|
|
247
|
+
if isinstance(value, dict):
|
|
248
|
+
return {str(k): _to_jsonable(v) for k, v in value.items()}
|
|
249
|
+
if isinstance(value, (list, tuple, set)):
|
|
250
|
+
return [_to_jsonable(v) for v in value]
|
|
251
|
+
if hasattr(value, "__dict__"):
|
|
252
|
+
return {str(k): _to_jsonable(v) for k, v in vars(value).items()}
|
|
253
|
+
return str(value)
|
|
254
|
+
|
|
255
|
+
def _compact_metrics_formatter(metrics_data) -> str:
|
|
256
|
+
try:
|
|
257
|
+
from opentelemetry.sdk.metrics.export import MetricsJSONEncoder
|
|
258
|
+
line = json.dumps(metrics_data, cls=MetricsJSONEncoder, separators=(",", ":"))
|
|
259
|
+
except Exception:
|
|
260
|
+
try:
|
|
261
|
+
from opentelemetry.sdk.metrics.export import metrics_to_json
|
|
262
|
+
compact = metrics_to_json(metrics_data)
|
|
263
|
+
if isinstance(compact, str):
|
|
264
|
+
line = compact.replace("\n", "")
|
|
265
|
+
else:
|
|
266
|
+
line = json.dumps(compact, separators=(",", ":"))
|
|
267
|
+
except Exception:
|
|
268
|
+
line = json.dumps(_to_jsonable(metrics_data), separators=(",", ":"))
|
|
269
|
+
if not line.endswith("\n"):
|
|
270
|
+
line = f"{line}\n"
|
|
271
|
+
return line
|
|
272
|
+
|
|
273
|
+
class _CompactConsoleMetricExporter(MetricExporter):
|
|
274
|
+
def __init__(self, preferred_temporality=None, preferred_aggregation=None):
|
|
275
|
+
super().__init__(
|
|
276
|
+
preferred_temporality=preferred_temporality,
|
|
277
|
+
preferred_aggregation=preferred_aggregation,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
def export(self, metrics_data):
|
|
281
|
+
try:
|
|
282
|
+
line = _compact_metrics_formatter(metrics_data)
|
|
283
|
+
sys.stdout.write(line)
|
|
284
|
+
sys.stdout.flush()
|
|
285
|
+
return MetricExportResult.SUCCESS
|
|
286
|
+
except Exception:
|
|
287
|
+
return MetricExportResult.FAILURE
|
|
288
|
+
|
|
289
|
+
def shutdown(self, timeout_millis: int = 30000):
|
|
290
|
+
return None
|
|
291
|
+
|
|
292
|
+
def force_flush(self, timeout_millis: int = 30000):
|
|
293
|
+
return True
|
|
294
|
+
|
|
295
|
+
try:
|
|
296
|
+
import inspect
|
|
297
|
+
if "formatter" in inspect.signature(ConsoleMetricExporter).parameters:
|
|
298
|
+
exporter = ConsoleMetricExporter(formatter=_compact_metrics_formatter)
|
|
299
|
+
else:
|
|
300
|
+
exporter = _CompactConsoleMetricExporter()
|
|
301
|
+
except Exception:
|
|
302
|
+
exporter = _CompactConsoleMetricExporter()
|
|
303
|
+
except ImportError:
|
|
304
|
+
logger = get_logger(__name__)
|
|
305
|
+
logger.warning("Console exporter not available, falling back to OTLP")
|
|
306
|
+
exporter = OTLPMetricExporter(
|
|
307
|
+
endpoint=os.getenv('OTEL_EXPORTER_OTLP_ENDPOINT'),
|
|
308
|
+
)
|
|
309
|
+
else:
|
|
310
|
+
# Configure OTLP exporter (supports Datadog, Honeycomb, etc.)
|
|
311
|
+
exporter = OTLPMetricExporter(
|
|
312
|
+
endpoint=os.getenv('OTEL_EXPORTER_OTLP_ENDPOINT'),
|
|
313
|
+
# Headers can be set via OTEL_EXPORTER_OTLP_HEADERS env var
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
# Create meter provider with periodic export
|
|
317
|
+
reader = PeriodicExportingMetricReader(
|
|
318
|
+
exporter=exporter,
|
|
319
|
+
export_interval_millis=int(os.getenv('OTEL_METRIC_EXPORT_INTERVAL', '5000' if exporter_type == 'console' else '60000'))
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
provider = MeterProvider(
|
|
323
|
+
resource=resource,
|
|
324
|
+
metric_readers=[reader]
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
metrics.set_meter_provider(provider)
|
|
328
|
+
_meter = metrics.get_meter(__name__)
|
|
329
|
+
_metrics_enabled = True
|
|
330
|
+
|
|
331
|
+
logger = get_logger(__name__)
|
|
332
|
+
logger.info(f"OpenTelemetry metrics enabled for service: {service_name}")
|
|
333
|
+
|
|
334
|
+
except Exception as e:
|
|
335
|
+
logger = get_logger(__name__)
|
|
336
|
+
logger.warning(f"Failed to initialize OpenTelemetry metrics: {e}")
|
|
337
|
+
_metrics_enabled = False
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def get_meter():
|
|
341
|
+
"""
|
|
342
|
+
Get the OpenTelemetry meter for creating custom metrics.
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
OpenTelemetry Meter instance or None if metrics disabled
|
|
346
|
+
|
|
347
|
+
Example:
|
|
348
|
+
>>> from flatagents import get_meter
|
|
349
|
+
>>> meter = get_meter()
|
|
350
|
+
>>> if meter:
|
|
351
|
+
... counter = meter.create_counter("my_custom_metric")
|
|
352
|
+
... counter.add(1, {"attribute": "value"})
|
|
353
|
+
"""
|
|
354
|
+
global _meter
|
|
355
|
+
|
|
356
|
+
if not _metrics_init_attempted:
|
|
357
|
+
_init_metrics()
|
|
358
|
+
|
|
359
|
+
return _meter
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
class AgentMonitor:
|
|
363
|
+
"""
|
|
364
|
+
Context manager for tracking agent execution metrics.
|
|
365
|
+
|
|
366
|
+
Automatically tracks:
|
|
367
|
+
- Execution duration
|
|
368
|
+
- Success/failure status
|
|
369
|
+
- Custom metrics via the metrics dict
|
|
370
|
+
|
|
371
|
+
Example:
|
|
372
|
+
>>> from flatagents import AgentMonitor
|
|
373
|
+
>>> with AgentMonitor("my-agent") as monitor:
|
|
374
|
+
... # Do agent work
|
|
375
|
+
... monitor.metrics["tokens"] = 1500
|
|
376
|
+
... monitor.metrics["cost"] = 0.03
|
|
377
|
+
>>> # Metrics automatically emitted on exit
|
|
378
|
+
"""
|
|
379
|
+
|
|
380
|
+
def __init__(self, agent_id: str, extra_attributes: Optional[Dict[str, Any]] = None):
|
|
381
|
+
"""
|
|
382
|
+
Initialize the monitor.
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
agent_id: Identifier for this agent/operation
|
|
386
|
+
extra_attributes: Additional attributes to attach to all metrics
|
|
387
|
+
"""
|
|
388
|
+
self.agent_id = agent_id
|
|
389
|
+
self.start_time = None
|
|
390
|
+
self.metrics: Dict[str, Any] = {}
|
|
391
|
+
self.extra_attributes = extra_attributes or {}
|
|
392
|
+
self.logger = get_logger(f"flatagents.monitor.{agent_id}")
|
|
393
|
+
|
|
394
|
+
# Get or create metric instruments
|
|
395
|
+
self._meter = get_meter()
|
|
396
|
+
if self._meter:
|
|
397
|
+
self._duration_histogram = self._meter.create_histogram(
|
|
398
|
+
"flatagents.agent.duration",
|
|
399
|
+
unit="ms",
|
|
400
|
+
description="Agent execution duration"
|
|
401
|
+
)
|
|
402
|
+
self._token_counter = self._meter.create_counter(
|
|
403
|
+
"flatagents.agent.tokens",
|
|
404
|
+
description="Tokens used by agent"
|
|
405
|
+
)
|
|
406
|
+
self._cost_counter = self._meter.create_counter(
|
|
407
|
+
"flatagents.agent.cost",
|
|
408
|
+
description="Estimated cost of agent execution"
|
|
409
|
+
)
|
|
410
|
+
self._status_counter = self._meter.create_counter(
|
|
411
|
+
"flatagents.agent.executions",
|
|
412
|
+
description="Agent execution count by status"
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
def __enter__(self):
|
|
416
|
+
"""Start monitoring."""
|
|
417
|
+
self.start_time = time.time()
|
|
418
|
+
self.logger.debug(f"Agent {self.agent_id} started")
|
|
419
|
+
return self
|
|
420
|
+
|
|
421
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
422
|
+
"""Stop monitoring and emit metrics."""
|
|
423
|
+
duration_ms = (time.time() - self.start_time) * 1000
|
|
424
|
+
status = "success" if exc_type is None else "error"
|
|
425
|
+
|
|
426
|
+
# Build attributes
|
|
427
|
+
attributes = {
|
|
428
|
+
"agent_id": self.agent_id,
|
|
429
|
+
"status": status,
|
|
430
|
+
**self.extra_attributes
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if exc_type is not None:
|
|
434
|
+
attributes["error_type"] = exc_type.__name__
|
|
435
|
+
|
|
436
|
+
# Log completion
|
|
437
|
+
self.logger.info(
|
|
438
|
+
f"Agent {self.agent_id} completed in {duration_ms:.2f}ms - {status}"
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
# Emit metrics if enabled
|
|
442
|
+
if self._meter:
|
|
443
|
+
self._duration_histogram.record(duration_ms, attributes)
|
|
444
|
+
self._status_counter.add(1, attributes)
|
|
445
|
+
|
|
446
|
+
if "tokens" in self.metrics:
|
|
447
|
+
self._token_counter.add(self.metrics["tokens"], attributes)
|
|
448
|
+
|
|
449
|
+
if "cost" in self.metrics:
|
|
450
|
+
self._cost_counter.add(self.metrics["cost"], attributes)
|
|
451
|
+
|
|
452
|
+
# Don't suppress exceptions
|
|
453
|
+
return False
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
457
|
+
# Convenience context manager for temporary metrics
|
|
458
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
459
|
+
|
|
460
|
+
@contextmanager
|
|
461
|
+
def track_operation(operation_name: str, **attributes):
|
|
462
|
+
"""
|
|
463
|
+
Track duration of an operation.
|
|
464
|
+
|
|
465
|
+
Args:
|
|
466
|
+
operation_name: Name of the operation
|
|
467
|
+
**attributes: Additional attributes to attach
|
|
468
|
+
|
|
469
|
+
Example:
|
|
470
|
+
>>> from flatagents.monitoring import track_operation
|
|
471
|
+
>>> with track_operation("llm_call", model="gpt-4"):
|
|
472
|
+
... response = await llm.call(messages)
|
|
473
|
+
"""
|
|
474
|
+
meter = get_meter()
|
|
475
|
+
start_time = time.time()
|
|
476
|
+
|
|
477
|
+
try:
|
|
478
|
+
yield
|
|
479
|
+
status = "success"
|
|
480
|
+
except Exception as e:
|
|
481
|
+
status = "error"
|
|
482
|
+
attributes["error_type"] = type(e).__name__
|
|
483
|
+
raise
|
|
484
|
+
finally:
|
|
485
|
+
duration_ms = (time.time() - start_time) * 1000
|
|
486
|
+
|
|
487
|
+
if meter:
|
|
488
|
+
# Cache histogram to avoid recreating on each call
|
|
489
|
+
cache_key = f"flatagents.{operation_name}.duration"
|
|
490
|
+
if cache_key not in _cached_histograms:
|
|
491
|
+
_cached_histograms[cache_key] = meter.create_histogram(
|
|
492
|
+
cache_key,
|
|
493
|
+
unit="ms",
|
|
494
|
+
description=f"Duration of {operation_name}"
|
|
495
|
+
)
|
|
496
|
+
_cached_histograms[cache_key].record(duration_ms, {**attributes, "status": status})
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
__all__ = [
|
|
500
|
+
"setup_logging",
|
|
501
|
+
"get_logger",
|
|
502
|
+
"get_meter",
|
|
503
|
+
"AgentMonitor",
|
|
504
|
+
"track_operation",
|
|
505
|
+
]
|