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,44 @@
1
+ # Copyright The OpenTelemetry Authors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from opentelemetry.metrics import Histogram, Meter
16
+
17
+
18
+ class Instruments:
19
+ """
20
+ Manages OpenTelemetry metrics instruments for GenAI telemetry.
21
+ """
22
+
23
+ def __init__(self, meter: Meter):
24
+ self.operation_duration_histogram: Histogram = meter.create_histogram(
25
+ name="gen_ai.client.operation.duration",
26
+ unit="s",
27
+ description="Duration of GenAI client operations",
28
+ )
29
+ self.token_usage_histogram: Histogram = meter.create_histogram(
30
+ name="gen_ai.client.token.usage",
31
+ unit="{token}",
32
+ description="Number of input and output tokens used",
33
+ )
34
+ # Agentic AI metrics
35
+ self.workflow_duration_histogram: Histogram = meter.create_histogram(
36
+ name="gen_ai.workflow.duration",
37
+ unit="s",
38
+ description="Duration of GenAI workflows",
39
+ )
40
+ self.agent_duration_histogram: Histogram = meter.create_histogram(
41
+ name="gen_ai.agent.duration",
42
+ unit="s",
43
+ description="Duration of agent operations",
44
+ )
@@ -0,0 +1,58 @@
1
+ # Phase 1 refactor: introduce lightweight protocol-style interfaces so future
2
+ # composite generator + plugin system can rely on a stable narrow contract.
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Protocol, Sequence, runtime_checkable
6
+
7
+ from .types import Error, EvaluationResult, LLMInvocation
8
+
9
+
10
+ @runtime_checkable
11
+ class EmitterProtocol(Protocol):
12
+ """Protocol implemented by all telemetry emitters.
13
+
14
+ Accepts any GenAI domain object (LLMInvocation, EmbeddingInvocation, etc.).
15
+ Implementations MAY ignore objects of unsupported types.
16
+ """
17
+
18
+ def on_start(self, obj: Any) -> None: # pragma: no cover - structural
19
+ ...
20
+
21
+ def on_end(self, obj: Any) -> None: # pragma: no cover - structural
22
+ ...
23
+
24
+ def on_error(
25
+ self, error: Error, obj: Any
26
+ ) -> None: # pragma: no cover - structural
27
+ ...
28
+
29
+ def on_evaluation_results(
30
+ self, results: Sequence[EvaluationResult], obj: Any | None = None
31
+ ) -> None: # pragma: no cover - structural
32
+ ...
33
+
34
+
35
+ @runtime_checkable
36
+ class EvaluatorProtocol(Protocol):
37
+ """Protocol for evaluator objects (future phases may broaden)."""
38
+
39
+ def evaluate(
40
+ self, invocation: LLMInvocation
41
+ ) -> Any: # pragma: no cover - structural
42
+ ...
43
+
44
+
45
+ class EmitterMeta:
46
+ """Simple metadata mixin for emitters (role/name used by future plugin system)."""
47
+
48
+ role: str = "span" # default / legacy generators are span focused
49
+ name: str = "legacy"
50
+ override: bool = False
51
+
52
+ def handles(self, obj: Any) -> bool: # pragma: no cover (trivial)
53
+ return True
54
+
55
+ def on_evaluation_results(
56
+ self, results: Sequence[EvaluationResult], obj: Any | None = None
57
+ ) -> None: # pragma: no cover - default no-op
58
+ return None
@@ -0,0 +1,114 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from typing import Iterable, Mapping, Sequence
5
+
6
+ from opentelemetry.util._importlib_metadata import (
7
+ entry_points, # pyright: ignore[reportUnknownVariableType]
8
+ )
9
+
10
+ from .emitters.spec import EmitterSpec
11
+
12
+ _logger = logging.getLogger(__name__)
13
+
14
+
15
+ def load_emitter_specs(
16
+ names: Sequence[str] | None = None,
17
+ ) -> list[EmitterSpec]:
18
+ """Load emitter specs declared under the ``opentelemetry_util_genai_emitters`` entry point.
19
+
20
+ Entry points should return an iterable of :class:`EmitterSpec` instances or dictionaries
21
+ matching the ``EmitterSpec`` constructor signature. When ``names`` is provided, only
22
+ entry points whose name matches (case-insensitive) the selection are loaded.
23
+ Legacy group support has been removed; vendor packages must migrate to the new group.
24
+ """
25
+
26
+ selected = {name.lower() for name in names} if names else None
27
+ loaded_specs: list[EmitterSpec] = []
28
+ seen: set[str] = set()
29
+ # Primary (new) group
30
+ for ep in entry_points(group="opentelemetry_util_genai_emitters"):
31
+ ep_name = getattr(ep, "name", "")
32
+ seen.add(ep_name.lower())
33
+ if selected and ep_name.lower() not in selected:
34
+ continue
35
+ try:
36
+ provider = ep.load()
37
+ except Exception: # pragma: no cover - defensive
38
+ _logger.exception("Emitter entry point %s failed to load", ep_name)
39
+ continue
40
+ try:
41
+ loaded_specs.extend(_coerce_to_specs(provider, ep_name))
42
+ except Exception: # pragma: no cover - defensive
43
+ _logger.exception(
44
+ "Emitter entry point %s returned an unsupported value", ep_name
45
+ )
46
+ # Silent legacy fallback (temporary for transition/tests). Only consult if specific names requested
47
+ # or if no specs loaded yet and legacy group is present.
48
+ if (selected and loaded_specs) or (not selected and loaded_specs):
49
+ pass # already satisfied
50
+ else:
51
+ try:
52
+ for ep in entry_points(group="opentelemetry_genai_emitters"):
53
+ ep_name = getattr(ep, "name", "")
54
+ if ep_name.lower() in seen:
55
+ continue
56
+ if selected and ep_name.lower() not in selected:
57
+ continue
58
+ try:
59
+ provider = ep.load()
60
+ except Exception: # pragma: no cover - defensive
61
+ _logger.exception(
62
+ "(legacy group) Emitter entry point %s failed to load",
63
+ ep_name,
64
+ )
65
+ continue
66
+ try:
67
+ loaded_specs.extend(_coerce_to_specs(provider, ep_name))
68
+ except Exception: # pragma: no cover - defensive
69
+ _logger.exception(
70
+ "(legacy group) Emitter entry point %s returned an unsupported value",
71
+ ep_name,
72
+ )
73
+ except Exception: # pragma: no cover - defensive
74
+ _logger.debug("Legacy emitter entry point group not available")
75
+ if selected:
76
+ missing = selected - seen
77
+ for name in missing:
78
+ _logger.debug("Emitter entry point '%s' was not found", name)
79
+ return loaded_specs
80
+
81
+
82
+ def _coerce_to_specs(provider: object, source: str) -> list[EmitterSpec]:
83
+ if provider is None:
84
+ return []
85
+ if callable(provider):
86
+ return _coerce_to_specs(provider(), source)
87
+ if isinstance(provider, EmitterSpec):
88
+ return [provider]
89
+ if isinstance(provider, Mapping):
90
+ return [_mapping_to_spec(provider, source)]
91
+ if isinstance(provider, Iterable):
92
+ specs: list[EmitterSpec] = []
93
+ for item in provider:
94
+ if isinstance(item, EmitterSpec):
95
+ specs.append(item)
96
+ elif isinstance(item, Mapping):
97
+ specs.append(_mapping_to_spec(item, source))
98
+ else:
99
+ raise TypeError(
100
+ f"Unsupported emitter spec element {item!r} from {source}"
101
+ )
102
+ return specs
103
+ raise TypeError(
104
+ f"Unsupported emitter spec provider {provider!r} from {source}"
105
+ )
106
+
107
+
108
+ def _mapping_to_spec(data: Mapping[str, object], source: str) -> EmitterSpec:
109
+ if "factory" not in data:
110
+ raise ValueError(f"Emitter spec from {source} must define a factory")
111
+ return EmitterSpec(**data) # type: ignore[arg-type]
112
+
113
+
114
+ __all__ = ["load_emitter_specs"]
@@ -0,0 +1,80 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Tuple
4
+
5
+ from opentelemetry import trace
6
+ from opentelemetry.trace import NonRecordingSpan, Span, SpanContext
7
+
8
+ from .types import GenAI
9
+
10
+
11
+ def extract_span_context(span: Span | Any | None) -> SpanContext | None:
12
+ """Return a SpanContext from the given span-like object if available."""
13
+
14
+ if span is None:
15
+ return None
16
+
17
+ context = getattr(span, "context", None)
18
+ if context is not None and getattr(context, "trace_id", 0):
19
+ return context
20
+
21
+ get_ctx = getattr(span, "get_span_context", None)
22
+ if callable(get_ctx):
23
+ try:
24
+ context = get_ctx()
25
+ except Exception: # pragma: no cover - defensive
26
+ context = None
27
+ if context is not None and getattr(context, "trace_id", 0):
28
+ return context
29
+ return None
30
+
31
+
32
+ def store_span_context(target: GenAI, context: SpanContext | None) -> None:
33
+ """Persist span context metadata on the GenAI invocation."""
34
+
35
+ if context is None:
36
+ return
37
+ if not getattr(context, "trace_id", 0):
38
+ return
39
+
40
+ target.span_context = context
41
+ target.trace_id = getattr(context, "trace_id", None)
42
+ target.span_id = getattr(context, "span_id", None)
43
+ trace_flags = getattr(context, "trace_flags", None)
44
+ if trace_flags is not None:
45
+ try:
46
+ target.trace_flags = int(trace_flags)
47
+ except Exception: # pragma: no cover - defensive
48
+ target.trace_flags = None
49
+
50
+
51
+ def span_context_hex_ids(
52
+ context: SpanContext | None,
53
+ ) -> Tuple[str | None, str | None]:
54
+ """Return hexadecimal trace/span identifiers."""
55
+
56
+ if context is None or not getattr(context, "trace_id", 0):
57
+ return None, None
58
+ trace_id = f"{context.trace_id:032x}"
59
+ span_id = f"{context.span_id:016x}"
60
+ return trace_id, span_id
61
+
62
+
63
+ def build_otel_context(
64
+ span: Span | None,
65
+ context: SpanContext | None,
66
+ ) -> trace.Context | None:
67
+ """Return an OpenTelemetry Context carrying a span or span_context."""
68
+
69
+ if span is not None:
70
+ try:
71
+ return trace.set_span_in_context(span)
72
+ except Exception: # pragma: no cover - defensive
73
+ pass
74
+ if context is not None:
75
+ try:
76
+ non_recording = NonRecordingSpan(context)
77
+ return trace.set_span_in_context(non_recording)
78
+ except Exception: # pragma: no cover - defensive
79
+ return None
80
+ return None