Skip to content

Commit 1316fae

Browse files
committed
feat(core): move to OTEL backbone
1 parent 5f598cc commit 1316fae

11 files changed

Lines changed: 1188 additions & 477 deletions

File tree

langfuse/otel/__init__.py

Lines changed: 504 additions & 0 deletions
Large diffs are not rendered by default.

langfuse/otel/_logger.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import logging
2+
3+
logger = logging.getLogger("langfuse")
4+
5+
# handle httpx logging
6+
httpx_logger = logging.getLogger("httpx")
7+
httpx_logger.setLevel(logging.WARNING) # Set the desired log level
8+
9+
console_handler = logging.StreamHandler()
10+
httpx_logger.addHandler(console_handler)

langfuse/otel/_utils.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import json
2+
3+
from opentelemetry import trace as otel_trace_api
4+
from opentelemetry.sdk import util
5+
from opentelemetry.sdk.trace import ReadableSpan
6+
7+
8+
def span_formatter(span: ReadableSpan):
9+
parent_id = None
10+
if span.parent is not None:
11+
parent_id = f"0x{otel_trace_api.format_span_id(span.parent.span_id)}"
12+
13+
start_time = None
14+
if span._start_time:
15+
start_time = util.ns_to_iso_str(span._start_time)
16+
17+
end_time = None
18+
if span._end_time:
19+
end_time = util.ns_to_iso_str(span._end_time)
20+
21+
status = {
22+
"status_code": str(span._status.status_code.name),
23+
}
24+
if span._status.description:
25+
status["description"] = span._status.description
26+
27+
return (
28+
json.dumps(
29+
{
30+
"name": span._name,
31+
"context": (
32+
span._format_context(span._context) if span._context else None
33+
),
34+
"kind": str(span.kind),
35+
"parent_id": parent_id,
36+
"start_time": start_time,
37+
"end_time": end_time,
38+
"status": status,
39+
"attributes": span._format_attributes(span._attributes),
40+
"events": span._format_events(span._events),
41+
"links": span._format_links(span._links),
42+
"resource": json.loads(span.resource.to_json()),
43+
"instrumentationScope": json.loads(
44+
span._instrumentation_scope.to_json()
45+
if span._instrumentation_scope
46+
else "{}"
47+
),
48+
},
49+
indent=2,
50+
)
51+
+ "\n"
52+
)

