Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
00c2091
WIP initial code import
keith-decker Aug 15, 2025
40e6c48
remove references to tool types
keith-decker Aug 15, 2025
43526d6
add a simple unit test
keith-decker Aug 17, 2025
76dbd57
rename exporter to emitter.
keith-decker Aug 18, 2025
567e0a4
rename api file to client
keith-decker Aug 18, 2025
fd41dfb
Merge branch 'main' into util-genai-inference
keith-decker Aug 20, 2025
4bd72aa
WIP gen_ai chat refactor
keith-decker Aug 25, 2025
59414fa
Add provider.name, rename client to handler
keith-decker Aug 25, 2025
5127c39
add message to log functions
keith-decker Aug 25, 2025
66e0c26
remove events, emit structured logs
keith-decker Aug 26, 2025
4346580
update documentation
keith-decker Aug 27, 2025
4be03c4
first round of cleanup for lintcheck
keith-decker Aug 27, 2025
c19e4e6
typecheck fixes
keith-decker Aug 27, 2025
11d937b
typecheck fixes
keith-decker Aug 27, 2025
fdc71a3
linting fixes
keith-decker Aug 27, 2025
04d6e97
linting fixes, refactor for complexity
keith-decker Aug 27, 2025
a2e017a
typecheck fixes
keith-decker Aug 27, 2025
92cd5c8
update documentation
keith-decker Aug 28, 2025
770f878
rename context, update _to_semconv_dict name
keith-decker Sep 2, 2025
78bc6bf
refactor: rename emitters to generators and update method names for c…
keith-decker Sep 2, 2025
b7360f8
refactor: convert API LogRecord to SDK LogRecord, add unit test
keith-decker Sep 2, 2025
86152df
added changelog
keith-decker Sep 3, 2025
c0e03c4
Merge branch 'main' into util-genai-inference
keith-decker Sep 4, 2025
a5b04be
Merge branch 'main' into util-genai-inference
keith-decker Sep 5, 2025
3c94ecd
Wip Convert input messages
keith-decker Sep 5, 2025
a59ca1e
wip, refactor chat generation to output message
keith-decker Sep 5, 2025
a8bb486
cleanup
keith-decker Sep 5, 2025
f64bf69
merge test files
keith-decker Sep 5, 2025
4304819
lint updates
keith-decker Sep 8, 2025
f689ad1
remove logging, lower PR size
keith-decker Sep 8, 2025
b3bb75c
lint update
keith-decker Sep 8, 2025
fb30d45
check env for content capture
keith-decker Sep 8, 2025
fd45c43
check env for content capture on output
keith-decker Sep 8, 2025
aeaa319
lint updates
keith-decker Sep 8, 2025
6a19e04
remove metrics to reduce PR size
keith-decker Sep 8, 2025
ad062e3
remove metrics file
keith-decker Sep 8, 2025
fc0cde0
cleanup docs, remove meter/logger provider
keith-decker Sep 8, 2025
5cea4a6
remove overly complicated env check, add messages to test
keith-decker Sep 8, 2025
8e5c6ab
update file doc strings
keith-decker Sep 8, 2025
16f20b8
lint updates
keith-decker Sep 8, 2025
5ba84ef
remove unused properties
keith-decker Sep 9, 2025
381d38d
move utility function to utils
keith-decker Sep 9, 2025
b983dcd
Merge branch 'main' into util-genai-inference
keith-decker Sep 9, 2025
3a37f43
Additional span attributes
keith-decker Sep 9, 2025
b769ed6
Merge branch 'util-genai-inference' of github.com:zhirafovod/opentele…
keith-decker Sep 9, 2025
0776a7f
update span name
keith-decker Sep 9, 2025
7071d0f
Revert "Additional span attributes"
keith-decker Sep 9, 2025
bb184da
remove unused thread lock
keith-decker Sep 9, 2025
0b18708
update span name
keith-decker Sep 9, 2025
4f8e860
Merge branch 'main' into util-genai-inference
keith-decker Sep 9, 2025
e72a320
Update documentation, rename system to provider
keith-decker Sep 10, 2025
a704fc6
cleanup docs and unclear code
keith-decker Sep 10, 2025
54d753b
Merge branch 'main' into util-genai-inference
keith-decker Sep 11, 2025
0d4f204
add unit test for parent/child relationship
keith-decker Sep 11, 2025
82e4bff
cleanup some duplicated code
keith-decker Sep 11, 2025
d7c6187
cleanup duplicated start span code, move parent check into context ma…
keith-decker Sep 11, 2025
b12f865
remove unneccesary helper function
keith-decker Sep 11, 2025
b61437f
First commit for langchain instrumentation
wrisa Jun 4, 2025
1a11630
removed env
wrisa Jul 14, 2025
4f0e86c
added tool support and modified llm accordingly
wrisa Aug 10, 2025
4e07998
Added evaluation span and event. Added log api for event
wrisa Aug 12, 2025
0847f3b
added deepeval metric measure
wrisa Aug 12, 2025
b86d793
Fixing tests and adding tool call tests for langchain instrumentation
Aug 15, 2025
89388e1
Adding readme for test_langchain_llm.py
Aug 15, 2025
b26952d
WIP initial code import
keith-decker Aug 15, 2025
8578d76
remove references to tool types
keith-decker Aug 15, 2025
765758e
add a simple unit test
keith-decker Aug 17, 2025
3dbe121
rename exporter to emitter.
keith-decker Aug 18, 2025
2743109
rename api file to client
keith-decker Aug 18, 2025
5c009b3
WIP gen_ai chat refactor
keith-decker Aug 25, 2025
2b4d96c
Add provider.name, rename client to handler
keith-decker Aug 25, 2025
5e723c4
add message to log functions
keith-decker Aug 25, 2025
4404c20
remove events, emit structured logs
keith-decker Aug 26, 2025
a9f0f64
update documentation
keith-decker Aug 27, 2025
9eb69bf
first round of cleanup for lintcheck
keith-decker Aug 27, 2025
c92fdec
typecheck fixes
keith-decker Aug 27, 2025
91b18ad
typecheck fixes
keith-decker Aug 27, 2025
09d6f4c
linting fixes
keith-decker Aug 27, 2025
840e9fd
linting fixes, refactor for complexity
keith-decker Aug 27, 2025
70e8d1d
typecheck fixes
keith-decker Aug 27, 2025
a1485d2
update documentation
keith-decker Aug 28, 2025
a734de7
rename context, update _to_semconv_dict name
keith-decker Sep 2, 2025
c5bc930
refactor: rename emitters to generators and update method names for c…
keith-decker Sep 2, 2025
ab9f170
refactor: convert API LogRecord to SDK LogRecord, add unit test
keith-decker Sep 2, 2025
521824f
added changelog
keith-decker Sep 3, 2025
7c017c4
Merge branch 'main' into util-genai-inference
keith-decker Sep 16, 2025
cab5ca0
clean up extra functions
keith-decker Sep 16, 2025
d97fdca
Merge branch 'genai-utils-e2e-dev' into util-genai-inference
zhirafovod Sep 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions instrumentation-genai/opentelemetry-genai-sdk/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
Installation
============

