cloudbase-agent-observability 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.
@@ -0,0 +1,336 @@
1
+ """
2
+ Attribute creation utilities for Cloudbase Agent observability.
3
+
4
+ Maps observation attributes to OpenInference and OpenTelemetry semantic conventions.
5
+ """
6
+
7
+ import json
8
+ import logging
9
+ from typing import Any, Dict, Optional
10
+
11
+ from opentelemetry import trace
12
+ from opentelemetry.util.types import AttributeValue
13
+ from openinference.semconv.trace import SpanAttributes, OpenInferenceSpanKindValues
14
+
15
+ from cloudbase_agent.observability.types import (
16
+ BaseSpanAttributes,
17
+ LLMAttributes,
18
+ ObservationType,
19
+ ObservationAttributes,
20
+ TraceAttributes,
21
+ )
22
+ from cloudbase_agent.observability.constants import (
23
+ OtelSpanAttributes,
24
+ OpenInferenceSpanKind,
25
+ )
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ def _serialize(value: Any) -> Optional[str]:
30
+ """Safely serialize a value to a JSON string.
31
+
32
+ Args:
33
+ value: The value to serialize.
34
+
35
+ Returns:
36
+ JSON string representation, or None if value is None/empty.
37
+ """
38
+ if value is None:
39
+ return None
40
+ if isinstance(value, str):
41
+ return value
42
+ try:
43
+ return json.dumps(value, default=str)
44
+ except Exception:
45
+ return "<failed to serialize>"
46
+
47
+
48
+ def _flatten_metadata(
49
+ metadata: Optional[Dict[str, Any]], prefix: str
50
+ ) -> Dict[str, str]:
51
+ """Flatten and serialize metadata into OpenTelemetry attribute format.
52
+
53
+ Converts nested metadata objects into dot-notation attribute keys.
54
+ For example, `{ "database": { "host": "localhost" } }` becomes
55
+ `{ "metadata.database.host": "localhost" }`.
56
+
57
+ Args:
58
+ metadata: Metadata object to flatten.
59
+ prefix: Attribute prefix (e.g., 'metadata' or 'agkit.observation.metadata').
60
+
61
+ Returns:
62
+ Flattened metadata attributes with string values.
63
+ """
64
+ metadata_attributes: Dict[str, str] = {}
65
+
66
+ if metadata is None:
67
+ return metadata_attributes
68
+
69
+ if not isinstance(metadata, dict):
70
+ serialized = _serialize(metadata)
71
+ if serialized:
72
+ metadata_attributes[prefix] = serialized
73
+ return metadata_attributes
74
+
75
+ for key, value in metadata.items():
76
+ serialized = value if isinstance(value, str) else _serialize(value)
77
+ if serialized:
78
+ metadata_attributes[f"{prefix}.{key}"] = serialized
79
+
80
+ return metadata_attributes
81
+
82
+
83
+ def create_trace_attributes(attributes: TraceAttributes) -> Dict[str, AttributeValue]:
84
+ """Create OpenTelemetry trace attributes from TraceAttributes.
85
+
86
+ Args:
87
+ attributes: Trace attributes to convert.
88
+
89
+ Returns:
90
+ OpenTelemetry-compatible trace attributes.
91
+ """
92
+ result = {
93
+ OtelSpanAttributes.TRACE_NAME: attributes.name,
94
+ OtelSpanAttributes.USER_ID: attributes.user_id,
95
+ OtelSpanAttributes.SESSION_ID: attributes.session_id,
96
+ OtelSpanAttributes.VERSION: attributes.version,
97
+ OtelSpanAttributes.RELEASE: attributes.release,
98
+ OtelSpanAttributes.TRACE_INPUT: _serialize(attributes.input),
99
+ OtelSpanAttributes.TRACE_OUTPUT: _serialize(attributes.output),
100
+ OtelSpanAttributes.TRACE_TAGS: (
101
+ ",".join(attributes.tags) if attributes.tags else None
102
+ ),
103
+ OtelSpanAttributes.ENVIRONMENT: attributes.environment,
104
+ OtelSpanAttributes.TRACE_PUBLIC: attributes.public,
105
+ }
106
+
107
+ # Add flattened metadata
108
+ metadata_attrs = _flatten_and_serialize_metadata(
109
+ attributes.metadata, OtelSpanAttributes.TRACE_METADATA.value
110
+ )
111
+ result.update(metadata_attrs)
112
+
113
+ # Filter out None values
114
+ return {k: v for k, v in result.items() if v is not None}
115
+
116
+
117
+ def create_observation_attributes(
118
+ observation_type: ObservationType, attributes: ObservationAttributes
119
+ ) -> Dict[str, AttributeValue]:
120
+ """Create OpenTelemetry span attributes from observation attributes.
121
+
122
+ Maps observation attributes to OpenInference semantic conventions:
123
+ - Uses `openinference.span.kind` for span type
124
+ - Uses `llm.*` for LLM-specific attributes
125
+ - Uses `tool.*` for tool-specific attributes
126
+ - Falls back to `agkit.observation.*` for non-standard attributes
127
+
128
+ Args:
129
+ observation_type: The type of observation (llm, tool, chain, etc.)
130
+ attributes: Observation attributes to convert.
131
+
132
+ Returns:
133
+ OpenTelemetry-compatible span attributes.
134
+ """
135
+ # Base attributes for all observation types
136
+ # Handle both dataclass instances and dict input
137
+ level = None
138
+ status_message = None
139
+ version = None
140
+
141
+ if isinstance(attributes, dict):
142
+ level = attributes.get("level")
143
+ status_message = attributes.get("status_message")
144
+ version = attributes.get("version")
145
+ input_value = attributes.get("input")
146
+ output_value = attributes.get("output")
147
+ metadata = attributes.get("metadata")
148
+ else:
149
+ level = attributes.level
150
+ status_message = attributes.status_message
151
+ version = attributes.version
152
+ input_value = attributes.input
153
+ output_value = attributes.output
154
+ metadata = attributes.metadata
155
+
156
+ # Get OpenInference span kind with fallback to UNKNOWN
157
+ try:
158
+ span_kind = OpenInferenceSpanKind[observation_type.upper()].value
159
+ except KeyError:
160
+ logger.warning(
161
+ f"Unknown observation type '{observation_type}', using UNKNOWN span kind"
162
+ )
163
+ span_kind = OpenInferenceSpanKindValues.UNKNOWN.value
164
+
165
+ otel_attributes: Dict[str, AttributeValue] = {
166
+ SpanAttributes.OPENINFERENCE_SPAN_KIND: span_kind,
167
+ OtelSpanAttributes.OBSERVATION_TYPE.value: observation_type,
168
+ OtelSpanAttributes.OBSERVATION_LEVEL.value: (
169
+ level.value if level and hasattr(level, "value") else level
170
+ ),
171
+ OtelSpanAttributes.OBSERVATION_STATUS_MESSAGE.value: status_message,
172
+ OtelSpanAttributes.VERSION.value: version,
173
+ # Use OpenInference input.value convention
174
+ SpanAttributes.INPUT_VALUE: _serialize(input_value),
175
+ # Also set legacy observation.input for compatibility
176
+ OtelSpanAttributes.OBSERVATION_INPUT.value: _serialize(input_value),
177
+ # Use OpenInference output.value convention
178
+ SpanAttributes.OUTPUT_VALUE: _serialize(output_value),
179
+ # Also set legacy observation.output for compatibility
180
+ OtelSpanAttributes.OBSERVATION_OUTPUT.value: _serialize(output_value),
181
+ }
182
+
183
+ # LLM-specific attributes
184
+ if observation_type == ObservationType.LLM:
185
+ # Handle both dict and dataclass for LLM attributes
186
+ model = None
187
+ model_parameters = None
188
+ usage_details = None
189
+ completion_start_time = None
190
+
191
+ if isinstance(attributes, dict):
192
+ model = attributes.get("model")
193
+ model_parameters = attributes.get("model_parameters")
194
+ usage_details = attributes.get("usage_details")
195
+ completion_start_time = attributes.get("completion_start_time")
196
+ elif hasattr(attributes, "model"):
197
+ model = attributes.model
198
+ model_parameters = attributes.model_parameters
199
+ usage_details = attributes.usage_details
200
+ completion_start_time = attributes.completion_start_time
201
+
202
+ if model:
203
+ otel_attributes[
204
+ SpanAttributes.LLM_MODEL_NAME
205
+ ] = model
206
+ if model_parameters:
207
+ otel_attributes[
208
+ SpanAttributes.LLM_INVOCATION_PARAMETERS
209
+ ] = _serialize(model_parameters)
210
+ # Also set legacy llm.model_parameters for compatibility
211
+ otel_attributes[
212
+ OtelSpanAttributes.LLM_MODEL_PARAMETERS.value
213
+ ] = _serialize(model_parameters)
214
+ if usage_details:
215
+ usage = usage_details
216
+ if isinstance(usage, dict):
217
+ if usage.get("input") is not None:
218
+ otel_attributes[
219
+ SpanAttributes.LLM_TOKEN_COUNT_PROMPT
220
+ ] = usage["input"]
221
+ if usage.get("output") is not None:
222
+ otel_attributes[
223
+ SpanAttributes.LLM_TOKEN_COUNT_COMPLETION
224
+ ] = usage["output"]
225
+ if usage.get("total") is not None:
226
+ otel_attributes[
227
+ SpanAttributes.LLM_TOKEN_COUNT_TOTAL
228
+ ] = usage["total"]
229
+ # Also set legacy llm.usage_details for compatibility
230
+ otel_attributes[
231
+ OtelSpanAttributes.LLM_USAGE_DETAILS.value
232
+ ] = _serialize(usage_details)
233
+ if completion_start_time:
234
+ otel_attributes[
235
+ OtelSpanAttributes.LLM_COMPLETION_START_TIME.value
236
+ ] = _serialize(completion_start_time)
237
+
238
+ # Embedding-specific attributes
239
+ if observation_type == ObservationType.EMBEDDING:
240
+ # Handle both dict and dataclass for embedding attributes
241
+ model = None
242
+ model_parameters = None
243
+
244
+ if isinstance(attributes, dict):
245
+ model = attributes.get("model")
246
+ model_parameters = attributes.get("model_parameters")
247
+ elif hasattr(attributes, "model"):
248
+ model = attributes.model
249
+ model_parameters = attributes.model_parameters
250
+
251
+ if model:
252
+ otel_attributes[
253
+ SpanAttributes.EMBEDDING_MODEL_NAME
254
+ ] = model
255
+ if model_parameters:
256
+ otel_attributes[
257
+ SpanAttributes.LLM_INVOCATION_PARAMETERS
258
+ ] = _serialize(model_parameters)
259
+
260
+ # Tool-specific attributes
261
+ if observation_type == ObservationType.TOOL:
262
+ tool_name = None
263
+ if isinstance(attributes, dict):
264
+ tool_name = attributes.get("tool_name")
265
+ elif hasattr(attributes, "tool_name"):
266
+ tool_name = attributes.tool_name
267
+ if tool_name:
268
+ otel_attributes[SpanAttributes.TOOL_NAME] = tool_name
269
+
270
+ # Add metadata (use OpenInference metadata convention)
271
+ metadata_attrs = _flatten_and_serialize_metadata(
272
+ metadata, SpanAttributes.METADATA
273
+ )
274
+ otel_attributes.update(metadata_attrs)
275
+
276
+ # Also add observation.metadata for compatibility
277
+ obsetvability_metadata_attrs = _flatten_and_serialize_metadata(
278
+ metadata, OtelSpanAttributes.OBSERVATION_METADATA.value
279
+ )
280
+ otel_attributes.update(obsetvability_metadata_attrs)
281
+
282
+ # Filter out None values
283
+ return {k: v for k, v in otel_attributes.items() if v is not None}
284
+
285
+
286
+ def _flatten_and_serialize_metadata(
287
+ metadata: Optional[Dict[str, Any]], prefix: str
288
+ ) -> Dict[str, str]:
289
+ """Flatten and serialize metadata into OpenTelemetry attribute format.
290
+
291
+ This is an alias for _flatten_metadata for backward compatibility.
292
+ """
293
+ return _flatten_metadata(metadata, prefix)
294
+
295
+
296
+ def update_active_trace(attributes: TraceAttributes) -> None:
297
+ """Update the currently active trace with new attributes.
298
+
299
+ Args:
300
+ attributes: Trace attributes to set.
301
+ """
302
+ current_span = trace.get_current_span()
303
+ if not current_span or not current_span.is_recording():
304
+ logger.debug(
305
+ "No active OTEL span in context. Skipping trace update."
306
+ )
307
+ return
308
+
309
+ trace_attrs = create_trace_attributes(attributes)
310
+ current_span.set_attributes(trace_attrs)
311
+
312
+
313
+ def get_active_trace_id() -> Optional[str]:
314
+ """Get the current active trace ID.
315
+
316
+ Returns:
317
+ The trace ID as a hex string, or undefined if no active span.
318
+ """
319
+ current_span = trace.get_current_span()
320
+ if not current_span or not current_span.is_recording():
321
+ return None
322
+ span_context = current_span.get_span_context()
323
+ return format(span_context.trace_id, "032x")
324
+
325
+
326
+ def get_active_span_id() -> Optional[str]:
327
+ """Get the current active observation ID.
328
+
329
+ Returns:
330
+ The span ID as a hex string, or undefined if no active span.
331
+ """
332
+ current_span = trace.get_current_span()
333
+ if not current_span or not current_span.is_recording():
334
+ return None
335
+ span_context = current_span.get_span_context()
336
+ return format(span_context.span_id, "016x")
@@ -0,0 +1,78 @@
1
+ """
2
+ OTEL attribute constants for Cloudbase Agent observability.
3
+
4
+ Uses OpenInference semantic conventions where applicable:
5
+ https://github.com/Arize-ai/openinference/tree/main/spec
6
+
7
+ Falls back to Cloudbase Agent specific attributes where OpenInference doesn't define a standard.
8
+ """
9
+
10
+ from enum import Enum
11
+ from openinference.semconv.trace import SpanAttributes, OpenInferenceSpanKindValues
12
+
13
+
14
+ # Re-export OpenInference SpanAttributes for standard attributes
15
+ # Use this for all OpenInference standard conventions (input.value, llm.model_name, etc.)
16
+ OpenInferenceAttributes = SpanAttributes
17
+
18
+
19
+ # Re-export OpenInference span kind values
20
+ class OpenInferenceSpanKind(str, Enum):
21
+ """OpenInference span kind values."""
22
+
23
+ AGENT = OpenInferenceSpanKindValues.AGENT.value
24
+ CHAIN = OpenInferenceSpanKindValues.CHAIN.value
25
+ LLM = OpenInferenceSpanKindValues.LLM.value
26
+ TOOL = OpenInferenceSpanKindValues.TOOL.value
27
+ RETRIEVER = OpenInferenceSpanKindValues.RETRIEVER.value
28
+ EMBEDDING = OpenInferenceSpanKindValues.EMBEDDING.value
29
+ RERANKER = OpenInferenceSpanKindValues.RERANKER.value
30
+ EVALUATOR = OpenInferenceSpanKindValues.EVALUATOR.value
31
+ GUARDRAIL = OpenInferenceSpanKindValues.GUARDRAIL.value
32
+
33
+
34
+ # SDK information
35
+ OBSERVABILITY_TRACER_NAME = "agkit-tracer"
36
+ OBSERVABILITY_SDK_NAME = "agkit-observability"
37
+
38
+
39
+ class OtelSpanAttributes(str, Enum):
40
+ """
41
+ Cloudbase Agent specific (non-standard) OTEL attributes.
42
+
43
+ For standard OpenInference attributes (input.value, llm.model_name, etc.),
44
+ use OpenInferenceAttributes (SpanAttributes) instead.
45
+ """
46
+
47
+ # Cloudbase Agent Trace attributes (non-standard)
48
+ TRACE_NAME = "trace.name"
49
+ TRACE_TAGS = "trace.tags"
50
+ TRACE_PUBLIC = "trace.public"
51
+ TRACE_METADATA = "trace.metadata"
52
+ TRACE_INPUT = "trace.input"
53
+ TRACE_OUTPUT = "trace.output"
54
+
55
+ # Cloudbase Agent Observation attributes (non-standard)
56
+ OBSERVATION_TYPE = "observation.type"
57
+ OBSERVATION_LEVEL = "observation.level"
58
+ OBSERVATION_STATUS_MESSAGE = "observation.status_message"
59
+ OBSERVATION_INPUT = "observation.input"
60
+ OBSERVATION_OUTPUT = "observation.output"
61
+ OBSERVATION_METADATA = "observation.metadata"
62
+
63
+ # Cloudbase Agent LLM-specific (non-standard)
64
+ LLM_COMPLETION_START_TIME = "llm.completion_start_time"
65
+ LLM_MODEL_PARAMETERS = "llm.model_parameters"
66
+ LLM_USAGE_DETAILS = "llm.usage_details"
67
+ LLM_COST_DETAILS = "llm.cost_details"
68
+
69
+ # Cloudbase Agent Retriever-specific (non-standard)
70
+ RETRIEVER_NAME = "retriever.name"
71
+ RETRIEVER_QUERY = "retriever.query"
72
+ RETRIEVER_INDEX_ID = "retriever.index_id"
73
+ RETRIEVER_TOP_K = "retriever.top_k"
74
+
75
+ # Cloudbase Agent General (non-standard)
76
+ ENVIRONMENT = "environment"
77
+ RELEASE = "release"
78
+ VERSION = "version"
@@ -0,0 +1,10 @@
1
+ """
2
+ Coze integration for Cloudbase Agent observability.
3
+
4
+ This module provides a CozeEventHandler that automatically traces Coze
5
+ bot workflows using OpenTelemetry with OpenInference semantic conventions.
6
+ """
7
+
8
+ from cloudbase_agent.observability.coze.event_handler import CozeEventHandler
9
+
10
+ __all__ = ["CozeEventHandler"]