traccia 0.1.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.
- traccia/__init__.py +73 -0
- traccia/auto.py +736 -0
- traccia/auto_instrumentation.py +74 -0
- traccia/cli.py +349 -0
- traccia/config.py +693 -0
- traccia/errors.py +48 -0
- traccia/pricing_config.py +58 -0
- traccia/runtime_config.py +106 -0
- traccia-0.1.0.dist-info/METADATA +674 -0
- traccia-0.1.0.dist-info/RECORD +14 -0
- traccia-0.1.0.dist-info/WHEEL +5 -0
- traccia-0.1.0.dist-info/entry_points.txt +2 -0
- traccia-0.1.0.dist-info/licenses/LICENSE +190 -0
- traccia-0.1.0.dist-info/top_level.txt +1 -0
traccia/config.py
ADDED
|
@@ -0,0 +1,693 @@
|
|
|
1
|
+
"""Configuration management with Pydantic models and validation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Dict, Optional, Literal
|
|
8
|
+
from pydantic import BaseModel, Field, field_validator, model_validator, HttpUrl
|
|
9
|
+
|
|
10
|
+
from traccia.errors import ConfigError, ValidationError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Environment variable mapping
|
|
14
|
+
ENV_VAR_MAPPING = {
|
|
15
|
+
# Tracing config
|
|
16
|
+
"api_key": ["TRACCIA_API_KEY", "AGENT_DASHBOARD_API_KEY"],
|
|
17
|
+
"endpoint": ["TRACCIA_ENDPOINT", "AGENT_DASHBOARD_ENDPOINT"],
|
|
18
|
+
"sample_rate": ["TRACCIA_SAMPLE_RATE", "AGENT_DASHBOARD_SAMPLE_RATE"],
|
|
19
|
+
"auto_start_trace": ["TRACCIA_AUTO_START_TRACE", "AGENT_DASHBOARD_AUTO_START_TRACE"],
|
|
20
|
+
"auto_trace_name": ["TRACCIA_AUTO_TRACE_NAME"],
|
|
21
|
+
"use_otlp": ["TRACCIA_USE_OTLP"],
|
|
22
|
+
"service_name": ["TRACCIA_SERVICE_NAME"],
|
|
23
|
+
|
|
24
|
+
# Exporter config
|
|
25
|
+
"enable_console": ["TRACCIA_ENABLE_CONSOLE", "AGENT_DASHBOARD_ENABLE_CONSOLE_EXPORTER"],
|
|
26
|
+
"enable_file": ["TRACCIA_ENABLE_FILE", "AGENT_DASHBOARD_ENABLE_FILE_EXPORTER"],
|
|
27
|
+
"file_exporter_path": ["TRACCIA_FILE_PATH"],
|
|
28
|
+
"reset_trace_file": ["TRACCIA_RESET_TRACE_FILE"],
|
|
29
|
+
|
|
30
|
+
# Instrumentation config
|
|
31
|
+
"enable_patching": ["TRACCIA_ENABLE_PATCHING", "AGENT_DASHBOARD_ENABLE_PATCHING"],
|
|
32
|
+
"enable_token_counting": ["TRACCIA_ENABLE_TOKEN_COUNTING", "AGENT_DASHBOARD_ENABLE_TOKEN_COUNTING"],
|
|
33
|
+
"enable_costs": ["TRACCIA_ENABLE_COSTS", "AGENT_DASHBOARD_ENABLE_COSTS"],
|
|
34
|
+
"auto_instrument_tools": ["TRACCIA_AUTO_INSTRUMENT_TOOLS"],
|
|
35
|
+
"max_tool_spans": ["TRACCIA_MAX_TOOL_SPANS"],
|
|
36
|
+
"max_span_depth": ["TRACCIA_MAX_SPAN_DEPTH"],
|
|
37
|
+
|
|
38
|
+
# Rate limiting & Batching
|
|
39
|
+
"max_spans_per_second": ["TRACCIA_MAX_SPANS_PER_SECOND"],
|
|
40
|
+
"max_queue_size": ["TRACCIA_MAX_QUEUE_SIZE"],
|
|
41
|
+
"max_block_ms": ["TRACCIA_MAX_BLOCK_MS"],
|
|
42
|
+
"max_export_batch_size": ["TRACCIA_MAX_EXPORT_BATCH_SIZE"],
|
|
43
|
+
"schedule_delay_millis": ["TRACCIA_SCHEDULE_DELAY_MILLIS"],
|
|
44
|
+
|
|
45
|
+
# Runtime metadata
|
|
46
|
+
"session_id": ["TRACCIA_SESSION_ID"],
|
|
47
|
+
"user_id": ["TRACCIA_USER_ID"],
|
|
48
|
+
"tenant_id": ["TRACCIA_TENANT_ID"],
|
|
49
|
+
"project_id": ["TRACCIA_PROJECT_ID"],
|
|
50
|
+
"agent_id": ["TRACCIA_AGENT_ID", "AGENT_DASHBOARD_AGENT_ID"],
|
|
51
|
+
|
|
52
|
+
# Logging
|
|
53
|
+
"debug": ["TRACCIA_DEBUG"],
|
|
54
|
+
"enable_span_logging": ["TRACCIA_ENABLE_SPAN_LOGGING"],
|
|
55
|
+
|
|
56
|
+
# Advanced
|
|
57
|
+
"attr_truncation_limit": ["TRACCIA_ATTR_TRUNCATION_LIMIT"],
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class TracingConfig(BaseModel):
|
|
62
|
+
"""Tracing configuration section."""
|
|
63
|
+
|
|
64
|
+
api_key: Optional[str] = Field(
|
|
65
|
+
default=None,
|
|
66
|
+
description="API key for authentication (required for SaaS, optional for open-source)"
|
|
67
|
+
)
|
|
68
|
+
endpoint: Optional[str] = Field(
|
|
69
|
+
default=None,
|
|
70
|
+
description="Endpoint URL for trace ingestion"
|
|
71
|
+
)
|
|
72
|
+
sample_rate: float = Field(
|
|
73
|
+
default=1.0,
|
|
74
|
+
ge=0.0,
|
|
75
|
+
le=1.0,
|
|
76
|
+
description="Sampling rate (0.0 to 1.0)"
|
|
77
|
+
)
|
|
78
|
+
auto_start_trace: bool = Field(
|
|
79
|
+
default=True,
|
|
80
|
+
description="Automatically start a root trace on init"
|
|
81
|
+
)
|
|
82
|
+
auto_trace_name: str = Field(
|
|
83
|
+
default="root",
|
|
84
|
+
description="Name for the auto-started root trace"
|
|
85
|
+
)
|
|
86
|
+
use_otlp: bool = Field(
|
|
87
|
+
default=True,
|
|
88
|
+
description="Use OTLP exporter (set to false for console/file exporters)"
|
|
89
|
+
)
|
|
90
|
+
service_name: Optional[str] = Field(
|
|
91
|
+
default=None,
|
|
92
|
+
description="Service name for the application"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class ExporterConfig(BaseModel):
|
|
97
|
+
"""Exporter configuration section."""
|
|
98
|
+
|
|
99
|
+
enable_console: bool = Field(
|
|
100
|
+
default=False,
|
|
101
|
+
description="Enable console exporter for debugging"
|
|
102
|
+
)
|
|
103
|
+
enable_file: bool = Field(
|
|
104
|
+
default=False,
|
|
105
|
+
description="Enable file exporter to write traces to local file"
|
|
106
|
+
)
|
|
107
|
+
file_exporter_path: str = Field(
|
|
108
|
+
default="traces.jsonl",
|
|
109
|
+
description="File path for file exporter"
|
|
110
|
+
)
|
|
111
|
+
reset_trace_file: bool = Field(
|
|
112
|
+
default=False,
|
|
113
|
+
description="Reset/clear trace file on initialization"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
@model_validator(mode='after')
|
|
117
|
+
def check_single_exporter(self) -> 'ExporterConfig':
|
|
118
|
+
"""Ensure only one exporter is enabled at a time."""
|
|
119
|
+
enabled_count = sum([
|
|
120
|
+
self.enable_console,
|
|
121
|
+
self.enable_file,
|
|
122
|
+
])
|
|
123
|
+
if enabled_count > 1:
|
|
124
|
+
raise ValidationError(
|
|
125
|
+
"Only one exporter can be enabled at a time. "
|
|
126
|
+
"Choose either console or file exporter (OTLP is controlled by use_otlp in tracing section).",
|
|
127
|
+
details={
|
|
128
|
+
"enable_console": self.enable_console,
|
|
129
|
+
"enable_file": self.enable_file,
|
|
130
|
+
}
|
|
131
|
+
)
|
|
132
|
+
return self
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class InstrumentationConfig(BaseModel):
|
|
136
|
+
"""Instrumentation configuration section."""
|
|
137
|
+
|
|
138
|
+
enable_patching: bool = Field(
|
|
139
|
+
default=True,
|
|
140
|
+
description="Auto-patch popular libraries (OpenAI, Anthropic, requests)"
|
|
141
|
+
)
|
|
142
|
+
enable_token_counting: bool = Field(
|
|
143
|
+
default=True,
|
|
144
|
+
description="Count tokens for LLM calls"
|
|
145
|
+
)
|
|
146
|
+
enable_costs: bool = Field(
|
|
147
|
+
default=True,
|
|
148
|
+
description="Calculate costs for LLM calls"
|
|
149
|
+
)
|
|
150
|
+
auto_instrument_tools: bool = Field(
|
|
151
|
+
default=False,
|
|
152
|
+
description="Automatically instrument tool calls"
|
|
153
|
+
)
|
|
154
|
+
max_tool_spans: int = Field(
|
|
155
|
+
default=100,
|
|
156
|
+
gt=0,
|
|
157
|
+
description="Maximum number of tool spans to create"
|
|
158
|
+
)
|
|
159
|
+
max_span_depth: int = Field(
|
|
160
|
+
default=10,
|
|
161
|
+
gt=0,
|
|
162
|
+
description="Maximum depth of nested spans"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class RateLimitConfig(BaseModel):
|
|
167
|
+
"""Rate limiting and batching configuration section."""
|
|
168
|
+
|
|
169
|
+
max_spans_per_second: Optional[float] = Field(
|
|
170
|
+
default=None,
|
|
171
|
+
gt=0,
|
|
172
|
+
description="Maximum spans per second (None = unlimited)"
|
|
173
|
+
)
|
|
174
|
+
max_queue_size: int = Field(
|
|
175
|
+
default=5000,
|
|
176
|
+
gt=0,
|
|
177
|
+
description="Maximum queue size for buffered spans"
|
|
178
|
+
)
|
|
179
|
+
max_block_ms: int = Field(
|
|
180
|
+
default=100,
|
|
181
|
+
ge=0,
|
|
182
|
+
description="Maximum milliseconds to block before dropping spans"
|
|
183
|
+
)
|
|
184
|
+
max_export_batch_size: int = Field(
|
|
185
|
+
default=512,
|
|
186
|
+
gt=0,
|
|
187
|
+
description="Maximum number of spans in a single export batch"
|
|
188
|
+
)
|
|
189
|
+
schedule_delay_millis: int = Field(
|
|
190
|
+
default=5000,
|
|
191
|
+
gt=0,
|
|
192
|
+
description="Delay in milliseconds between export batches"
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class LoggingConfig(BaseModel):
|
|
197
|
+
"""Logging configuration section."""
|
|
198
|
+
|
|
199
|
+
debug: bool = Field(
|
|
200
|
+
default=False,
|
|
201
|
+
description="Enable debug logging"
|
|
202
|
+
)
|
|
203
|
+
enable_span_logging: bool = Field(
|
|
204
|
+
default=False,
|
|
205
|
+
description="Enable span-level logging for debugging"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class RuntimeConfig(BaseModel):
|
|
210
|
+
"""Runtime metadata configuration section."""
|
|
211
|
+
|
|
212
|
+
session_id: Optional[str] = Field(
|
|
213
|
+
default=None,
|
|
214
|
+
description="Session identifier for grouping traces"
|
|
215
|
+
)
|
|
216
|
+
user_id: Optional[str] = Field(
|
|
217
|
+
default=None,
|
|
218
|
+
description="User identifier for the current session"
|
|
219
|
+
)
|
|
220
|
+
tenant_id: Optional[str] = Field(
|
|
221
|
+
default=None,
|
|
222
|
+
description="Tenant/organization identifier"
|
|
223
|
+
)
|
|
224
|
+
project_id: Optional[str] = Field(
|
|
225
|
+
default=None,
|
|
226
|
+
description="Project identifier"
|
|
227
|
+
)
|
|
228
|
+
agent_id: Optional[str] = Field(
|
|
229
|
+
default=None,
|
|
230
|
+
description="Agent identifier for the current session"
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class AdvancedConfig(BaseModel):
|
|
235
|
+
"""Advanced configuration options."""
|
|
236
|
+
|
|
237
|
+
attr_truncation_limit: Optional[int] = Field(
|
|
238
|
+
default=None,
|
|
239
|
+
gt=0,
|
|
240
|
+
description="Maximum length for attribute values (None = no limit)"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class TracciaConfig(BaseModel):
|
|
245
|
+
"""
|
|
246
|
+
Complete Traccia SDK configuration.
|
|
247
|
+
|
|
248
|
+
This model validates and merges configuration from multiple sources:
|
|
249
|
+
1. Config file (traccia.toml)
|
|
250
|
+
2. Environment variables
|
|
251
|
+
3. Explicit parameters
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
tracing: TracingConfig = Field(default_factory=TracingConfig)
|
|
255
|
+
exporters: ExporterConfig = Field(default_factory=ExporterConfig)
|
|
256
|
+
instrumentation: InstrumentationConfig = Field(default_factory=InstrumentationConfig)
|
|
257
|
+
rate_limiting: RateLimitConfig = Field(default_factory=RateLimitConfig)
|
|
258
|
+
runtime: RuntimeConfig = Field(default_factory=RuntimeConfig)
|
|
259
|
+
logging: LoggingConfig = Field(default_factory=LoggingConfig)
|
|
260
|
+
advanced: AdvancedConfig = Field(default_factory=AdvancedConfig)
|
|
261
|
+
|
|
262
|
+
@model_validator(mode='after')
|
|
263
|
+
def validate_complete_config(self) -> 'TracciaConfig':
|
|
264
|
+
"""Validate the complete configuration for conflicts."""
|
|
265
|
+
# If OTLP is disabled, at least one other exporter must be enabled
|
|
266
|
+
if not self.tracing.use_otlp:
|
|
267
|
+
if not (self.exporters.enable_console or self.exporters.enable_file):
|
|
268
|
+
raise ConfigError(
|
|
269
|
+
"When use_otlp is false, you must enable either console or file exporter.",
|
|
270
|
+
details={
|
|
271
|
+
"use_otlp": self.tracing.use_otlp,
|
|
272
|
+
"enable_console": self.exporters.enable_console,
|
|
273
|
+
"enable_file": self.exporters.enable_file,
|
|
274
|
+
}
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
return self
|
|
278
|
+
|
|
279
|
+
def to_flat_dict(self) -> Dict[str, Any]:
|
|
280
|
+
"""Convert to flat dictionary for backward compatibility."""
|
|
281
|
+
return {
|
|
282
|
+
# Tracing
|
|
283
|
+
"api_key": self.tracing.api_key,
|
|
284
|
+
"endpoint": self.tracing.endpoint,
|
|
285
|
+
"sample_rate": self.tracing.sample_rate,
|
|
286
|
+
"auto_start_trace": self.tracing.auto_start_trace,
|
|
287
|
+
"auto_trace_name": self.tracing.auto_trace_name,
|
|
288
|
+
"use_otlp": self.tracing.use_otlp,
|
|
289
|
+
"service_name": self.tracing.service_name,
|
|
290
|
+
# Exporters
|
|
291
|
+
"enable_console": self.exporters.enable_console,
|
|
292
|
+
"enable_file": self.exporters.enable_file,
|
|
293
|
+
"file_exporter_path": self.exporters.file_exporter_path,
|
|
294
|
+
"reset_trace_file": self.exporters.reset_trace_file,
|
|
295
|
+
# Instrumentation
|
|
296
|
+
"enable_patching": self.instrumentation.enable_patching,
|
|
297
|
+
"enable_token_counting": self.instrumentation.enable_token_counting,
|
|
298
|
+
"enable_costs": self.instrumentation.enable_costs,
|
|
299
|
+
"auto_instrument_tools": self.instrumentation.auto_instrument_tools,
|
|
300
|
+
"max_tool_spans": self.instrumentation.max_tool_spans,
|
|
301
|
+
"max_span_depth": self.instrumentation.max_span_depth,
|
|
302
|
+
# Rate limiting & Batching
|
|
303
|
+
"max_spans_per_second": self.rate_limiting.max_spans_per_second,
|
|
304
|
+
"max_queue_size": self.rate_limiting.max_queue_size,
|
|
305
|
+
"max_block_ms": self.rate_limiting.max_block_ms,
|
|
306
|
+
"max_export_batch_size": self.rate_limiting.max_export_batch_size,
|
|
307
|
+
"schedule_delay_millis": self.rate_limiting.schedule_delay_millis,
|
|
308
|
+
# Runtime
|
|
309
|
+
"session_id": self.runtime.session_id,
|
|
310
|
+
"user_id": self.runtime.user_id,
|
|
311
|
+
"tenant_id": self.runtime.tenant_id,
|
|
312
|
+
"project_id": self.runtime.project_id,
|
|
313
|
+
"agent_id": self.runtime.agent_id,
|
|
314
|
+
# Logging
|
|
315
|
+
"debug": self.logging.debug,
|
|
316
|
+
"enable_span_logging": self.logging.enable_span_logging,
|
|
317
|
+
# Advanced
|
|
318
|
+
"attr_truncation_limit": self.advanced.attr_truncation_limit,
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def load_dotenv(path: str = ".env") -> None:
|
|
323
|
+
"""Minimal .env loader (no external dependency)."""
|
|
324
|
+
if not os.path.exists(path):
|
|
325
|
+
return
|
|
326
|
+
try:
|
|
327
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
328
|
+
for line in f:
|
|
329
|
+
line = line.strip()
|
|
330
|
+
if not line or line.startswith("#"):
|
|
331
|
+
continue
|
|
332
|
+
if "=" not in line:
|
|
333
|
+
continue
|
|
334
|
+
key, value = line.split("=", 1)
|
|
335
|
+
key, value = key.strip(), value.strip().strip("\"'")
|
|
336
|
+
if key and key not in os.environ:
|
|
337
|
+
os.environ[key] = value
|
|
338
|
+
except Exception:
|
|
339
|
+
# Fail silently; this loader is best-effort.
|
|
340
|
+
return
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def find_config_file() -> Optional[str]:
|
|
344
|
+
"""
|
|
345
|
+
Find traccia.toml config file in standard locations.
|
|
346
|
+
|
|
347
|
+
Lookup order:
|
|
348
|
+
1. ./traccia.toml (current directory)
|
|
349
|
+
2. ~/.traccia/config.toml (user home)
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
Path to config file if found, None otherwise
|
|
353
|
+
"""
|
|
354
|
+
# Check current directory
|
|
355
|
+
cwd_config = Path.cwd() / "traccia.toml"
|
|
356
|
+
if cwd_config.exists():
|
|
357
|
+
return str(cwd_config)
|
|
358
|
+
|
|
359
|
+
# Check user home directory
|
|
360
|
+
home_config = Path.home() / ".traccia" / "config.toml"
|
|
361
|
+
if home_config.exists():
|
|
362
|
+
return str(home_config)
|
|
363
|
+
|
|
364
|
+
return None
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def load_toml_config(path: str) -> Dict[str, Any]:
|
|
368
|
+
"""
|
|
369
|
+
Load configuration from a TOML file.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
path: Path to the TOML config file
|
|
373
|
+
|
|
374
|
+
Returns:
|
|
375
|
+
Dictionary with nested config structure
|
|
376
|
+
"""
|
|
377
|
+
if not os.path.exists(path):
|
|
378
|
+
return {}
|
|
379
|
+
|
|
380
|
+
try:
|
|
381
|
+
# Try to import tomli for Python 3.11+, fall back to toml
|
|
382
|
+
try:
|
|
383
|
+
import tomli as toml_lib
|
|
384
|
+
with open(path, "rb") as f:
|
|
385
|
+
data = toml_lib.load(f)
|
|
386
|
+
except ImportError:
|
|
387
|
+
try:
|
|
388
|
+
import toml as toml_lib # type: ignore
|
|
389
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
390
|
+
data = toml_lib.load(f)
|
|
391
|
+
except ImportError:
|
|
392
|
+
raise ConfigError(
|
|
393
|
+
"No TOML library available. Install tomli or toml: pip install tomli"
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
return data
|
|
397
|
+
|
|
398
|
+
except Exception as e:
|
|
399
|
+
if isinstance(e, ConfigError):
|
|
400
|
+
raise
|
|
401
|
+
raise ConfigError(f"Failed to load config file: {e}")
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def get_env_value(config_key: str) -> Optional[str]:
|
|
405
|
+
"""
|
|
406
|
+
Get environment variable value for a config key.
|
|
407
|
+
|
|
408
|
+
Tries multiple environment variable names in order of preference.
|
|
409
|
+
"""
|
|
410
|
+
env_vars = ENV_VAR_MAPPING.get(config_key, [])
|
|
411
|
+
for env_var in env_vars:
|
|
412
|
+
value = os.getenv(env_var)
|
|
413
|
+
if value is not None:
|
|
414
|
+
return value
|
|
415
|
+
return None
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def load_config_from_env(flat: bool = False) -> Dict[str, Any]:
|
|
419
|
+
"""
|
|
420
|
+
Load configuration from environment variables.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
flat: If True, return flat dictionary for backward compatibility
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
Dictionary of config values from environment (nested structure by default)
|
|
427
|
+
"""
|
|
428
|
+
env_config = {
|
|
429
|
+
"tracing": {},
|
|
430
|
+
"exporters": {},
|
|
431
|
+
"instrumentation": {},
|
|
432
|
+
"rate_limiting": {},
|
|
433
|
+
"runtime": {},
|
|
434
|
+
"logging": {},
|
|
435
|
+
"advanced": {},
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
# Tracing section
|
|
439
|
+
for key in ["api_key", "endpoint", "sample_rate", "auto_start_trace", "auto_trace_name", "use_otlp", "service_name"]:
|
|
440
|
+
value = get_env_value(key)
|
|
441
|
+
if value is not None:
|
|
442
|
+
# Convert string to appropriate type
|
|
443
|
+
if key in ["auto_start_trace", "use_otlp"]:
|
|
444
|
+
env_config["tracing"][key] = value.lower() in ("true", "1", "yes")
|
|
445
|
+
elif key == "sample_rate":
|
|
446
|
+
try:
|
|
447
|
+
env_config["tracing"][key] = float(value)
|
|
448
|
+
except ValueError:
|
|
449
|
+
raise ConfigError(f"Invalid sample_rate value: {value}. Must be a float between 0.0 and 1.0.")
|
|
450
|
+
else:
|
|
451
|
+
env_config["tracing"][key] = value
|
|
452
|
+
|
|
453
|
+
# Exporters section
|
|
454
|
+
for key in ["enable_console", "enable_file", "file_exporter_path", "reset_trace_file"]:
|
|
455
|
+
value = get_env_value(key)
|
|
456
|
+
if value is not None:
|
|
457
|
+
if key in ["enable_console", "enable_file", "reset_trace_file"]:
|
|
458
|
+
env_config["exporters"][key] = value.lower() in ("true", "1", "yes")
|
|
459
|
+
else:
|
|
460
|
+
env_config["exporters"][key] = value
|
|
461
|
+
|
|
462
|
+
# Instrumentation section
|
|
463
|
+
for key in ["enable_patching", "enable_token_counting", "enable_costs", "auto_instrument_tools"]:
|
|
464
|
+
value = get_env_value(key)
|
|
465
|
+
if value is not None:
|
|
466
|
+
env_config["instrumentation"][key] = value.lower() in ("true", "1", "yes")
|
|
467
|
+
|
|
468
|
+
for key in ["max_tool_spans", "max_span_depth"]:
|
|
469
|
+
value = get_env_value(key)
|
|
470
|
+
if value is not None:
|
|
471
|
+
try:
|
|
472
|
+
env_config["instrumentation"][key] = int(value)
|
|
473
|
+
except ValueError:
|
|
474
|
+
raise ConfigError(f"Invalid {key} value: {value}. Must be an integer.")
|
|
475
|
+
|
|
476
|
+
# Rate limiting section
|
|
477
|
+
for key in ["max_spans_per_second"]:
|
|
478
|
+
value = get_env_value(key)
|
|
479
|
+
if value is not None:
|
|
480
|
+
try:
|
|
481
|
+
env_config["rate_limiting"][key] = float(value) if value else None
|
|
482
|
+
except ValueError:
|
|
483
|
+
raise ConfigError(f"Invalid {key} value: {value}. Must be a number.")
|
|
484
|
+
|
|
485
|
+
for key in ["max_queue_size", "max_block_ms", "max_export_batch_size", "schedule_delay_millis"]:
|
|
486
|
+
value = get_env_value(key)
|
|
487
|
+
if value is not None:
|
|
488
|
+
try:
|
|
489
|
+
env_config["rate_limiting"][key] = int(value)
|
|
490
|
+
except ValueError:
|
|
491
|
+
raise ConfigError(f"Invalid {key} value: {value}. Must be a number.")
|
|
492
|
+
|
|
493
|
+
# Runtime section
|
|
494
|
+
for key in ["session_id", "user_id", "tenant_id", "project_id", "agent_id"]:
|
|
495
|
+
value = get_env_value(key)
|
|
496
|
+
if value is not None:
|
|
497
|
+
env_config["runtime"][key] = value
|
|
498
|
+
|
|
499
|
+
# Logging section
|
|
500
|
+
for key in ["debug", "enable_span_logging"]:
|
|
501
|
+
value = get_env_value(key)
|
|
502
|
+
if value is not None:
|
|
503
|
+
env_config["logging"][key] = value.lower() in ("true", "1", "yes")
|
|
504
|
+
|
|
505
|
+
# Advanced section
|
|
506
|
+
value = get_env_value("attr_truncation_limit")
|
|
507
|
+
if value is not None:
|
|
508
|
+
try:
|
|
509
|
+
env_config["advanced"]["attr_truncation_limit"] = int(value)
|
|
510
|
+
except ValueError:
|
|
511
|
+
raise ConfigError(f"Invalid attr_truncation_limit value: {value}. Must be an integer.")
|
|
512
|
+
|
|
513
|
+
# Remove empty sections
|
|
514
|
+
nested_result = {k: v for k, v in env_config.items() if v}
|
|
515
|
+
|
|
516
|
+
# Flatten if requested (for backward compatibility)
|
|
517
|
+
if flat:
|
|
518
|
+
flat_result = {}
|
|
519
|
+
for section, values in nested_result.items():
|
|
520
|
+
flat_result.update(values)
|
|
521
|
+
return flat_result
|
|
522
|
+
|
|
523
|
+
return nested_result
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def merge_configs(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
|
|
527
|
+
"""
|
|
528
|
+
Deep merge two config dictionaries.
|
|
529
|
+
|
|
530
|
+
Args:
|
|
531
|
+
base: Base configuration
|
|
532
|
+
override: Override configuration (takes precedence)
|
|
533
|
+
|
|
534
|
+
Returns:
|
|
535
|
+
Merged configuration
|
|
536
|
+
"""
|
|
537
|
+
result = base.copy()
|
|
538
|
+
for key, value in override.items():
|
|
539
|
+
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
|
540
|
+
result[key] = merge_configs(result[key], value)
|
|
541
|
+
else:
|
|
542
|
+
result[key] = value
|
|
543
|
+
return result
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def load_config(
|
|
547
|
+
config_file: Optional[str] = None,
|
|
548
|
+
overrides: Optional[Dict[str, Any]] = None
|
|
549
|
+
) -> TracciaConfig:
|
|
550
|
+
"""
|
|
551
|
+
Load and validate Traccia configuration from multiple sources.
|
|
552
|
+
|
|
553
|
+
Priority (highest to lowest):
|
|
554
|
+
1. Explicit overrides (passed as parameters)
|
|
555
|
+
2. Environment variables
|
|
556
|
+
3. Config file (./traccia.toml or ~/.traccia/config.toml)
|
|
557
|
+
4. Defaults
|
|
558
|
+
|
|
559
|
+
Args:
|
|
560
|
+
config_file: Optional explicit path to config file
|
|
561
|
+
overrides: Optional dict of explicit parameter overrides
|
|
562
|
+
|
|
563
|
+
Returns:
|
|
564
|
+
Validated TracciaConfig instance
|
|
565
|
+
|
|
566
|
+
Raises:
|
|
567
|
+
ConfigError: If configuration is invalid or conflicting
|
|
568
|
+
"""
|
|
569
|
+
merged_config: Dict[str, Any] = {}
|
|
570
|
+
|
|
571
|
+
# 1. Load from config file (lowest priority)
|
|
572
|
+
if config_file:
|
|
573
|
+
file_config = load_toml_config(config_file)
|
|
574
|
+
merged_config = merge_configs(merged_config, file_config)
|
|
575
|
+
else:
|
|
576
|
+
# Try to find config file automatically
|
|
577
|
+
found_config = find_config_file()
|
|
578
|
+
if found_config:
|
|
579
|
+
file_config = load_toml_config(found_config)
|
|
580
|
+
merged_config = merge_configs(merged_config, file_config)
|
|
581
|
+
|
|
582
|
+
# 2. Override with environment variables (medium priority)
|
|
583
|
+
env_config = load_config_from_env()
|
|
584
|
+
merged_config = merge_configs(merged_config, env_config)
|
|
585
|
+
|
|
586
|
+
# 3. Override with explicit parameters (highest priority)
|
|
587
|
+
if overrides:
|
|
588
|
+
merged_config = merge_configs(merged_config, overrides)
|
|
589
|
+
|
|
590
|
+
# 4. Create and validate Pydantic model
|
|
591
|
+
try:
|
|
592
|
+
return TracciaConfig(**merged_config)
|
|
593
|
+
except Exception as e:
|
|
594
|
+
raise ConfigError(f"Configuration validation failed: {e}")
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def validate_config(
|
|
598
|
+
config_file: Optional[str] = None,
|
|
599
|
+
overrides: Optional[Dict[str, Any]] = None
|
|
600
|
+
) -> tuple[bool, str, Optional[TracciaConfig]]:
|
|
601
|
+
"""
|
|
602
|
+
Validate configuration without loading it.
|
|
603
|
+
|
|
604
|
+
Used by the `traccia doctor` CLI command.
|
|
605
|
+
|
|
606
|
+
Args:
|
|
607
|
+
config_file: Optional explicit path to config file
|
|
608
|
+
overrides: Optional dict of explicit parameter overrides
|
|
609
|
+
|
|
610
|
+
Returns:
|
|
611
|
+
Tuple of (is_valid, message, config_or_none)
|
|
612
|
+
"""
|
|
613
|
+
try:
|
|
614
|
+
config = load_config(config_file=config_file, overrides=overrides)
|
|
615
|
+
return True, "Configuration is valid", config
|
|
616
|
+
except ConfigError as e:
|
|
617
|
+
return False, f"Configuration error: {e}", None
|
|
618
|
+
except Exception as e:
|
|
619
|
+
return False, f"Unexpected error: {e}", None
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
# Backward compatibility functions
|
|
623
|
+
def load_config_with_priority(
|
|
624
|
+
config_file: Optional[str] = None,
|
|
625
|
+
overrides: Optional[Dict[str, Any]] = None
|
|
626
|
+
) -> Dict[str, Any]:
|
|
627
|
+
"""
|
|
628
|
+
Legacy function for backward compatibility.
|
|
629
|
+
|
|
630
|
+
Returns flattened config dictionary instead of Pydantic model.
|
|
631
|
+
Accepts flat overrides and converts them to nested format.
|
|
632
|
+
"""
|
|
633
|
+
# Convert flat overrides to nested format
|
|
634
|
+
nested_overrides = None
|
|
635
|
+
if overrides:
|
|
636
|
+
nested_overrides = {
|
|
637
|
+
"tracing": {},
|
|
638
|
+
"exporters": {},
|
|
639
|
+
"instrumentation": {},
|
|
640
|
+
"rate_limiting": {},
|
|
641
|
+
"runtime": {},
|
|
642
|
+
"logging": {},
|
|
643
|
+
"advanced": {}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
# Map flat keys to nested structure
|
|
647
|
+
flat_to_nested = {
|
|
648
|
+
# Tracing
|
|
649
|
+
"api_key": ("tracing", "api_key"),
|
|
650
|
+
"endpoint": ("tracing", "endpoint"),
|
|
651
|
+
"sample_rate": ("tracing", "sample_rate"),
|
|
652
|
+
"auto_start_trace": ("tracing", "auto_start_trace"),
|
|
653
|
+
"auto_trace_name": ("tracing", "auto_trace_name"),
|
|
654
|
+
"use_otlp": ("tracing", "use_otlp"),
|
|
655
|
+
"service_name": ("tracing", "service_name"),
|
|
656
|
+
# Exporters
|
|
657
|
+
"enable_console": ("exporters", "enable_console"),
|
|
658
|
+
"enable_file": ("exporters", "enable_file"),
|
|
659
|
+
"file_exporter_path": ("exporters", "file_exporter_path"),
|
|
660
|
+
"reset_trace_file": ("exporters", "reset_trace_file"),
|
|
661
|
+
# Instrumentation
|
|
662
|
+
"enable_patching": ("instrumentation", "enable_patching"),
|
|
663
|
+
"enable_token_counting": ("instrumentation", "enable_token_counting"),
|
|
664
|
+
"enable_costs": ("instrumentation", "enable_costs"),
|
|
665
|
+
"auto_instrument_tools": ("instrumentation", "auto_instrument_tools"),
|
|
666
|
+
"max_tool_spans": ("instrumentation", "max_tool_spans"),
|
|
667
|
+
"max_span_depth": ("instrumentation", "max_span_depth"),
|
|
668
|
+
# Rate limiting & Batching
|
|
669
|
+
"max_spans_per_second": ("rate_limiting", "max_spans_per_second"),
|
|
670
|
+
"max_queue_size": ("rate_limiting", "max_queue_size"),
|
|
671
|
+
"max_block_ms": ("rate_limiting", "max_block_ms"),
|
|
672
|
+
"max_export_batch_size": ("rate_limiting", "max_export_batch_size"),
|
|
673
|
+
"schedule_delay_millis": ("rate_limiting", "schedule_delay_millis"),
|
|
674
|
+
# Runtime
|
|
675
|
+
"session_id": ("runtime", "session_id"),
|
|
676
|
+
"user_id": ("runtime", "user_id"),
|
|
677
|
+
"tenant_id": ("runtime", "tenant_id"),
|
|
678
|
+
"project_id": ("runtime", "project_id"),
|
|
679
|
+
"agent_id": ("runtime", "agent_id"),
|
|
680
|
+
# Logging
|
|
681
|
+
"debug": ("logging", "debug"),
|
|
682
|
+
"enable_span_logging": ("logging", "enable_span_logging"),
|
|
683
|
+
# Advanced
|
|
684
|
+
"attr_truncation_limit": ("advanced", "attr_truncation_limit"),
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
for flat_key, value in overrides.items():
|
|
688
|
+
if flat_key in flat_to_nested:
|
|
689
|
+
section, nested_key = flat_to_nested[flat_key]
|
|
690
|
+
nested_overrides[section][nested_key] = value
|
|
691
|
+
|
|
692
|
+
config = load_config(config_file=config_file, overrides=nested_overrides)
|
|
693
|
+
return config.to_flat_dict()
|