Option 1: pip + requirements.txt
---------------------------------
::

python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

Option 2: Poetry
----------------
::

poetry install

Running Tests
=============

After installing dependencies, simply run:

::

pytest

This will discover and run `tests/test_sdk.py`.
53 changes: 53 additions & 0 deletions instrumentation-genai/opentelemetry-genai-sdk/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "opentelemetry-genai-sdk"
dynamic = ["version"]
description = "OpenTelemetry GenAI SDK"
readme = "README.rst"
license = "Apache-2.0"
requires-python = ">=3.8"
authors = [
{ name = "OpenTelemetry Authors", email = "[email protected]" },
]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
]
dependencies = [
"opentelemetry-api ~= 1.36.0",
"opentelemetry-instrumentation ~= 0.57b0",
"opentelemetry-semantic-conventions ~= 0.57b0",
]

[project.optional-dependencies]
test = [
"pytest>=7.0.0",
]
# evaluation = ["deepevals>=0.1.0", "openlit-sdk>=0.1.0"]

[project.urls]
Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation-genai/opentelemetry-genai-sdk"
Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib"

[tool.hatch.version]
path = "src/opentelemetry/genai/sdk/version.py"

[tool.hatch.build.targets.sdist]
include = [
"/src",
"/tests",
]

[tool.hatch.build.targets.wheel]
packages = ["src/opentelemetry"]
10 changes: 10 additions & 0 deletions instrumentation-genai/opentelemetry-genai-sdk/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# OpenTelemetry SDK
opentelemetry-api>=1.34.0
opentelemetry-sdk>=1.34.0

# Testing
pytest>=7.0.0

# (Optional) evaluation libraries
# deepevals>=0.1.0
# openlit-sdk>=0.1.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import time
from threading import Lock
from typing import List, Optional
from uuid import UUID

from .types import LLMInvocation, ToolInvocation
from .exporters import SpanMetricEventExporter, SpanMetricExporter
from .data import Message, ChatGeneration, Error, ToolOutput, ToolFunction

from opentelemetry.instrumentation.langchain.version import __version__
from opentelemetry.metrics import get_meter
from opentelemetry.trace import get_tracer
from opentelemetry._events import get_event_logger
from opentelemetry._logs import get_logger
from opentelemetry.semconv.schemas import Schemas


class TelemetryClient:
"""
High-level client managing GenAI invocation lifecycles and exporting
them as spans, metrics, and events.
"""
def __init__(self, exporter_type_full: bool = True, **kwargs):
tracer_provider = kwargs.get("tracer_provider")
self._tracer = get_tracer(
__name__, __version__, tracer_provider, schema_url=Schemas.V1_28_0.value
)

meter_provider = kwargs.get("meter_provider")
self._meter = get_meter(
__name__, __version__, meter_provider, schema_url=Schemas.V1_28_0.value
)