langfuse/otel/attributes.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import json
2+
from datetime import datetime
3+
from typing import Any, Dict, List, Optional
4+
5+
from langfuse.model import PromptClient
6+
7+
from ..serializer import EventSerializer
8+
from ..types import MapValue, SpanLevel
9+
10+
11+
class LangfuseSpanAttributes:
12+
# Langfuse-Trace attributes
13+
TRACE_NAME = "langfuse.trace.name"
14+
TRACE_ID = "langfuse.trace.id"
15+
TRACE_USER_ID = "user.id"
16+
TRACE_SESSION_ID = "session.id"
17+
TRACE_TAGS = "langfuse.trace.tags"
18+
TRACE_PUBLIC = "langfuse.trace.public"
19+
TRACE_METADATA = "langfuse.trace.metadata"
20+
TRACE_INPUT = "langfuse.trace.input"
21+
TRACE_OUTPUT = "langfuse.trace.output"
22+
23+
# Langfuse-observation attributes
24+
OBSERVATION_NAME = "langfuse.observation.name"
25+
OBSERVATION_TYPE = "langfuse.observation.type"
26+
OBSERVATION_METADATA = "langfuse.observation.metadata"
27+
OBSERVATION_LEVEL = "langfuse.observation.level"
28+
OBSERVATION_STATUS_MESSAGE = "langfuse.observation.status_message"
29+
OBSERVATION_INPUT = "langfuse.observation.input"
30+
OBSERVATION_OUTPUT = "langfuse.observation.output"
31+
32+
# Langfuse-observation of type Generation attributes
33+
OBSERVATION_COMPLETION_START_TIME = "langfuse.observation.completion_start_time"
34+
OBSERVATION_MODEL = "gen_ai.response.model"
35+
OBSERVATION_MODEL_PARAMETERS = "langfuse.observation.model_parameters"
36+
OBSERVATION_USAGE_DETAILS = "gen_ai.usage"
37+
OBSERVATION_COST_DETAILS = "langfuse.observation.cost_details"
38+
OBSERVATION_PROMPT_NAME = "langfuse.observation.prompt.name"
39+
OBSERVATION_PROMPT_VERSION = "langfuse.observation.prompt.version"
40+
41+
# General
42+
ENVIRONMENT = "langfuse.environment"
43+
RELEASE = "langfuse.release"
44+
VERSION = "langfuse.version"
45+
46+
47+
def create_trace_attributes(
48+
*,
49+
name: Optional[str] = None,
50+
user_id: Optional[str] = None,
51+
session_id: Optional[str] = None,
52+
version: Optional[str] = None,
53+
release: Optional[str] = None,
54+
input: Optional[Any] = None,
55+
output: Optional[Any] = None,
56+
metadata: Optional[Any] = None,
57+
tags: Optional[List[str]] = None,
58+
public: Optional[bool] = None,
59+
):
60+
attributes = {
61+
LangfuseSpanAttributes.TRACE_NAME: name,
62+
LangfuseSpanAttributes.TRACE_USER_ID: user_id,
63+
LangfuseSpanAttributes.TRACE_SESSION_ID: session_id,
64+
LangfuseSpanAttributes.VERSION: version,
65+
LangfuseSpanAttributes.RELEASE: release,
66+
LangfuseSpanAttributes.TRACE_INPUT: _serialize(input),
67+
LangfuseSpanAttributes.TRACE_OUTPUT: _serialize(output),
68+
LangfuseSpanAttributes.TRACE_METADATA: _serialize(metadata),
69+
LangfuseSpanAttributes.TRACE_TAGS: tags,
70+
LangfuseSpanAttributes.TRACE_PUBLIC: public,
71+
}
72+
73+
return {k: v for k, v in attributes.items() if v is not None}
74+
75+
76+
def create_span_attributes(
77+
*,
78+
metadata: Optional[Any] = None,
79+
input: Optional[Any] = None,
80+
output: Optional[Any] = None,
81+
level: Optional[SpanLevel] = None,
82+
status_message: Optional[str] = None,
83+
version: Optional[str] = None,
84+
):
85+
attributes = {
86+
LangfuseSpanAttributes.OBSERVATION_TYPE: "span",
87+
LangfuseSpanAttributes.OBSERVATION_LEVEL: level,
88+
LangfuseSpanAttributes.OBSERVATION_STATUS_MESSAGE: status_message,
89+
LangfuseSpanAttributes.VERSION: version,
90+
LangfuseSpanAttributes.OBSERVATION_INPUT: _serialize(input),
91+
LangfuseSpanAttributes.OBSERVATION_OUTPUT: _serialize(output),
92+
LangfuseSpanAttributes.OBSERVATION_METADATA: _serialize(metadata),
93+
}
94+
95+
return {k: v for k, v in attributes.items() if v is not None}
96+
97+
98+
def create_generation_attributes(
99+
*,
100+
name: Optional[str] = None,
101+
completion_start_time: Optional[datetime] = None,
102+
metadata: Optional[Any] = None,
103+
level: Optional[SpanLevel] = None,
104+
status_message: Optional[str] = None,
105+
version: Optional[str] = None,
106+
model: Optional[str] = None,
107+
model_parameters: Optional[Dict[str, MapValue]] = None,
108+
input: Optional[Any] = None,
109+
output: Optional[Any] = None,
110+
usage_details: Optional[Dict[str, int]] = None,
111+
cost_details: Optional[Dict[str, float]] = None,
112+
prompt: Optional[PromptClient] = None,
113+
):
114+
attributes = {
115+
LangfuseSpanAttributes.OBSERVATION_TYPE: "generation",
116+
LangfuseSpanAttributes.OBSERVATION_NAME: name,
117+
LangfuseSpanAttributes.OBSERVATION_LEVEL: level,
118+
LangfuseSpanAttributes.OBSERVATION_STATUS_MESSAGE: status_message,
119+
LangfuseSpanAttributes.VERSION: version,
120+
LangfuseSpanAttributes.OBSERVATION_INPUT: _serialize(input),
121+
LangfuseSpanAttributes.OBSERVATION_OUTPUT: _serialize(output),
122+
LangfuseSpanAttributes.OBSERVATION_METADATA: _serialize(metadata),
123+
LangfuseSpanAttributes.OBSERVATION_MODEL: model,
124+
LangfuseSpanAttributes.OBSERVATION_PROMPT_NAME: prompt and prompt.name,
125+
LangfuseSpanAttributes.OBSERVATION_PROMPT_VERSION: prompt and prompt.version,
126+
LangfuseSpanAttributes.OBSERVATION_USAGE_DETAILS: _serialize(usage_details),
127+
LangfuseSpanAttributes.OBSERVATION_COST_DETAILS: _serialize(cost_details),
128+
LangfuseSpanAttributes.OBSERVATION_COMPLETION_START_TIME: _serialize(
129+
completion_start_time
130+
),
131+
LangfuseSpanAttributes.OBSERVATION_MODEL_PARAMETERS: _serialize(
132+
model_parameters
133+
),
134+
}
135+
136+
return {k: v for k, v in attributes.items() if v is not None}
137+
138+
139+
def _serialize(obj):
140+
return json.dumps(obj, cls=EventSerializer) if obj is not None else None

