splunk-otel-util-genai 0.1.3__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 (31) hide show
  1. opentelemetry/util/genai/__init__.py +17 -0
  2. opentelemetry/util/genai/_fsspec_upload/__init__.py +39 -0
  3. opentelemetry/util/genai/_fsspec_upload/fsspec_hook.py +184 -0
  4. opentelemetry/util/genai/attributes.py +60 -0
  5. opentelemetry/util/genai/callbacks.py +24 -0
  6. opentelemetry/util/genai/config.py +184 -0
  7. opentelemetry/util/genai/debug.py +183 -0
  8. opentelemetry/util/genai/emitters/__init__.py +25 -0
  9. opentelemetry/util/genai/emitters/composite.py +186 -0
  10. opentelemetry/util/genai/emitters/configuration.py +324 -0
  11. opentelemetry/util/genai/emitters/content_events.py +153 -0
  12. opentelemetry/util/genai/emitters/evaluation.py +519 -0
  13. opentelemetry/util/genai/emitters/metrics.py +308 -0
  14. opentelemetry/util/genai/emitters/span.py +774 -0
  15. opentelemetry/util/genai/emitters/spec.py +48 -0
  16. opentelemetry/util/genai/emitters/utils.py +961 -0
  17. opentelemetry/util/genai/environment_variables.py +200 -0
  18. opentelemetry/util/genai/handler.py +1002 -0
  19. opentelemetry/util/genai/instruments.py +44 -0
  20. opentelemetry/util/genai/interfaces.py +58 -0
  21. opentelemetry/util/genai/plugins.py +114 -0
  22. opentelemetry/util/genai/span_context.py +80 -0
  23. opentelemetry/util/genai/types.py +440 -0
  24. opentelemetry/util/genai/upload_hook.py +119 -0
  25. opentelemetry/util/genai/utils.py +182 -0
  26. opentelemetry/util/genai/version.py +15 -0
  27. splunk_otel_util_genai-0.1.3.dist-info/METADATA +70 -0
  28. splunk_otel_util_genai-0.1.3.dist-info/RECORD +31 -0
  29. splunk_otel_util_genai-0.1.3.dist-info/WHEEL +4 -0
  30. splunk_otel_util_genai-0.1.3.dist-info/entry_points.txt +5 -0
  31. splunk_otel_util_genai-0.1.3.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,25 @@