event_logger_provider = kwargs.get("event_logger_provider")
self._event_logger = get_event_logger(
__name__, __version__, event_logger_provider=event_logger_provider, schema_url=Schemas.V1_28_0.value
)

logger_provider = kwargs.get("logger_provider")
self._logger = get_logger(
__name__, __version__, logger_provider=logger_provider, schema_url=Schemas.V1_28_0.value
)

self._exporter = (
SpanMetricEventExporter(tracer=self._tracer, meter=self._meter, event_logger=self._event_logger, logger=self._event_logger)
if exporter_type_full
else SpanMetricExporter(tracer=self._tracer, meter=self._meter)
)

self._llm_registry: dict[UUID, LLMInvocation] = {}
self._tool_registry: dict[UUID, ToolInvocation] = {}
self._lock = Lock()

def start_llm(self, prompts: List[Message], tool_functions: List[ToolFunction], run_id: UUID, parent_run_id: Optional[UUID] = None, **attributes):
invocation = LLMInvocation(messages=prompts , tool_functions=tool_functions, run_id=run_id, parent_run_id=parent_run_id, attributes=attributes)
with self._lock:
self._llm_registry[invocation.run_id] = invocation
self._exporter.init_llm(invocation)

def stop_llm(self, run_id: UUID, chat_generations: List[ChatGeneration], **attributes) -> LLMInvocation:
with self._lock:
invocation = self._llm_registry.pop(run_id)
invocation.end_time = time.time()
invocation.chat_generations = chat_generations
invocation.attributes.update(attributes)
self._exporter.export_llm(invocation)
return invocation

def fail_llm(self, run_id: UUID, error: Error, **attributes) -> LLMInvocation:
with self._lock:
invocation = self._llm_registry.pop(run_id)
invocation.end_time = time.time()
invocation.attributes.update(**attributes)
self._exporter.error_llm(error, invocation)
return invocation

def start_tool(self, input_str: str, run_id: UUID, parent_run_id: Optional[UUID] = None, **attributes):
invocation = ToolInvocation(input_str=input_str , run_id=run_id, parent_run_id=parent_run_id, attributes=attributes)
with self._lock:
self._tool_registry[invocation.run_id] = invocation
self._exporter.init_tool(invocation)

def stop_tool(self, run_id: UUID, output: ToolOutput, **attributes) -> ToolInvocation:
with self._lock:
invocation = self._tool_registry.pop(run_id)
invocation.end_time = time.time()
invocation.output = output
self._exporter.export_tool(invocation)
return invocation

def fail_tool(self, run_id: UUID, error: Error, **attributes) -> ToolInvocation:
with self._lock:
invocation = self._tool_registry.pop(run_id)
invocation.end_time = time.time()
invocation.attributes.update(**attributes)
self._exporter.error_tool(error, invocation)
return invocation

# Singleton accessor
_default_client: TelemetryClient | None = None

def get_telemetry_client(exporter_type_full: bool = True, **kwargs) -> TelemetryClient:
global _default_client
if _default_client is None:
_default_client = TelemetryClient(exporter_type_full=exporter_type_full, **kwargs)
return _default_client

# Module‐level convenience functions
def llm_start(prompts: List[Message], run_id: UUID, parent_run_id: Optional[UUID] = None, **attributes):
return get_telemetry_client().start_llm(prompts=prompts, run_id=run_id, parent_run_id=parent_run_id, **attributes)

def llm_stop(run_id: UUID, chat_generations: List[ChatGeneration], **attributes) -> LLMInvocation:
return get_telemetry_client().stop_llm(run_id=run_id, chat_generations=chat_generations, **attributes)

def llm_fail(run_id: UUID, error: Error, **attributes) -> LLMInvocation:
return get_telemetry_client().fail_llm(run_id=run_id, error=error, **attributes)
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from dataclasses import dataclass, field
from typing import List


@dataclass
class ToolOutput:
tool_call_id: str
content: str

@dataclass
class ToolFunction:
name: str
description: str
parameters: str

@dataclass
class ToolFunctionCall:
id: str
name: str
arguments: str
type: str

@dataclass
class Message:
content: str
type: str
name: str
tool_call_id: str
tool_function_calls: List[ToolFunctionCall] = field(default_factory=list)

@dataclass
class ChatGeneration:
content: str
type: str
finish_reason: str = None
tool_function_calls: List[ToolFunctionCall] = field(default_factory=list)

@dataclass
class Error:
message: str
type: type[BaseException]
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from deepeval.models import DeepEvalBaseLLM
from deepeval.test_case import LLMTestCase
from deepeval.metrics import AnswerRelevancyMetric


def evaluate_answer_relevancy_metric(prompt:str, output:str, retrieval_context:list) -> AnswerRelevancyMetric:
test_case = LLMTestCase(input=prompt,
actual_output=output,
retrieval_context=retrieval_context,)
relevancy_metric = AnswerRelevancyMetric(threshold=0.5)
relevancy_metric.measure(test_case)
print(relevancy_metric.score, relevancy_metric.reason)
return relevancy_metric
Loading