iac-code 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.
- iac_code/__init__.py +2 -0
- iac_code/acp/__init__.py +97 -0
- iac_code/acp/convert.py +423 -0
- iac_code/acp/http_sse.py +448 -0
- iac_code/acp/mcp.py +54 -0
- iac_code/acp/metrics.py +71 -0
- iac_code/acp/server.py +662 -0
- iac_code/acp/session.py +446 -0
- iac_code/acp/slash_registry.py +125 -0
- iac_code/acp/state.py +99 -0
- iac_code/acp/tools.py +112 -0
- iac_code/acp/types.py +13 -0
- iac_code/acp/version.py +26 -0
- iac_code/agent/__init__.py +19 -0
- iac_code/agent/agent_loop.py +640 -0
- iac_code/agent/agent_tool.py +269 -0
- iac_code/agent/agent_types.py +87 -0
- iac_code/agent/message.py +153 -0
- iac_code/agent/system_prompt.py +313 -0
- iac_code/cli/__init__.py +3 -0
- iac_code/cli/headless.py +114 -0
- iac_code/cli/main.py +246 -0
- iac_code/cli/output_formats.py +125 -0
- iac_code/commands/__init__.py +93 -0
- iac_code/commands/auth.py +1055 -0
- iac_code/commands/clear.py +34 -0
- iac_code/commands/compact.py +43 -0
- iac_code/commands/debug.py +45 -0
- iac_code/commands/effort.py +116 -0
- iac_code/commands/exit.py +10 -0
- iac_code/commands/help.py +49 -0
- iac_code/commands/model.py +130 -0
- iac_code/commands/registry.py +245 -0
- iac_code/commands/resume.py +49 -0
- iac_code/commands/tasks.py +41 -0
- iac_code/config.py +304 -0
- iac_code/i18n/__init__.py +141 -0
- iac_code/i18n/locales/zh/LC_MESSAGES/messages.po +1355 -0
- iac_code/memory/__init__.py +1 -0
- iac_code/memory/memory_manager.py +92 -0
- iac_code/memory/memory_tools.py +88 -0
- iac_code/providers/__init__.py +1 -0
- iac_code/providers/anthropic_provider.py +284 -0
- iac_code/providers/base.py +128 -0
- iac_code/providers/dashscope_provider.py +47 -0
- iac_code/providers/deepseek_provider.py +36 -0
- iac_code/providers/manager.py +399 -0
- iac_code/providers/openai_provider.py +344 -0
- iac_code/providers/retry.py +58 -0
- iac_code/providers/stream_watchdog.py +47 -0
- iac_code/providers/thinking.py +164 -0
- iac_code/services/__init__.py +1 -0
- iac_code/services/agent_factory.py +127 -0
- iac_code/services/cloud_credentials.py +22 -0
- iac_code/services/context_manager.py +221 -0
- iac_code/services/providers/__init__.py +1 -0
- iac_code/services/providers/aliyun.py +232 -0
- iac_code/services/session_index.py +281 -0
- iac_code/services/session_storage.py +245 -0
- iac_code/services/telemetry/__init__.py +66 -0
- iac_code/services/telemetry/attributes.py +84 -0
- iac_code/services/telemetry/client.py +330 -0
- iac_code/services/telemetry/config.py +76 -0
- iac_code/services/telemetry/constants.py +75 -0
- iac_code/services/telemetry/content_serializer.py +124 -0
- iac_code/services/telemetry/events.py +42 -0
- iac_code/services/telemetry/fallback.py +59 -0
- iac_code/services/telemetry/identity.py +73 -0
- iac_code/services/telemetry/metrics.py +62 -0
- iac_code/services/telemetry/names.py +199 -0
- iac_code/services/telemetry/sanitize.py +88 -0
- iac_code/services/telemetry/sink.py +67 -0
- iac_code/services/telemetry/tracing.py +38 -0
- iac_code/services/telemetry/types.py +13 -0
- iac_code/services/token_budget.py +54 -0
- iac_code/services/token_counter.py +76 -0
- iac_code/skills/__init__.py +1 -0
- iac_code/skills/bundled/__init__.py +94 -0
- iac_code/skills/bundled/iac_aliyun/SKILL.md +192 -0
- iac_code/skills/bundled/iac_aliyun/__init__.py +16 -0
- iac_code/skills/bundled/iac_aliyun/references/cloud-products/ecs.md +167 -0
- iac_code/skills/bundled/iac_aliyun/references/cloud-products/oss.md +69 -0
- iac_code/skills/bundled/iac_aliyun/references/cloud-products/rds.md +95 -0
- iac_code/skills/bundled/iac_aliyun/references/cloud-products/redis.md +100 -0
- iac_code/skills/bundled/iac_aliyun/references/cloud-products/slb.md +60 -0
- iac_code/skills/bundled/iac_aliyun/references/cloud-products/vpc.md +54 -0
- iac_code/skills/bundled/iac_aliyun/references/ros-template.md +155 -0
- iac_code/skills/bundled/iac_aliyun/references/template-parameters.md +206 -0
- iac_code/skills/bundled/iac_aliyun/references/terraform-template.md +101 -0
- iac_code/skills/bundled/iac_aliyun/scripts/tf2ros.py +77 -0
- iac_code/skills/bundled/simplify.py +28 -0
- iac_code/skills/discovery.py +136 -0
- iac_code/skills/frontmatter.py +119 -0
- iac_code/skills/listing.py +92 -0
- iac_code/skills/loader.py +42 -0
- iac_code/skills/processor.py +81 -0
- iac_code/skills/renderer.py +157 -0
- iac_code/skills/skill_definition.py +82 -0
- iac_code/skills/skill_tool.py +261 -0
- iac_code/state/__init__.py +5 -0
- iac_code/state/app_state.py +122 -0
- iac_code/tasks/__init__.py +1 -0
- iac_code/tasks/notification_queue.py +28 -0
- iac_code/tasks/task_state.py +66 -0
- iac_code/tasks/task_tools.py +114 -0
- iac_code/tools/__init__.py +8 -0
- iac_code/tools/base.py +226 -0
- iac_code/tools/bash.py +133 -0
- iac_code/tools/cloud/__init__.py +0 -0
- iac_code/tools/cloud/aliyun/__init__.py +0 -0
- iac_code/tools/cloud/aliyun/aliyun_api.py +510 -0
- iac_code/tools/cloud/aliyun/aliyun_doc_search.py +145 -0
- iac_code/tools/cloud/aliyun/endpoints.yml +343 -0
- iac_code/tools/cloud/aliyun/ros_client.py +56 -0
- iac_code/tools/cloud/aliyun/ros_stack.py +633 -0
- iac_code/tools/cloud/aliyun/ros_stack_instances.py +247 -0
- iac_code/tools/cloud/base_api.py +162 -0
- iac_code/tools/cloud/base_stack.py +242 -0
- iac_code/tools/cloud/registry.py +20 -0
- iac_code/tools/cloud/types.py +105 -0
- iac_code/tools/edit_file.py +121 -0
- iac_code/tools/glob.py +103 -0
- iac_code/tools/grep.py +254 -0
- iac_code/tools/list_files.py +104 -0
- iac_code/tools/read_file.py +127 -0
- iac_code/tools/result_storage.py +39 -0
- iac_code/tools/tool_executor.py +165 -0
- iac_code/tools/web_fetch.py +177 -0
- iac_code/tools/write_file.py +88 -0
- iac_code/types/__init__.py +40 -0
- iac_code/types/permissions.py +26 -0
- iac_code/types/skill_source.py +11 -0
- iac_code/types/stream_events.py +227 -0
- iac_code/ui/__init__.py +5 -0
- iac_code/ui/banner.py +110 -0
- iac_code/ui/components/__init__.py +0 -0
- iac_code/ui/components/dialog.py +142 -0
- iac_code/ui/components/divider.py +20 -0
- iac_code/ui/components/fuzzy_picker.py +308 -0
- iac_code/ui/components/progress_bar.py +54 -0
- iac_code/ui/components/search_box.py +165 -0
- iac_code/ui/components/select.py +319 -0
- iac_code/ui/components/status_icon.py +42 -0
- iac_code/ui/components/tabs.py +128 -0
- iac_code/ui/core/__init__.py +0 -0
- iac_code/ui/core/in_place_render.py +129 -0
- iac_code/ui/core/input_history.py +118 -0
- iac_code/ui/core/key_event.py +41 -0
- iac_code/ui/core/prompt_input.py +507 -0
- iac_code/ui/core/raw_input.py +302 -0
- iac_code/ui/core/screen.py +80 -0
- iac_code/ui/dialogs/__init__.py +0 -0
- iac_code/ui/dialogs/global_search.py +178 -0
- iac_code/ui/dialogs/history_search.py +100 -0
- iac_code/ui/dialogs/model_picker.py +280 -0
- iac_code/ui/dialogs/quick_open.py +108 -0
- iac_code/ui/dialogs/resume_picker.py +749 -0
- iac_code/ui/keybindings/__init__.py +0 -0
- iac_code/ui/keybindings/manager.py +124 -0
- iac_code/ui/renderer.py +1535 -0
- iac_code/ui/repl.py +772 -0
- iac_code/ui/spinner.py +112 -0
- iac_code/ui/suggestions/__init__.py +0 -0
- iac_code/ui/suggestions/aggregator.py +171 -0
- iac_code/ui/suggestions/command_provider.py +43 -0
- iac_code/ui/suggestions/directory_provider.py +95 -0
- iac_code/ui/suggestions/file_provider.py +121 -0
- iac_code/ui/suggestions/shell_history_provider.py +108 -0
- iac_code/ui/suggestions/token_extractor.py +77 -0
- iac_code/ui/suggestions/types.py +45 -0
- iac_code/ui/transcript_view.py +199 -0
- iac_code/utils/__init__.py +0 -0
- iac_code/utils/background_housekeeping.py +53 -0
- iac_code/utils/cleanup.py +68 -0
- iac_code/utils/json_utils.py +60 -0
- iac_code/utils/log.py +150 -0
- iac_code/utils/project_paths.py +74 -0
- iac_code/utils/tool_input_parser.py +62 -0
- iac_code-0.1.0.dist-info/LICENSE +201 -0
- iac_code-0.1.0.dist-info/METADATA +64 -0
- iac_code-0.1.0.dist-info/RECORD +184 -0
- iac_code-0.1.0.dist-info/WHEEL +5 -0
- iac_code-0.1.0.dist-info/entry_points.txt +2 -0
- iac_code-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""TelemetryClient — top-level facade that wires all components."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from opentelemetry._logs import set_logger_provider
|
|
11
|
+
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
|
|
12
|
+
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
|
|
13
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
14
|
+
from opentelemetry.sdk._logs import LoggerProvider
|
|
15
|
+
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
|
|
16
|
+
from opentelemetry.sdk.metrics import MeterProvider
|
|
17
|
+
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
|
|
18
|
+
from opentelemetry.sdk.resources import Resource
|
|
19
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
20
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
21
|
+
|
|
22
|
+
from iac_code.config import get_config_dir, get_settings_path
|
|
23
|
+
from iac_code.services.telemetry.attributes import AttributeBuilder
|
|
24
|
+
from iac_code.services.telemetry.config import is_telemetry_disabled
|
|
25
|
+
from iac_code.services.telemetry.events import EventEmitter
|
|
26
|
+
from iac_code.services.telemetry.fallback import FallbackStore
|
|
27
|
+
from iac_code.services.telemetry.identity import Identity
|
|
28
|
+
from iac_code.services.telemetry.metrics import MetricsRegistry
|
|
29
|
+
from iac_code.services.telemetry.names import Events
|
|
30
|
+
from iac_code.services.telemetry.sink import AnalyticsSink
|
|
31
|
+
from iac_code.services.telemetry.tracing import SpanFactory
|
|
32
|
+
|
|
33
|
+
log = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
_METRICS_EXPORT_INTERVAL_MS = 60_000
|
|
36
|
+
_EVENTS_BATCH_DELAY_MS = 10_000
|
|
37
|
+
_EVENTS_BATCH_MAX_SIZE = 200
|
|
38
|
+
_FLUSH_TIMEOUT_MS = 2_000
|
|
39
|
+
|
|
40
|
+
_SERVICE_NAME = "iac-code"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TelemetryClient:
|
|
44
|
+
"""Top-level facade. Assembles all components and manages lifecycle."""
|
|
45
|
+
|
|
46
|
+
# ------------------------------------------------------------------
|
|
47
|
+
# Built-in default backend configuration
|
|
48
|
+
# ------------------------------------------------------------------
|
|
49
|
+
# Initial target: Aliyun ARMS (APM tracing). Users override via
|
|
50
|
+
# IAC_CODE_TELEMETRY_* environment variables. Each signal has its own
|
|
51
|
+
# endpoint because ARMS uses different URL paths per signal (unlike
|
|
52
|
+
# standard OTLP which uses a single base URL).
|
|
53
|
+
#
|
|
54
|
+
# To switch to a different backend, update these class constants (or
|
|
55
|
+
# have your release pipeline inject them). The code itself is
|
|
56
|
+
# backend-agnostic — it just speaks OTLP/HTTP.
|
|
57
|
+
_DEFAULT_TRACES_ENDPOINT_FALLBACK = (
|
|
58
|
+
"https://proj-xtrace-64f8c93632b0d2a93035551d6b73bcf-cn-beijing"
|
|
59
|
+
".cn-beijing.log.aliyuncs.com/apm/trace/opentelemetry/v1/traces"
|
|
60
|
+
)
|
|
61
|
+
_DEFAULT_METRICS_ENDPOINT_FALLBACK = "" # ARMS metrics OTLP endpoint TBD
|
|
62
|
+
_DEFAULT_LOGS_ENDPOINT_FALLBACK = "" # ARMS logs OTLP endpoint TBD
|
|
63
|
+
_DEFAULT_HEADERS_FALLBACK = (
|
|
64
|
+
"x-arms-license-key=h9y3c809kq@6f8319efc69b73a,"
|
|
65
|
+
"x-arms-project=proj-xtrace-64f8c93632b0d2a93035551d6b73bcf-cn-beijing,"
|
|
66
|
+
"x-cms-workspace=iac-code"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
*,
|
|
72
|
+
session_id: str | None = None,
|
|
73
|
+
identity: Identity | None = None,
|
|
74
|
+
attributes: AttributeBuilder | None = None,
|
|
75
|
+
metrics: MetricsRegistry | None = None,
|
|
76
|
+
events: EventEmitter | None = None,
|
|
77
|
+
tracer: SpanFactory | None = None,
|
|
78
|
+
sink: AnalyticsSink | None = None,
|
|
79
|
+
fallback: FallbackStore | None = None,
|
|
80
|
+
settings_path: Path | None = None,
|
|
81
|
+
telemetry_dir: Path | None = None,
|
|
82
|
+
) -> None:
|
|
83
|
+
# Default-wire every component that wasn't injected.
|
|
84
|
+
self._identity = identity or Identity(settings_path or get_settings_path(), session_id=session_id)
|
|
85
|
+
self._attributes = attributes or AttributeBuilder(self._identity, _SERVICE_NAME)
|
|
86
|
+
self._metrics = metrics or MetricsRegistry()
|
|
87
|
+
self._events = events or EventEmitter(self._attributes)
|
|
88
|
+
self._tracer = tracer or SpanFactory()
|
|
89
|
+
self._sink = sink or AnalyticsSink(self._events)
|
|
90
|
+
self._fallback = fallback or FallbackStore(telemetry_dir or (get_config_dir() / "telemetry"))
|
|
91
|
+
|
|
92
|
+
self._meter_provider: MeterProvider | None = None
|
|
93
|
+
self._logger_provider: LoggerProvider | None = None
|
|
94
|
+
self._tracer_provider: TracerProvider | None = None
|
|
95
|
+
self._bootstrapped = False
|
|
96
|
+
|
|
97
|
+
# -------- Public API --------
|
|
98
|
+
|
|
99
|
+
def log_event(self, event_name: str, metadata: dict[str, Any] | None = None) -> None:
|
|
100
|
+
self._sink.log_event(event_name, metadata or {})
|
|
101
|
+
|
|
102
|
+
def add_metric(self, name: str, value: int | float, attrs: dict[str, Any] | None = None) -> None:
|
|
103
|
+
self._metrics.add(name, value, attrs or {})
|
|
104
|
+
|
|
105
|
+
def start_span(self, name: str, attrs: dict[str, Any] | None = None):
|
|
106
|
+
return self._tracer.start(name, attrs)
|
|
107
|
+
|
|
108
|
+
def get_session_id(self) -> str:
|
|
109
|
+
return self._identity.get_session_id()
|
|
110
|
+
|
|
111
|
+
def get_user_id(self) -> str:
|
|
112
|
+
return self._identity.get_user_id()
|
|
113
|
+
|
|
114
|
+
# -------- Lifecycle --------
|
|
115
|
+
|
|
116
|
+
def bootstrap(self) -> None:
|
|
117
|
+
"""Wire OTel SDK, activate sink, replay pre-queue, retry failed batches.
|
|
118
|
+
|
|
119
|
+
Idempotent.
|
|
120
|
+
"""
|
|
121
|
+
if self._bootstrapped:
|
|
122
|
+
return
|
|
123
|
+
self._bootstrapped = True
|
|
124
|
+
|
|
125
|
+
resource = Resource.create(self._attributes.build_resource())
|
|
126
|
+
|
|
127
|
+
# MeterProvider
|
|
128
|
+
readers: list = []
|
|
129
|
+
if self._default_metrics_enabled():
|
|
130
|
+
readers.append(
|
|
131
|
+
PeriodicExportingMetricReader(
|
|
132
|
+
self._build_default_metric_exporter(),
|
|
133
|
+
export_interval_millis=_METRICS_EXPORT_INTERVAL_MS,
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
self._maybe_append_user_metric_reader(readers)
|
|
137
|
+
self._meter_provider = MeterProvider(resource=resource, metric_readers=readers)
|
|
138
|
+
self._metrics.register_all(self._meter_provider.get_meter("com.iac-code.metrics", "1.0.0"))
|
|
139
|
+
|
|
140
|
+
# LoggerProvider (Events)
|
|
141
|
+
lp = LoggerProvider(resource=resource)
|
|
142
|
+
if self._default_logs_enabled():
|
|
143
|
+
lp.add_log_record_processor(
|
|
144
|
+
BatchLogRecordProcessor(
|
|
145
|
+
self._build_default_log_exporter(),
|
|
146
|
+
schedule_delay_millis=_EVENTS_BATCH_DELAY_MS,
|
|
147
|
+
max_export_batch_size=_EVENTS_BATCH_MAX_SIZE,
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
self._maybe_append_user_log_processor(lp)
|
|
151
|
+
self._logger_provider = lp
|
|
152
|
+
set_logger_provider(lp)
|
|
153
|
+
self._events.attach(lp.get_logger("com.iac-code.events", "1.0.0"))
|
|
154
|
+
|
|
155
|
+
# TracerProvider
|
|
156
|
+
tp = TracerProvider(resource=resource)
|
|
157
|
+
if self._default_traces_enabled():
|
|
158
|
+
tp.add_span_processor(BatchSpanProcessor(self._build_default_span_exporter()))
|
|
159
|
+
self._maybe_append_user_span_processor(tp)
|
|
160
|
+
self._tracer_provider = tp
|
|
161
|
+
self._tracer.attach(tp.get_tracer("com.iac-code.tracing", "1.0.0"))
|
|
162
|
+
|
|
163
|
+
# Activate sink + drain pre-queue
|
|
164
|
+
self._sink.activate()
|
|
165
|
+
self._sink.drain_soon()
|
|
166
|
+
|
|
167
|
+
# iac.init once per lifetime
|
|
168
|
+
if self._identity.was_first_run():
|
|
169
|
+
self.log_event(Events.INIT, {"is_first_run": True})
|
|
170
|
+
|
|
171
|
+
# Retry previously failed batches
|
|
172
|
+
self._retry_failed_batches()
|
|
173
|
+
|
|
174
|
+
def shutdown(self) -> None:
|
|
175
|
+
"""Force-flush providers with bounded timeout; never raise."""
|
|
176
|
+
for provider, label in (
|
|
177
|
+
(self._meter_provider, "MeterProvider"),
|
|
178
|
+
(self._logger_provider, "LoggerProvider"),
|
|
179
|
+
(self._tracer_provider, "TracerProvider"),
|
|
180
|
+
):
|
|
181
|
+
if provider is None:
|
|
182
|
+
continue
|
|
183
|
+
self._safe_flush(provider, label)
|
|
184
|
+
|
|
185
|
+
# -------- Default OTLP backend wiring helpers --------
|
|
186
|
+
|
|
187
|
+
@classmethod
|
|
188
|
+
def _traces_endpoint(cls) -> str:
|
|
189
|
+
"""Full URL for traces export. Per-signal override > base + suffix > fallback."""
|
|
190
|
+
override = os.environ.get("IAC_CODE_TELEMETRY_TRACES_ENDPOINT", "").strip()
|
|
191
|
+
if override:
|
|
192
|
+
return override
|
|
193
|
+
base = os.environ.get("IAC_CODE_TELEMETRY_ENDPOINT", "").strip()
|
|
194
|
+
if base:
|
|
195
|
+
return f"{base}/v1/traces"
|
|
196
|
+
return cls._DEFAULT_TRACES_ENDPOINT_FALLBACK
|
|
197
|
+
|
|
198
|
+
@classmethod
|
|
199
|
+
def _metrics_endpoint(cls) -> str:
|
|
200
|
+
override = os.environ.get("IAC_CODE_TELEMETRY_METRICS_ENDPOINT", "").strip()
|
|
201
|
+
if override:
|
|
202
|
+
return override
|
|
203
|
+
base = os.environ.get("IAC_CODE_TELEMETRY_ENDPOINT", "").strip()
|
|
204
|
+
if base:
|
|
205
|
+
return f"{base}/v1/metrics"
|
|
206
|
+
return cls._DEFAULT_METRICS_ENDPOINT_FALLBACK
|
|
207
|
+
|
|
208
|
+
@classmethod
|
|
209
|
+
def _logs_endpoint(cls) -> str:
|
|
210
|
+
override = os.environ.get("IAC_CODE_TELEMETRY_LOGS_ENDPOINT", "").strip()
|
|
211
|
+
if override:
|
|
212
|
+
return override
|
|
213
|
+
base = os.environ.get("IAC_CODE_TELEMETRY_ENDPOINT", "").strip()
|
|
214
|
+
if base:
|
|
215
|
+
return f"{base}/v1/logs"
|
|
216
|
+
return cls._DEFAULT_LOGS_ENDPOINT_FALLBACK
|
|
217
|
+
|
|
218
|
+
@classmethod
|
|
219
|
+
def _default_headers(cls) -> dict[str, str]:
|
|
220
|
+
"""Parse 'k1=v1,k2=v2' format (matches OTEL_EXPORTER_OTLP_HEADERS)."""
|
|
221
|
+
raw = os.environ.get("IAC_CODE_TELEMETRY_HEADERS", cls._DEFAULT_HEADERS_FALLBACK)
|
|
222
|
+
headers: dict[str, str] = {}
|
|
223
|
+
for part in raw.split(","):
|
|
224
|
+
part = part.strip()
|
|
225
|
+
if not part or "=" not in part:
|
|
226
|
+
continue
|
|
227
|
+
k, _, v = part.partition("=")
|
|
228
|
+
headers[k.strip()] = v.strip()
|
|
229
|
+
return headers
|
|
230
|
+
|
|
231
|
+
@classmethod
|
|
232
|
+
def _default_traces_enabled(cls) -> bool:
|
|
233
|
+
return bool(cls._traces_endpoint()) and not is_telemetry_disabled()
|
|
234
|
+
|
|
235
|
+
@classmethod
|
|
236
|
+
def _default_metrics_enabled(cls) -> bool:
|
|
237
|
+
return bool(cls._metrics_endpoint()) and not is_telemetry_disabled()
|
|
238
|
+
|
|
239
|
+
@classmethod
|
|
240
|
+
def _default_logs_enabled(cls) -> bool:
|
|
241
|
+
return bool(cls._logs_endpoint()) and not is_telemetry_disabled()
|
|
242
|
+
|
|
243
|
+
@classmethod
|
|
244
|
+
def _build_default_metric_exporter(cls) -> OTLPMetricExporter:
|
|
245
|
+
return OTLPMetricExporter(
|
|
246
|
+
endpoint=cls._metrics_endpoint(),
|
|
247
|
+
headers=cls._default_headers(),
|
|
248
|
+
timeout=5,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
@classmethod
|
|
252
|
+
def _build_default_log_exporter(cls) -> OTLPLogExporter:
|
|
253
|
+
return OTLPLogExporter(
|
|
254
|
+
endpoint=cls._logs_endpoint(),
|
|
255
|
+
headers=cls._default_headers(),
|
|
256
|
+
timeout=10,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
@classmethod
|
|
260
|
+
def _build_default_span_exporter(cls) -> OTLPSpanExporter:
|
|
261
|
+
return OTLPSpanExporter(
|
|
262
|
+
endpoint=cls._traces_endpoint(),
|
|
263
|
+
headers=cls._default_headers(),
|
|
264
|
+
timeout=10,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
@staticmethod
|
|
268
|
+
def _user_otlp_enabled() -> bool:
|
|
269
|
+
return bool(os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT"))
|
|
270
|
+
|
|
271
|
+
def _maybe_append_user_metric_reader(self, readers: list) -> None:
|
|
272
|
+
if not self._user_otlp_enabled():
|
|
273
|
+
return
|
|
274
|
+
try:
|
|
275
|
+
readers.append(
|
|
276
|
+
PeriodicExportingMetricReader(
|
|
277
|
+
OTLPMetricExporter(), # reads env
|
|
278
|
+
export_interval_millis=_METRICS_EXPORT_INTERVAL_MS,
|
|
279
|
+
)
|
|
280
|
+
)
|
|
281
|
+
except Exception as e:
|
|
282
|
+
log.warning("Failed to wire user OTLP metric exporter: %s", e)
|
|
283
|
+
|
|
284
|
+
def _maybe_append_user_log_processor(self, provider: LoggerProvider) -> None:
|
|
285
|
+
if not self._user_otlp_enabled():
|
|
286
|
+
return
|
|
287
|
+
try:
|
|
288
|
+
provider.add_log_record_processor(
|
|
289
|
+
BatchLogRecordProcessor(
|
|
290
|
+
OTLPLogExporter(),
|
|
291
|
+
schedule_delay_millis=_EVENTS_BATCH_DELAY_MS,
|
|
292
|
+
max_export_batch_size=_EVENTS_BATCH_MAX_SIZE,
|
|
293
|
+
)
|
|
294
|
+
)
|
|
295
|
+
except Exception as e:
|
|
296
|
+
log.warning("Failed to wire user OTLP log exporter: %s", e)
|
|
297
|
+
|
|
298
|
+
def _maybe_append_user_span_processor(self, provider: TracerProvider) -> None:
|
|
299
|
+
if not self._user_otlp_enabled():
|
|
300
|
+
return
|
|
301
|
+
try:
|
|
302
|
+
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
|
|
303
|
+
except Exception as e:
|
|
304
|
+
log.warning("Failed to wire user OTLP span exporter: %s", e)
|
|
305
|
+
|
|
306
|
+
def _retry_failed_batches(self) -> None:
|
|
307
|
+
"""Scan disk for failed batches from previous sessions, re-emit, delete."""
|
|
308
|
+
for path in self._fallback.list_pending():
|
|
309
|
+
try:
|
|
310
|
+
for event in self._fallback.read(path):
|
|
311
|
+
name = event.pop("event.name", "iac.unknown")
|
|
312
|
+
self._events.emit(name, event)
|
|
313
|
+
self._fallback.remove(path)
|
|
314
|
+
except Exception as e:
|
|
315
|
+
log.warning("Failed to retry batch %s: %s", path, e)
|
|
316
|
+
|
|
317
|
+
@staticmethod
|
|
318
|
+
def _safe_flush(provider: object, label: str) -> None:
|
|
319
|
+
flush = getattr(provider, "force_flush", None)
|
|
320
|
+
if flush is not None:
|
|
321
|
+
try:
|
|
322
|
+
flush(_FLUSH_TIMEOUT_MS)
|
|
323
|
+
except Exception as e:
|
|
324
|
+
log.warning("Flush %s failed: %s", label, e)
|
|
325
|
+
shutdown = getattr(provider, "shutdown", None)
|
|
326
|
+
if shutdown is not None:
|
|
327
|
+
try:
|
|
328
|
+
shutdown()
|
|
329
|
+
except Exception as e:
|
|
330
|
+
log.warning("Shutdown %s failed: %s", label, e)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Privacy-level and content-capture detection for telemetry."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from enum import Enum
|
|
7
|
+
|
|
8
|
+
# =====================================================================
|
|
9
|
+
# Privacy level
|
|
10
|
+
# =====================================================================
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PrivacyLevel(Enum):
|
|
14
|
+
DEFAULT = "default"
|
|
15
|
+
NO_TELEMETRY = "no-telemetry"
|
|
16
|
+
ESSENTIAL_TRAFFIC = "essential-traffic"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
_TRUTHY = {"1", "true", "yes", "on"}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _is_env_truthy(name: str) -> bool:
|
|
23
|
+
raw = os.environ.get(name, "").strip().lower()
|
|
24
|
+
return raw in _TRUTHY
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_privacy_level() -> PrivacyLevel:
|
|
28
|
+
"""Most restrictive wins: ESSENTIAL_TRAFFIC > NO_TELEMETRY > DEFAULT."""
|
|
29
|
+
if _is_env_truthy("IAC_CODE_DISABLE_NONESSENTIAL_TRAFFIC"):
|
|
30
|
+
return PrivacyLevel.ESSENTIAL_TRAFFIC
|
|
31
|
+
if _is_env_truthy("DISABLE_TELEMETRY"):
|
|
32
|
+
return PrivacyLevel.NO_TELEMETRY
|
|
33
|
+
return PrivacyLevel.DEFAULT
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def is_telemetry_disabled() -> bool:
|
|
37
|
+
return get_privacy_level() != PrivacyLevel.DEFAULT
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def is_essential_traffic_only() -> bool:
|
|
41
|
+
return get_privacy_level() == PrivacyLevel.ESSENTIAL_TRAFFIC
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# =====================================================================
|
|
45
|
+
# Content capture mode (gen_ai message/tool content on spans)
|
|
46
|
+
# =====================================================================
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ContentCaptureMode(Enum):
|
|
50
|
+
NO_CONTENT = "no_content"
|
|
51
|
+
SPAN_ONLY = "span_only"
|
|
52
|
+
EVENT_ONLY = "event_only"
|
|
53
|
+
SPAN_AND_EVENT = "span_and_event"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def get_content_capture_mode() -> ContentCaptureMode:
|
|
57
|
+
"""Read OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT env var.
|
|
58
|
+
|
|
59
|
+
Compatible with loongsuite-util-genai. Default: NO_CONTENT.
|
|
60
|
+
"""
|
|
61
|
+
raw = os.environ.get("OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", "").strip().upper()
|
|
62
|
+
_mapping = {
|
|
63
|
+
"SPAN_ONLY": ContentCaptureMode.SPAN_ONLY,
|
|
64
|
+
"EVENT_ONLY": ContentCaptureMode.EVENT_ONLY,
|
|
65
|
+
"SPAN_AND_EVENT": ContentCaptureMode.SPAN_AND_EVENT,
|
|
66
|
+
}
|
|
67
|
+
return _mapping.get(raw, ContentCaptureMode.NO_CONTENT)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def should_capture_content_on_span() -> bool:
|
|
71
|
+
from iac_code.utils.log import is_debug_enabled
|
|
72
|
+
|
|
73
|
+
if is_debug_enabled():
|
|
74
|
+
return True
|
|
75
|
+
mode = get_content_capture_mode()
|
|
76
|
+
return mode in (ContentCaptureMode.SPAN_ONLY, ContentCaptureMode.SPAN_AND_EVENT)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Whitelists used by the sanitize module."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
# bundled skills keep their real name. Custom user skills
|
|
6
|
+
# (outside this set) become "custom".
|
|
7
|
+
BUNDLED_SKILLS: frozenset[str] = frozenset(
|
|
8
|
+
{
|
|
9
|
+
"iac_aliyun",
|
|
10
|
+
}
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
# Terraform official providers keep their real name.
|
|
14
|
+
# Custom providers become "other".
|
|
15
|
+
TERRAFORM_OFFICIAL_PROVIDERS: frozenset[str] = frozenset(
|
|
16
|
+
{
|
|
17
|
+
"alicloud",
|
|
18
|
+
"aws",
|
|
19
|
+
"azurerm",
|
|
20
|
+
"google",
|
|
21
|
+
"kubernetes",
|
|
22
|
+
"oci",
|
|
23
|
+
"tencentcloud",
|
|
24
|
+
"huaweicloud",
|
|
25
|
+
"volcengine",
|
|
26
|
+
"vsphere",
|
|
27
|
+
"helm",
|
|
28
|
+
"null",
|
|
29
|
+
"random",
|
|
30
|
+
"time",
|
|
31
|
+
"archive",
|
|
32
|
+
"local",
|
|
33
|
+
"external",
|
|
34
|
+
"http",
|
|
35
|
+
"tls",
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# ROS resource type prefixes.
|
|
40
|
+
ROS_ALLOWED_PREFIXES: tuple[str, ...] = ("ALIYUN::", "DATASOURCE::")
|
|
41
|
+
|
|
42
|
+
# normalized model names. Unknown → "other".
|
|
43
|
+
KNOWN_MODELS: frozenset[str] = frozenset(
|
|
44
|
+
{
|
|
45
|
+
# Anthropic
|
|
46
|
+
"claude-opus-4-7",
|
|
47
|
+
"claude-sonnet-4-6",
|
|
48
|
+
"claude-haiku-4-5",
|
|
49
|
+
"claude-opus-4-6",
|
|
50
|
+
"claude-sonnet-4-5",
|
|
51
|
+
"claude-haiku-4-5-20251001",
|
|
52
|
+
# OpenAI
|
|
53
|
+
"gpt-4o",
|
|
54
|
+
"gpt-4o-mini",
|
|
55
|
+
"gpt-4-turbo",
|
|
56
|
+
"o1",
|
|
57
|
+
"o1-mini",
|
|
58
|
+
"o3-mini",
|
|
59
|
+
# Dashscope / Qwen
|
|
60
|
+
"qwen-max",
|
|
61
|
+
"qwen-plus",
|
|
62
|
+
"qwen-turbo",
|
|
63
|
+
"qwen3-coder",
|
|
64
|
+
"qwen2.5-coder",
|
|
65
|
+
"qwen2.5-72b-instruct",
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Sentinels used throughout the module.
|
|
70
|
+
CUSTOM_SKILL_PLACEHOLDER = "custom"
|
|
71
|
+
OTHER_MODEL_PLACEHOLDER = "other"
|
|
72
|
+
CUSTOM_TF_PROVIDER_PLACEHOLDER = "other"
|
|
73
|
+
CUSTOM_ROS_RESOURCE_PLACEHOLDER = "Custom::Other"
|
|
74
|
+
CUSTOM_TF_RESOURCE_PLACEHOLDER = "custom_provider::other"
|
|
75
|
+
MCP_TOOL_PLACEHOLDER = "mcp_tool"
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Serialize messages, tools, and content for gen_ai.* span attributes.
|
|
2
|
+
|
|
3
|
+
Follows the ARMS LLM trace field schema for gen_ai.input.messages,
|
|
4
|
+
gen_ai.output.messages, gen_ai.system_instructions, gen_ai.tool.definitions,
|
|
5
|
+
gen_ai.tool.call.arguments, and gen_ai.tool.call.result.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
_MAX_CONTENT_BYTES = 4096
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _truncate(s: str, max_bytes: int = _MAX_CONTENT_BYTES) -> str:
|
|
17
|
+
encoded = s.encode("utf-8")
|
|
18
|
+
if len(encoded) <= max_bytes:
|
|
19
|
+
return s
|
|
20
|
+
return encoded[:max_bytes].decode("utf-8", errors="ignore") + "...[truncated]"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _json_dumps(obj: Any) -> str:
|
|
24
|
+
return json.dumps(obj, ensure_ascii=False, default=str)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def serialize_user_input(user_input: str) -> str:
|
|
28
|
+
"""Serialize a plain user input string to gen_ai.input.messages JSON."""
|
|
29
|
+
return _json_dumps([{"role": "user", "parts": [{"type": "text", "content": _truncate(user_input)}]}])
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def serialize_input_messages(messages: list) -> str:
|
|
33
|
+
"""Serialize provider Message list to gen_ai.input.messages JSON string.
|
|
34
|
+
|
|
35
|
+
OTel semconv: [{role, parts: [{type, content|...}]}]
|
|
36
|
+
"""
|
|
37
|
+
result = []
|
|
38
|
+
for msg in messages:
|
|
39
|
+
role = getattr(msg, "role", "unknown")
|
|
40
|
+
content = getattr(msg, "content", "")
|
|
41
|
+
if isinstance(content, str):
|
|
42
|
+
parts = [{"type": "text", "content": _truncate(content)}]
|
|
43
|
+
elif isinstance(content, list):
|
|
44
|
+
parts = []
|
|
45
|
+
for block in content:
|
|
46
|
+
btype = getattr(block, "type", "text")
|
|
47
|
+
if btype == "text":
|
|
48
|
+
parts.append({"type": "text", "content": _truncate(getattr(block, "text", "") or "")})
|
|
49
|
+
elif btype == "tool_use":
|
|
50
|
+
parts.append(
|
|
51
|
+
{
|
|
52
|
+
"type": "tool_call",
|
|
53
|
+
"name": getattr(block, "name", ""),
|
|
54
|
+
"id": getattr(block, "tool_use_id", ""),
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
elif btype == "tool_result":
|
|
58
|
+
parts.append(
|
|
59
|
+
{
|
|
60
|
+
"type": "tool_call_response",
|
|
61
|
+
"id": getattr(block, "tool_use_id", ""),
|
|
62
|
+
"response": _truncate(getattr(block, "text", "") or ""),
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
else:
|
|
66
|
+
parts.append({"type": btype})
|
|
67
|
+
else:
|
|
68
|
+
parts = [{"type": "text", "content": _truncate(str(content))}]
|
|
69
|
+
result.append({"role": role, "parts": parts})
|
|
70
|
+
return _json_dumps(result)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def serialize_output_messages(text: str, finish_reason: str) -> str:
|
|
74
|
+
"""Serialize assistant output to gen_ai.output.messages JSON string.
|
|
75
|
+
|
|
76
|
+
OTel semconv: [{role, parts: [{type, content}], finish_reason}]
|
|
77
|
+
"""
|
|
78
|
+
return _json_dumps(
|
|
79
|
+
[
|
|
80
|
+
{
|
|
81
|
+
"role": "assistant",
|
|
82
|
+
"parts": [{"type": "text", "content": _truncate(text)}],
|
|
83
|
+
"finish_reason": finish_reason,
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def serialize_system_instructions(system: str) -> str:
|
|
90
|
+
"""Serialize system prompt to gen_ai.system_instructions JSON string."""
|
|
91
|
+
return _json_dumps([{"type": "text", "content": _truncate(system)}])
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def serialize_tool_definitions(tools: list | None) -> str:
|
|
95
|
+
"""Serialize ToolDefinition list to gen_ai.tool.definitions JSON string."""
|
|
96
|
+
if not tools:
|
|
97
|
+
return "[]"
|
|
98
|
+
result = []
|
|
99
|
+
for td in tools:
|
|
100
|
+
result.append(
|
|
101
|
+
{
|
|
102
|
+
"name": getattr(td, "name", ""),
|
|
103
|
+
"type": "function",
|
|
104
|
+
"description": _truncate(getattr(td, "description", "") or ""),
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
return _json_dumps(result)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def serialize_tool_arguments(arguments: dict | Any) -> str:
|
|
111
|
+
"""Serialize tool call arguments to JSON string."""
|
|
112
|
+
if isinstance(arguments, str):
|
|
113
|
+
return _truncate(arguments)
|
|
114
|
+
return _truncate(_json_dumps(arguments))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def serialize_tool_result(result: Any) -> str:
|
|
118
|
+
"""Serialize tool call result to JSON string (truncated)."""
|
|
119
|
+
if isinstance(result, str):
|
|
120
|
+
return _truncate(result)
|
|
121
|
+
content = getattr(result, "content", None)
|
|
122
|
+
if content is not None:
|
|
123
|
+
return _truncate(str(content))
|
|
124
|
+
return _truncate(_json_dumps(result))
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""EventEmitter — OTel Logs signal emitter."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from loguru import logger
|
|
8
|
+
from opentelemetry._logs import LogRecord, SeverityNumber
|
|
9
|
+
|
|
10
|
+
from iac_code.services.telemetry.attributes import AttributeBuilder
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class EventEmitter:
|
|
14
|
+
"""Wraps an OTel Logger. Call `attach()` after SDK initialization."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, attributes: AttributeBuilder) -> None:
|
|
17
|
+
self._attributes = attributes
|
|
18
|
+
self._otel_logger: Any = None
|
|
19
|
+
|
|
20
|
+
def attach(self, otel_logger: Any) -> None:
|
|
21
|
+
"""Install the OTel Logger (or a mock during tests)."""
|
|
22
|
+
self._otel_logger = otel_logger
|
|
23
|
+
|
|
24
|
+
def emit(self, event_name: str, metadata: dict[str, Any]) -> None:
|
|
25
|
+
"""Build a LogRecord and emit via the attached logger.
|
|
26
|
+
|
|
27
|
+
No-op when no logger is attached (e.g., before bootstrap or in tests
|
|
28
|
+
that don't need emission).
|
|
29
|
+
"""
|
|
30
|
+
if self._otel_logger is None:
|
|
31
|
+
return
|
|
32
|
+
attrs: dict[str, Any] = {}
|
|
33
|
+
attrs.update(self._attributes.build_resource())
|
|
34
|
+
attrs.update(self._attributes.build_event(event_name))
|
|
35
|
+
attrs.update({k: v for k, v in metadata.items() if v is not None})
|
|
36
|
+
logger.debug("[event:export] {} {}", event_name, attrs)
|
|
37
|
+
record = LogRecord(
|
|
38
|
+
body=event_name,
|
|
39
|
+
severity_number=SeverityNumber.INFO,
|
|
40
|
+
attributes=attrs,
|
|
41
|
+
)
|
|
42
|
+
self._otel_logger.emit(record)
|