1
+ """Emitter package consolidating all telemetry signal emitters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pkgutil import extend_path
6
+
7
+ __path__ = extend_path(__path__, __name__)
8
+
9
+ from .composite import CompositeEmitter # noqa: F401
10
+ from .content_events import ContentEventsEmitter # noqa: F401
11
+ from .evaluation import ( # noqa: F401
12
+ EvaluationEventsEmitter,
13
+ EvaluationMetricsEmitter,
14
+ )
15
+ from .metrics import MetricsEmitter # noqa: F401
16
+ from .span import SpanEmitter # noqa: F401
17
+
18
+ __all__ = [
19
+ "SpanEmitter",
20
+ "MetricsEmitter",
21
+ "ContentEventsEmitter",
22
+ "CompositeEmitter",
23
+ "EvaluationMetricsEmitter",
24
+ "EvaluationEventsEmitter",
25
+ ]
@@ -0,0 +1,186 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from typing import Any, Iterable, Iterator, Mapping, Sequence
5
+
6
+ from ..debug import genai_debug_log
7
+ from ..interfaces import EmitterMeta, EmitterProtocol
8
+ from ..types import Error, EvaluationResult, GenAI
9
+
10
+ _LOGGER = logging.getLogger(__name__)
11
+
12
+ _CATEGORY_START_ORDER: Sequence[str] = ("span", "metrics", "content_events")
13
+ _CATEGORY_END_ORDER: Sequence[str] = (
14
+ "evaluation",
15
+ "metrics",
16
+ "content_events",
17
+ "span",
18
+ )
19
+ _EVALUATION_CATEGORY = "evaluation"
20
+
21
+
22
+ class CompositeEmitter(EmitterMeta):
23
+ """Category-aware orchestrator for GenAI emitters.
24
+
25
+ Emitters are grouped by category to allow targeted replacement/augmentation while
26
+ preserving ordering guarantees:
27
+
28
+ * ``span`` emitters run first on ``on_start`` and last on ``on_end``/``on_error``
29
+ * ``metrics`` emitters run before content emitters at the end of an invocation
30
+ * ``content_events`` emitters observe invocations after metrics but before the
31
+ final span closure
32
+ * ``evaluation`` emitters observe ``on_evaluation_results`` and receive ``on_end``/``on_error`` for flush-style behaviour
33
+ """
34
+
35
+ role = "composite"
36
+ name = "composite"
37
+
38
+ def __init__(
39
+ self,
40
+ *,
41
+ span_emitters: Iterable[EmitterProtocol] | None = None,
42
+ metrics_emitters: Iterable[EmitterProtocol] | None = None,
43
+ content_event_emitters: Iterable[EmitterProtocol] | None = None,
44
+ evaluation_emitters: Iterable[EmitterProtocol] | None = None,
45
+ ) -> None:
46
+ self._categories: dict[str, list[EmitterProtocol]] = {
47
+ "span": list(span_emitters or []),
48
+ "metrics": list(metrics_emitters or []),
49
+ "content_events": list(content_event_emitters or []),
50
+ _EVALUATION_CATEGORY: list(evaluation_emitters or []),
51
+ }
52
+
53
+ # ------------------------------------------------------------------
54
+ # Public API used by the handler lifecycle
55
+
56
+ def on_start(self, obj: Any) -> None: # type: ignore[override]
57
+ self._dispatch(_CATEGORY_START_ORDER, "on_start", obj=obj)
58
+
59
+ def on_end(self, obj: Any) -> None: # type: ignore[override]
60
+ self._dispatch(_CATEGORY_END_ORDER, "on_end", obj=obj)
61
+
62
+ def on_error(self, error: Error, obj: Any) -> None: # type: ignore[override]
63
+ self._dispatch(_CATEGORY_END_ORDER, "on_error", obj=obj, error=error)
64
+
65
+ def on_evaluation_results(
66
+ self,
67
+ results: Sequence[EvaluationResult],
68
+ obj: Any | None = None,
69
+ ) -> None: # type: ignore[override]
70
+ if not results:
71
+ genai_debug_log("emitter.on_evaluation_results.empty", obj)
72
+ return
73
+ self._dispatch(
74
+ (_EVALUATION_CATEGORY,),
75
+ "on_evaluation_results",
76
+ obj=obj,
77
+ results=results,
78
+ )
79
+
80
+ # ------------------------------------------------------------------
81
+ # Introspection helpers used during configuration refresh
82
+
83
+ def iter_emitters(
84
+ self, categories: Sequence[str] | None = None
85
+ ) -> Iterator[EmitterProtocol]:
86
+ names = categories or (
87
+ "span",
88
+ "metrics",
89
+ "content_events",
90
+ _EVALUATION_CATEGORY,
91
+ )
92
+ for name in names:
93
+ for emitter in self._categories.get(name, []):
94
+ yield emitter
95
+
96
+ def emitters_for(self, category: str) -> Sequence[EmitterProtocol]:
97
+ return self._categories.get(category, [])
98
+
99
+ def categories(self) -> Mapping[str, Sequence[EmitterProtocol]]:
100
+ return self._categories
101
+
102
+ def add_emitter(self, category: str, emitter: EmitterProtocol) -> None:
103
+ self._categories.setdefault(category, []).append(emitter)
104
+
105
+ # ------------------------------------------------------------------
106
+ # Internal helpers
107
+
108
+ def _dispatch(
109
+ self,
110
+ categories: Sequence[str],
111
+ method_name: str,
112
+ *,
113
+ obj: Any | None = None,
114
+ error: Error | None = None,
115
+ results: Sequence[EvaluationResult] | None = None,
116
+ ) -> None:
117
+ try:
118
+ genai_debug_log(
119
+ "composite.dispatch.begin",
120
+ obj if isinstance(obj, GenAI) else None,
121
+ method=method_name,
122
+ categories=list(categories),
123
+ result_count=len(results or ()),
124
+ )
125
+ except Exception: # pragma: no cover - defensive
126
+ pass
127
+ for category in categories:
128
+ emitters = self._categories.get(category)
129
+ if not emitters:
130
+ continue
131
+ for emitter in list(emitters):
132
+ handler = getattr(emitter, method_name, None)
133
+ if handler is None:
134
+ continue
135
+ if method_name == "on_evaluation_results":
136
+ args = (results or (), obj)
137
+ target = obj
138
+ elif method_name == "on_error":
139
+ args = (error, obj)
140
+ target = obj
141
+ else:
142
+ args = (obj,)
143
+ target = obj
144
+ try:
145
+ handles = getattr(emitter, "handles", None)
146
+ if handles is not None and target is not None:
147
+ if not handles(target):
148
+ try:
149
+ genai_debug_log(
150
+ "composite.dispatch.skip",
151
+ target
152
+ if isinstance(target, GenAI)
153
+ else None,
154
+ method=method_name,
155
+ category=category,
156
+ emitter=getattr(
157
+ emitter, "name", repr(emitter)
158
+ ),
159
+ )
160
+ except Exception: # pragma: no cover
161
+ pass
162
+ continue
163
+ genai_debug_log(
164
+ "composite.dispatch.emit",
165
+ target if isinstance(target, GenAI) else None,
166
+ method=method_name,
167
+ category=category,
168
+ emitter=getattr(emitter, "name", repr(emitter)),
169
+ )
170
+ handler(*args)
171
+ except Exception: # pragma: no cover - defensive
172
+ _LOGGER.debug(
173
+ "Emitter %s failed during %s for category %s",
174
+ getattr(emitter, "name", repr(emitter)),
175
+ method_name,
176
+ category,
177
+ exc_info=True,
178
+ )
179
+ try:
180
+ genai_debug_log(
181
+ "composite.dispatch.end",
182
+ obj if isinstance(obj, GenAI) else None,
183
+ method=method_name,
184
+ )
185
+ except Exception: # pragma: no cover - defensive
186
+ pass
@@ -0,0 +1,324 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from dataclasses import dataclass
5
+ from types import MethodType
6
+ from typing import Any, Dict, Iterable, List, Sequence
7
+
8
+ from ..config import Settings
9
+ from ..interfaces import EmitterProtocol
10
+ from ..plugins import load_emitter_specs
11
+ from ..types import ContentCapturingMode
12
+ from .composite import CompositeEmitter
13
+ from .content_events import ContentEventsEmitter
14
+ from .evaluation import EvaluationEventsEmitter, EvaluationMetricsEmitter
15
+ from .metrics import MetricsEmitter
16
+ from .span import SpanEmitter
17
+ from .spec import CategoryOverride, EmitterFactoryContext, EmitterSpec
18
+
19
+ _logger = logging.getLogger(__name__)
20
+
21
+ _CATEGORY_SPAN = "span"
22
+ _CATEGORY_METRICS = "metrics"
23
+ _CATEGORY_CONTENT = "content_events"
24
+ _CATEGORY_EVALUATION = "evaluation"
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class CaptureControl:
29
+ span_allowed: bool
30
+ span_initial: bool
31
+ events_initial: bool
32
+ mode: ContentCapturingMode
33
+
34
+
35
+ def build_emitter_pipeline(
36
+ *,
37
+ tracer: Any,
38
+ meter: Any,
39
+ event_logger: Any,
40
+ content_logger: Any,
41
+ evaluation_histogram: Any,
42
+ settings: Settings,
43
+ ) -> tuple[CompositeEmitter, CaptureControl]:
44
+ """Construct the CompositeEmitter and capture control metadata."""
45
+
46
+ span_allowed = (
47
+ settings.capture_messages_override
48
+ or settings.legacy_capture_request
49
+ or not settings.enable_content_events
50
+ )
51
+ span_initial = span_allowed and settings.capture_messages_mode in (
52
+ ContentCapturingMode.SPAN_ONLY,
53
+ ContentCapturingMode.SPAN_AND_EVENT,
54
+ )
55
+ events_initial = settings.enable_content_events and (
56
+ settings.capture_messages_mode
57
+ in (
58
+ ContentCapturingMode.EVENT_ONLY,
59
+ ContentCapturingMode.SPAN_AND_EVENT,
60
+ )
61
+ )
62
+
63
+ context = EmitterFactoryContext(
64
+ tracer=tracer,
65
+ meter=meter,
66
+ event_logger=event_logger,
67
+ content_logger=content_logger,
68
+ evaluation_histogram=evaluation_histogram,
69
+ capture_span_content=span_initial,
70
+ capture_event_content=events_initial,
71
+ )
72
+
73
+ category_specs: Dict[str, List[EmitterSpec]] = {
74
+ _CATEGORY_SPAN: [],
75
+ _CATEGORY_METRICS: [],
76
+ _CATEGORY_CONTENT: [],
77
+ _CATEGORY_EVALUATION: [],
78
+ }
79
+ spec_registry: Dict[str, EmitterSpec] = {}
80
+
81
+ def _register(spec: EmitterSpec) -> None:
82
+ target = category_specs.setdefault(spec.category, [])
83
+ mode = getattr(spec, "mode", "append")
84
+ if mode == "replace-category":
85
+ target.clear()
86
+ target.append(spec)
87
+ elif mode == "prepend":
88
+ target.insert(0, spec)
89
+ elif mode == "replace-same-name":
90
+ replaced = False
91
+ for idx, existing in enumerate(target):
92
+ if existing.name == spec.name:
93
+ target[idx] = spec
94
+ replaced = True
95
+ break
96
+ if not replaced:
97
+ target.append(spec)
98
+ else:
99
+ target.append(spec)
100
+ spec_registry[spec.name] = spec
101
+
102
+ if settings.enable_span and not settings.only_traceloop_compat:
103
+ _register(
104
+ EmitterSpec(
105
+ name="SemanticConvSpan",
106
+ category=_CATEGORY_SPAN,
107
+ factory=lambda ctx: SpanEmitter(
108
+ tracer=ctx.tracer,
109
+ capture_content=ctx.capture_span_content,
110
+ ),
111
+ )
112
+ )
113
+ if settings.enable_metrics:
114
+ _register(
115
+ EmitterSpec(
116
+ name="SemanticConvMetrics",
117
+ category=_CATEGORY_METRICS,
118
+ factory=lambda ctx: MetricsEmitter(meter=ctx.meter),
119
+ )
120
+ )
121
+ if settings.enable_content_events:
122
+ _register(
123
+ EmitterSpec(
124
+ name="ContentEvents",
125
+ category=_CATEGORY_CONTENT,
126
+ factory=lambda ctx: ContentEventsEmitter(
127
+ logger=ctx.content_logger,
128
+ capture_content=ctx.capture_event_content,
129
+ ),
130
+ )
131
+ )
132
+
133
+ # Evaluation emitters are always present
134
+ _register(
135
+ EmitterSpec(
136
+ name="EvaluationMetrics",
137
+ category=_CATEGORY_EVALUATION,
138
+ factory=lambda ctx: EvaluationMetricsEmitter(
139
+ ctx.evaluation_histogram # now a callable returning histogram per metric
140
+ ),
141
+ )
142
+ )
143
+ _register(
144
+ EmitterSpec(
145
+ name="EvaluationEvents",
146
+ category=_CATEGORY_EVALUATION,
147
+ factory=lambda ctx: EvaluationEventsEmitter(
148
+ ctx.content_logger,
149
+ emit_legacy_event=settings.emit_legacy_evaluation_event,
150
+ ),
151
+ )
152
+ )
153
+
154
+ for spec in load_emitter_specs(settings.extra_emitters):
155
+ if spec.category not in {
156
+ _CATEGORY_SPAN,
157
+ _CATEGORY_METRICS,
158
+ _CATEGORY_CONTENT,
159
+ _CATEGORY_EVALUATION,
160
+ }:
161
+ _logger.warning(
162
+ "Emitter spec %s targets unknown category '%s'",
163
+ spec.name,
164
+ spec.category,
165
+ )
166
+ continue
167
+ _register(spec)
168
+
169
+ _apply_category_overrides(
170
+ category_specs, spec_registry, settings.category_overrides
171
+ )
172
+
173
+ span_emitters = _instantiate_category(
174
+ category_specs.get(_CATEGORY_SPAN, ()), context
175
+ )
176
+ metrics_emitters = _instantiate_category(
177
+ category_specs.get(_CATEGORY_METRICS, ()), context
178
+ )
179
+ content_emitters = _instantiate_category(
180
+ category_specs.get(_CATEGORY_CONTENT, ()), context
181
+ )
182
+ evaluation_emitters = _instantiate_category(
183
+ category_specs.get(_CATEGORY_EVALUATION, ()), context
184
+ )
185
+
186
+ composite = CompositeEmitter(
187
+ span_emitters=span_emitters,
188
+ metrics_emitters=metrics_emitters,
189
+ content_event_emitters=content_emitters,
190
+ evaluation_emitters=evaluation_emitters,
191
+ )
192
+ control = CaptureControl(
193
+ span_allowed=span_allowed,
194
+ span_initial=span_initial,
195
+ events_initial=events_initial,
196
+ mode=settings.capture_messages_mode,
197
+ )
198
+ return composite, control
199
+
200
+
201
+ def _instantiate_category(
202
+ specs: Iterable[EmitterSpec], context: EmitterFactoryContext
203
+ ) -> List[EmitterProtocol]:
204
+ instances: List[EmitterProtocol] = []
205
+ for spec in specs:
206
+ try:
207
+ emitter = spec.factory(context)
208
+ if spec.invocation_types:
209
+ allowed = {name for name in spec.invocation_types}
210
+ original = getattr(emitter, "handles", None)
211
+ orig_func = getattr(original, "__func__", None)
212
+
213
+ def _filtered_handles(
214
+ self, obj, _allowed=allowed, _orig=orig_func
215
+ ):
216
+ if obj is None:
217
+ if _orig is not None:
218
+ return _orig(self, obj)
219
+ return True
220
+ if type(obj).__name__ not in _allowed:
221
+ return False
222
+ if _orig is not None:
223
+ return _orig(self, obj)
224
+ return True
225
+
226
+ setattr(
227
+ emitter,
228
+ "handles",
229
+ MethodType(_filtered_handles, emitter),
230
+ )
231
+ instances.append(emitter)
232
+ except Exception: # pragma: no cover - defensive
233
+ _logger.exception("Failed to instantiate emitter %s", spec.name)
234
+ return instances
235
+
236
+
237
+ def _apply_category_overrides(
238
+ category_specs: Dict[str, List[EmitterSpec]],
239
+ spec_registry: Dict[str, EmitterSpec],
240
+ overrides: Dict[str, CategoryOverride],
241
+ ) -> None:
242
+ for category, override in overrides.items():
243
+ current = category_specs.setdefault(category, [])
244
+ if override.mode == "replace-category":
245
+ replacement: List[EmitterSpec] = []
246
+ for name in override.emitter_names:
247
+ spec = spec_registry.get(name)
248
+ if spec is None:
249
+ _logger.warning(
250
+ "Emitter '%s' referenced in %s override is not registered",
251
+ name,
252
+ category,
253
+ )
254
+ continue
255
+ replacement.append(spec)
256
+ if not replacement:
257
+ _logger.warning(
258
+ "replace-category override for '%s' resolved to empty set; retaining existing emitters (fallback)",
259
+ category,
260
+ )
261
+ else:
262
+ # Auto-augment evaluation if user attempted to replace with only SplunkEvaluationResults
263
+ if (
264
+ category == _CATEGORY_EVALUATION
265
+ and len(replacement) == 1
266
+ and replacement[0].name == "SplunkEvaluationResults"
267
+ ):
268
+ builtin_metrics = spec_registry.get("EvaluationMetrics")
269
+ if builtin_metrics and builtin_metrics not in replacement:
270
+ replacement.insert(0, builtin_metrics)
271
+ category_specs[category] = replacement
272
+ continue
273
+ if override.mode == "prepend":
274
+ additions = _resolve_specs(
275
+ override.emitter_names, spec_registry, category
276
+ )
277
+ category_specs[category] = additions + current
278
+ continue
279
+ if override.mode == "replace-same-name":
280
+ for name in override.emitter_names:
281
+ spec = spec_registry.get(name)
282
+ if spec is None:
283
+ _logger.warning(
284
+ "Emitter '%s' referenced in %s override is not registered",
285
+ name,
286
+ category,
287
+ )
288
+ continue
289
+ replaced = False
290
+ for idx, existing in enumerate(current):
291
+ if existing.name == name:
292
+ current[idx] = spec
293
+ replaced = True
294
+ break
295
+ if not replaced:
296
+ current.append(spec)
297
+ continue
298
+ # append (default)
299
+ additions = _resolve_specs(
300
+ override.emitter_names, spec_registry, category
301
+ )
302
+ current.extend(additions)
303
+
304
+
305
+ def _resolve_specs(
306
+ names: Sequence[str],
307
+ spec_registry: Dict[str, EmitterSpec],
308
+ category: str,
309
+ ) -> List[EmitterSpec]:
310
+ resolved: List[EmitterSpec] = []
311
+ for name in names:
312
+ spec = spec_registry.get(name)
313
+ if spec is None:
314
+ _logger.warning(
315
+ "Emitter '%s' referenced in %s override is not registered",
316
+ name,
317
+ category,
318
+ )
319
+ continue
320
+ resolved.append(spec)
321
+ return resolved
322
+
323
+
324
+ __all__ = ["CaptureControl", "build_emitter_pipeline"]
@@ -0,0 +1,153 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from typing import Any, Optional
5
+
6
+ from opentelemetry._logs import Logger, get_logger
7
+
8
+ from ..interfaces import EmitterMeta
9
+ from ..types import (
10
+ AgentCreation,
11
+ AgentInvocation,
12
+ EmbeddingInvocation,
13
+ Error,
14
+ LLMInvocation,
15
+ Step,
16
+ Workflow,
17
+ )
18
+ from .utils import (
19
+ _agent_to_log_record,
20
+ _embedding_to_log_record,
21
+ _llm_invocation_to_log_record,
22
+ _step_to_log_record,
23
+ _workflow_to_log_record,
24
+ )
25
+
26
+
27
+ class ContentEventsEmitter(EmitterMeta):
28
+ """Emits input/output content as events (log records) instead of span attributes.
29
+
30
+ Supported: LLMInvocation only.
31
+
32
+ Exclusions:
33
+ * EmbeddingInvocation – embeddings are vector lookups; content events intentionally omitted to reduce noise & cost.
34
+ * ToolCall – tool calls typically reference external functions/APIs; their arguments are already span attributes and
35
+ are not duplicated as content events (future structured tool audit events may be added separately).
36
+
37
+ This explicit exclusion avoids surprising cardinality growth and keeps event volume proportional to user/chat messages.
38
+ """
39
+
40
+ role = "content_event"
41
+ name = "semconv_content_events"
42
+
43
+ def __init__(
44
+ self, logger: Optional[Logger] = None, capture_content: bool = False
45
+ ):
46
+ self._logger: Logger = logger or get_logger(__name__)
47
+ self._capture_content = capture_content
48
+ self._py_logger = logging.getLogger(f"{__name__}.ContentEventsEmitter")
49
+ if self._py_logger.isEnabledFor(logging.DEBUG):
50
+ self._py_logger.debug(
51
+ "Initialized ContentEventsEmitter capture_content=%s logger=%s",
52
+ capture_content,
53
+ type(self._logger).__name__,
54
+ )
55
+
56
+ def on_start(self, obj: Any) -> None:
57
+ # LLM events are emitted in finish() when we have both input and output
58
+ return None
59
+
60
+ def on_end(self, obj: Any) -> None:
61
+ if not self._capture_content:
62
+ if self._py_logger.isEnabledFor(logging.DEBUG):
63
+ self._py_logger.debug(
64
+ "Skipping content emission (capture_content disabled) obj_type=%s",
65
+ type(obj).__name__,
66
+ )
67
+ return
68
+ # Emit workflow event (includes initial input + final output messages)
69
+ if isinstance(obj, Workflow):
70
+ self._emit_workflow_event(obj)
71
+ return
72
+ # Emit agent creation/invocation event (input/output messages where available)
73
+ if isinstance(obj, (AgentCreation, AgentInvocation)):
74
+ self._emit_agent_event(obj)
75
+ return
76
+ # Optional: step and embedding events (currently excluded from request scope)
77
+ # Uncomment if needed later:
78
+ # if isinstance(obj, Step):
79
+ # self._emit_step_event(obj); return
80
+ # if isinstance(obj, EmbeddingInvocation):
81
+ # self._emit_embedding_event(obj); return
82
+ if isinstance(obj, LLMInvocation):
83
+ # Emit a single event for the entire LLM invocation
84
+ try:
85
+ record = _llm_invocation_to_log_record(
86
+ obj,
87
+ self._capture_content,
88
+ )
89
+ if record and self._logger:
90
+ if self._py_logger.isEnabledFor(logging.DEBUG):
91
+ self._py_logger.debug(
92
+ "Emitting LLM content event trace_id=%s span_id=%s",
93
+ getattr(obj, "trace_id", None),
94
+ getattr(obj, "span_id", None),
95
+ )
96
+ self._logger.emit(record)
97
+ elif self._py_logger.isEnabledFor(logging.DEBUG):
98
+ self._py_logger.debug(
99
+ "No log record generated for LLM invocation (capture_content=%s)",
100
+ self._capture_content,
101
+ )
102
+ except (TypeError, ValueError, AttributeError) as e:
103
+ logging.getLogger(__name__).warning(
104
+ "Failed to emit LLM invocation event: %s", e, exc_info=True
105
+ )
106
+
107
+ def on_error(self, error: Error, obj: Any) -> None:
108
+ return None
109
+
110
+ def handles(self, obj: Any) -> bool:
111
+ return isinstance(
112
+ obj,
113
+ (LLMInvocation, Workflow, AgentCreation, AgentInvocation, Step),
114
+ )
115
+
116
+ # Helper methods for new agentic types
117
+ def _emit_workflow_event(self, workflow: Workflow) -> None:
118
+ """Emit an event for a workflow."""
119
+ try:
120
+ record = _workflow_to_log_record(workflow, self._capture_content)
121
+ if record and self._logger:
122
+ self._logger.emit(record)
123
+ except (TypeError, ValueError, AttributeError):
124
+ return None
125
+
126
+ def _emit_agent_event(
127
+ self, agent: AgentCreation | AgentInvocation
128
+ ) -> None:
129
+ """Emit an event for an agent operation."""
130
+ try:
131
+ record = _agent_to_log_record(agent, self._capture_content)
132
+ if record and self._logger:
133
+ self._logger.emit(record)
134
+ except (TypeError, ValueError, AttributeError):
135
+ return None
136
+
137
+ def _emit_step_event(self, step: Step) -> None:
138
+ """Emit an event for a step."""
139
+ try:
140
+ record = _step_to_log_record(step, self._capture_content)
141
+ if record and self._logger:
142
+ self._logger.emit(record)
143
+ except (TypeError, ValueError, AttributeError):
144
+ return None
145
+
146
+ def _emit_embedding_event(self, embedding: EmbeddingInvocation) -> None:
147
+ """Emit an event for an embedding operation."""
148
+ try:
149
+ record = _embedding_to_log_record(embedding, self._capture_content)
150
+ if record and self._logger:
151
+ self._logger.emit(record)
152
+ except (TypeError, ValueError, AttributeError):
153
+ return None