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.
Files changed (184) hide show
  1. iac_code/__init__.py +2 -0
  2. iac_code/acp/__init__.py +97 -0
  3. iac_code/acp/convert.py +423 -0
  4. iac_code/acp/http_sse.py +448 -0
  5. iac_code/acp/mcp.py +54 -0
  6. iac_code/acp/metrics.py +71 -0
  7. iac_code/acp/server.py +662 -0
  8. iac_code/acp/session.py +446 -0
  9. iac_code/acp/slash_registry.py +125 -0
  10. iac_code/acp/state.py +99 -0
  11. iac_code/acp/tools.py +112 -0
  12. iac_code/acp/types.py +13 -0
  13. iac_code/acp/version.py +26 -0
  14. iac_code/agent/__init__.py +19 -0
  15. iac_code/agent/agent_loop.py +640 -0
  16. iac_code/agent/agent_tool.py +269 -0
  17. iac_code/agent/agent_types.py +87 -0
  18. iac_code/agent/message.py +153 -0
  19. iac_code/agent/system_prompt.py +313 -0
  20. iac_code/cli/__init__.py +3 -0
  21. iac_code/cli/headless.py +114 -0
  22. iac_code/cli/main.py +246 -0
  23. iac_code/cli/output_formats.py +125 -0
  24. iac_code/commands/__init__.py +93 -0
  25. iac_code/commands/auth.py +1055 -0
  26. iac_code/commands/clear.py +34 -0
  27. iac_code/commands/compact.py +43 -0
  28. iac_code/commands/debug.py +45 -0
  29. iac_code/commands/effort.py +116 -0
  30. iac_code/commands/exit.py +10 -0
  31. iac_code/commands/help.py +49 -0
  32. iac_code/commands/model.py +130 -0
  33. iac_code/commands/registry.py +245 -0
  34. iac_code/commands/resume.py +49 -0
  35. iac_code/commands/tasks.py +41 -0
  36. iac_code/config.py +304 -0
  37. iac_code/i18n/__init__.py +141 -0
  38. iac_code/i18n/locales/zh/LC_MESSAGES/messages.po +1355 -0
  39. iac_code/memory/__init__.py +1 -0
  40. iac_code/memory/memory_manager.py +92 -0
  41. iac_code/memory/memory_tools.py +88 -0
  42. iac_code/providers/__init__.py +1 -0
  43. iac_code/providers/anthropic_provider.py +284 -0
  44. iac_code/providers/base.py +128 -0
  45. iac_code/providers/dashscope_provider.py +47 -0
  46. iac_code/providers/deepseek_provider.py +36 -0
  47. iac_code/providers/manager.py +399 -0
  48. iac_code/providers/openai_provider.py +344 -0
  49. iac_code/providers/retry.py +58 -0
  50. iac_code/providers/stream_watchdog.py +47 -0
  51. iac_code/providers/thinking.py +164 -0
  52. iac_code/services/__init__.py +1 -0
  53. iac_code/services/agent_factory.py +127 -0
  54. iac_code/services/cloud_credentials.py +22 -0
  55. iac_code/services/context_manager.py +221 -0
  56. iac_code/services/providers/__init__.py +1 -0
  57. iac_code/services/providers/aliyun.py +232 -0
  58. iac_code/services/session_index.py +281 -0
  59. iac_code/services/session_storage.py +245 -0
  60. iac_code/services/telemetry/__init__.py +66 -0
  61. iac_code/services/telemetry/attributes.py +84 -0
  62. iac_code/services/telemetry/client.py +330 -0
  63. iac_code/services/telemetry/config.py +76 -0
  64. iac_code/services/telemetry/constants.py +75 -0
  65. iac_code/services/telemetry/content_serializer.py +124 -0
  66. iac_code/services/telemetry/events.py +42 -0
  67. iac_code/services/telemetry/fallback.py +59 -0
  68. iac_code/services/telemetry/identity.py +73 -0
  69. iac_code/services/telemetry/metrics.py +62 -0
  70. iac_code/services/telemetry/names.py +199 -0
  71. iac_code/services/telemetry/sanitize.py +88 -0
  72. iac_code/services/telemetry/sink.py +67 -0
  73. iac_code/services/telemetry/tracing.py +38 -0
  74. iac_code/services/telemetry/types.py +13 -0
  75. iac_code/services/token_budget.py +54 -0
  76. iac_code/services/token_counter.py +76 -0
  77. iac_code/skills/__init__.py +1 -0
  78. iac_code/skills/bundled/__init__.py +94 -0
  79. iac_code/skills/bundled/iac_aliyun/SKILL.md +192 -0
  80. iac_code/skills/bundled/iac_aliyun/__init__.py +16 -0
  81. iac_code/skills/bundled/iac_aliyun/references/cloud-products/ecs.md +167 -0
  82. iac_code/skills/bundled/iac_aliyun/references/cloud-products/oss.md +69 -0
  83. iac_code/skills/bundled/iac_aliyun/references/cloud-products/rds.md +95 -0
  84. iac_code/skills/bundled/iac_aliyun/references/cloud-products/redis.md +100 -0
  85. iac_code/skills/bundled/iac_aliyun/references/cloud-products/slb.md +60 -0
  86. iac_code/skills/bundled/iac_aliyun/references/cloud-products/vpc.md +54 -0
  87. iac_code/skills/bundled/iac_aliyun/references/ros-template.md +155 -0
  88. iac_code/skills/bundled/iac_aliyun/references/template-parameters.md +206 -0
  89. iac_code/skills/bundled/iac_aliyun/references/terraform-template.md +101 -0
  90. iac_code/skills/bundled/iac_aliyun/scripts/tf2ros.py +77 -0
  91. iac_code/skills/bundled/simplify.py +28 -0
  92. iac_code/skills/discovery.py +136 -0
  93. iac_code/skills/frontmatter.py +119 -0
  94. iac_code/skills/listing.py +92 -0
  95. iac_code/skills/loader.py +42 -0
  96. iac_code/skills/processor.py +81 -0
  97. iac_code/skills/renderer.py +157 -0
  98. iac_code/skills/skill_definition.py +82 -0
  99. iac_code/skills/skill_tool.py +261 -0
  100. iac_code/state/__init__.py +5 -0
  101. iac_code/state/app_state.py +122 -0
  102. iac_code/tasks/__init__.py +1 -0
  103. iac_code/tasks/notification_queue.py +28 -0
  104. iac_code/tasks/task_state.py +66 -0
  105. iac_code/tasks/task_tools.py +114 -0
  106. iac_code/tools/__init__.py +8 -0
  107. iac_code/tools/base.py +226 -0
  108. iac_code/tools/bash.py +133 -0
  109. iac_code/tools/cloud/__init__.py +0 -0
  110. iac_code/tools/cloud/aliyun/__init__.py +0 -0
  111. iac_code/tools/cloud/aliyun/aliyun_api.py +510 -0
  112. iac_code/tools/cloud/aliyun/aliyun_doc_search.py +145 -0
  113. iac_code/tools/cloud/aliyun/endpoints.yml +343 -0
  114. iac_code/tools/cloud/aliyun/ros_client.py +56 -0
  115. iac_code/tools/cloud/aliyun/ros_stack.py +633 -0
  116. iac_code/tools/cloud/aliyun/ros_stack_instances.py +247 -0
  117. iac_code/tools/cloud/base_api.py +162 -0
  118. iac_code/tools/cloud/base_stack.py +242 -0
  119. iac_code/tools/cloud/registry.py +20 -0
  120. iac_code/tools/cloud/types.py +105 -0
  121. iac_code/tools/edit_file.py +121 -0
  122. iac_code/tools/glob.py +103 -0
  123. iac_code/tools/grep.py +254 -0
  124. iac_code/tools/list_files.py +104 -0
  125. iac_code/tools/read_file.py +127 -0
  126. iac_code/tools/result_storage.py +39 -0
  127. iac_code/tools/tool_executor.py +165 -0
  128. iac_code/tools/web_fetch.py +177 -0
  129. iac_code/tools/write_file.py +88 -0
  130. iac_code/types/__init__.py +40 -0
  131. iac_code/types/permissions.py +26 -0
  132. iac_code/types/skill_source.py +11 -0
  133. iac_code/types/stream_events.py +227 -0
  134. iac_code/ui/__init__.py +5 -0
  135. iac_code/ui/banner.py +110 -0
  136. iac_code/ui/components/__init__.py +0 -0
  137. iac_code/ui/components/dialog.py +142 -0
  138. iac_code/ui/components/divider.py +20 -0
  139. iac_code/ui/components/fuzzy_picker.py +308 -0
  140. iac_code/ui/components/progress_bar.py +54 -0
  141. iac_code/ui/components/search_box.py +165 -0
  142. iac_code/ui/components/select.py +319 -0
  143. iac_code/ui/components/status_icon.py +42 -0
  144. iac_code/ui/components/tabs.py +128 -0
  145. iac_code/ui/core/__init__.py +0 -0
  146. iac_code/ui/core/in_place_render.py +129 -0
  147. iac_code/ui/core/input_history.py +118 -0
  148. iac_code/ui/core/key_event.py +41 -0
  149. iac_code/ui/core/prompt_input.py +507 -0
  150. iac_code/ui/core/raw_input.py +302 -0
  151. iac_code/ui/core/screen.py +80 -0
  152. iac_code/ui/dialogs/__init__.py +0 -0
  153. iac_code/ui/dialogs/global_search.py +178 -0
  154. iac_code/ui/dialogs/history_search.py +100 -0
  155. iac_code/ui/dialogs/model_picker.py +280 -0
  156. iac_code/ui/dialogs/quick_open.py +108 -0
  157. iac_code/ui/dialogs/resume_picker.py +749 -0
  158. iac_code/ui/keybindings/__init__.py +0 -0
  159. iac_code/ui/keybindings/manager.py +124 -0
  160. iac_code/ui/renderer.py +1535 -0
  161. iac_code/ui/repl.py +772 -0
  162. iac_code/ui/spinner.py +112 -0
  163. iac_code/ui/suggestions/__init__.py +0 -0
  164. iac_code/ui/suggestions/aggregator.py +171 -0
  165. iac_code/ui/suggestions/command_provider.py +43 -0
  166. iac_code/ui/suggestions/directory_provider.py +95 -0
  167. iac_code/ui/suggestions/file_provider.py +121 -0
  168. iac_code/ui/suggestions/shell_history_provider.py +108 -0
  169. iac_code/ui/suggestions/token_extractor.py +77 -0
  170. iac_code/ui/suggestions/types.py +45 -0
  171. iac_code/ui/transcript_view.py +199 -0
  172. iac_code/utils/__init__.py +0 -0
  173. iac_code/utils/background_housekeeping.py +53 -0
  174. iac_code/utils/cleanup.py +68 -0
  175. iac_code/utils/json_utils.py +60 -0
  176. iac_code/utils/log.py +150 -0
  177. iac_code/utils/project_paths.py +74 -0
  178. iac_code/utils/tool_input_parser.py +62 -0
  179. iac_code-0.1.0.dist-info/LICENSE +201 -0
  180. iac_code-0.1.0.dist-info/METADATA +64 -0
  181. iac_code-0.1.0.dist-info/RECORD +184 -0
  182. iac_code-0.1.0.dist-info/WHEEL +5 -0
  183. iac_code-0.1.0.dist-info/entry_points.txt +2 -0
  184. 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)