langfuse/otel/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
LANGFUSE_TRACER_NAME = "langfuse-sdk"

langfuse/otel/dx.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Desired DX
2+
3+
## Option 1: Langfuse classic
4+
5+
```python
6+
from langfuse import langfuse
7+
8+
langfuse = Langfuse()
9+
10+
trace = langfuse.trace(id='trace-123', name='my_trace', tags=['tag1', 'tag2'], metadata={"key1": "val"})
11+
span = langfuse.span(id='span-123', name='my-span')
12+
generation = langfuse.generation(input='hello', output="what's up", usage_details={"input_tokens": 1, "output_tokens": 2})
13+
14+
generation.end()
15+
span.end()
16+
trace.end()
17+
```
18+
19+
## Option 2: OTEL native, i.e. use spans throughout
20+
21+
```python
22+
from langfuse import langfuse
23+
24+
langfuse = Langfuse()
25+
26+
root_span = langfuse.span(trace={"id": "new-trace-id", name: "new-trace-name"}, id="root-span-id")
27+
regular_span = langfuse.span(id='span-123', name='my-span')
28+
generation = langfuse.span(as_type="generation", input='hello', output="what's up", usage_details={"input_tokens": 1, "output_tokens": 2})
29+
30+
generation.end()
31+
span.end()
32+
trace.end()
33+
```
34+
35+
## Late updates
36+
37+
```python
38+
trace_1 = langfuse.trace()
39+
trace_2 = langfuse.trace()
40+
41+
trace_1.span()
42+
trace_2.span()
43+
44+
trace_1
45+
```
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
LANGFUSE_TRACING_ENVIRONMENT = "LANGFUSE_TRACING_ENVIRONMENT"
2+
"""
3+
.. envvar:: LANGFUSE_TRACING_ENVIRONMENT
4+
5+
The tracing environment. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'.
6+
7+
**Default value:** ``"default"``
8+
"""
9+
10+
LANGFUSE_RELEASE = "LANGFUSE_RELEASE"
11+
"""
12+
.. envvar:: LANGFUSE_TRACING_ENVIRONMENT
13+
14+
Release number/hash of the application to provide analytics grouped by release.
15+
"""
16+
17+
18+
LANGFUSE_PUBLIC_KEY = "LANGFUSE_PUBLIC_KEY"
19+
"""
20+
.. envvar:: LANGFUSE_PUBLIC_KEY
21+
22+
Public API key of Langfuse project
23+
"""
24+
25+
LANGFUSE_SECRET_KEY = "LANGFUSE_SECRET_KEY"
26+
"""
27+
.. envvar:: LANGFUSE_SECRET_KEY
28+
29+
Secret API key of Langfuse project
30+
"""
31+
32+
LANGFUSE_HOST = "LANGFUSE_HOST"
33+
"""
34+
.. envvar:: LANGFUSE_HOST
35+
36+
Host of Langfuse API. Can be set via `LANGFUSE_HOST` environment variable. Defaults to `https://cloud.langfuse.com`.
37+
"""
38+
39+
LANGFUSE_DEBUG = "LANGFUSE_DEBUG"
40+
"""
41+
.. envvar:: LANGFUSE_DEBUG
42+
43+
Enables debug mode for more verbose logging.
44+
"""
45+
46+
LANGFUSE_TRACING_ENABLED = "LANGFUSE_TRACING_ENABLED"
47+
"""
48+
.. envvar:: LANGFUSE_TRACING_ENABLED
49+
50+
Enables or disables the Langfuse client. If disabled, all observability calls to the backend will be no-ops. Default is True. Set to `False` to disable tracing.
51+
"""

0 commit comments

Comments
 (0)