Skip to content

Commit b7cd032

Browse files
shah-sidddgustavocidornelas
authored andcommitted
feat: Add user_id and session_id in traces to track users and sessions
1 parent b149272 commit b7cd032

File tree

5 files changed

+249
-3
lines changed

5 files changed

+249
-3
lines changed

src/openlayer/lib/__init__.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
__all__ = [
44
"configure",
55
"trace",
6-
"trace_anthropic",
6+
"trace_anthropic",
77
"trace_openai",
88
"trace_openai_assistant_thread_run",
99
"trace_mistral",
@@ -14,11 +14,24 @@
1414
"trace_oci_genai",
1515
"trace_oci", # Alias for backward compatibility
1616
"update_current_trace",
17-
"update_current_step"
17+
"update_current_step",
18+
# User and session context functions
19+
"set_user_session_context",
20+
"update_trace_user_session",
21+
"get_current_user_id",
22+
"get_current_session_id",
23+
"clear_user_session_context",
1824
]
1925

2026
# ---------------------------------- Tracing --------------------------------- #
2127
from .tracing import tracer
28+
from .tracing.context import (
29+
set_user_session_context,
30+
update_trace_user_session,
31+
get_current_user_id,
32+
get_current_session_id,
33+
clear_user_session_context,
34+
)
2235

2336
configure = tracer.configure
2437
trace = tracer.trace
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"""OpenLayer tracing module."""
2+
3+
from .tracer import (
4+
trace,
5+
trace_async,
6+
update_current_trace,
7+
update_current_step,
8+
log_context,
9+
log_output,
10+
configure,
11+
get_current_trace,
12+
get_current_step,
13+
create_step,
14+
)
15+
16+
from .context import (
17+
set_user_session_context,
18+
update_trace_user_session,
19+
get_current_user_id,
20+
get_current_session_id,
21+
clear_user_session_context,
22+
)
23+
24+
__all__ = [
25+
# Core tracing functions
26+
"trace",
27+
"trace_async",
28+
"update_current_trace",
29+
"update_current_step",
30+
"log_context",
31+
"log_output",
32+
"configure",
33+
"get_current_trace",
34+
"get_current_step",
35+
"create_step",
36+
37+
# User and session context functions
38+
"set_user_session_context",
39+
"update_trace_user_session",
40+
"get_current_user_id",
41+
"get_current_session_id",
42+
"clear_user_session_context",
43+
]
44+
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
"""
2+
Streamlined user and session context management for OpenLayer tracing.
3+
4+
This module provides simple functions to set user_id and session_id in middleware
5+
and override them anywhere in your traced code.
6+
"""
7+
8+
import contextvars
9+
import threading
10+
from typing import Optional, Union
11+
12+
# Sentinel object to distinguish between "not provided" and "explicitly None"
13+
_NOT_PROVIDED = object()
14+
15+
# Context variables for user and session tracking
16+
_user_id_context = contextvars.ContextVar("openlayer_user_id", default=None)
17+
_session_id_context = contextvars.ContextVar("openlayer_session_id", default=None)
18+
19+
# Thread-local fallback for environments where contextvars don't work well
20+
_thread_local = threading.local()
21+
22+
23+
class UserSessionContext:
24+
"""Internal class to manage user and session context."""
25+
26+
@staticmethod
27+
def set_user_id(user_id: Union[str, int, None]) -> None:
28+
"""Set the user ID for the current context."""
29+
user_id_str = str(user_id) if user_id is not None else None
30+
_user_id_context.set(user_id_str)
31+
32+
# Thread-local fallback
33+
_thread_local.user_id = user_id_str
34+
35+
@staticmethod
36+
def get_user_id() -> Optional[str]:
37+
"""Get the current user ID."""
38+
try:
39+
return _user_id_context.get(None)
40+
except LookupError:
41+
# Fallback to thread-local
42+
return getattr(_thread_local, 'user_id', None)
43+
44+
@staticmethod
45+
def set_session_id(session_id: Union[str, None]) -> None:
46+
"""Set the session ID for the current context."""
47+
_session_id_context.set(session_id)
48+
49+
# Thread-local fallback
50+
_thread_local.session_id = session_id
51+
52+
@staticmethod
53+
def get_session_id() -> Optional[str]:
54+
"""Get the current session ID."""
55+
try:
56+
return _session_id_context.get(None)
57+
except LookupError:
58+
# Fallback to thread-local
59+
return getattr(_thread_local, 'session_id', None)
60+
61+
@staticmethod
62+
def clear_context() -> None:
63+
"""Clear all user and session context."""
64+
_user_id_context.set(None)
65+
_session_id_context.set(None)
66+
67+
# Clear thread-local
68+
for attr in ['user_id', 'session_id']:
69+
if hasattr(_thread_local, attr):
70+
delattr(_thread_local, attr)
71+
72+
73+
# ----------------------------- Public API Functions ----------------------------- #
74+
75+
def set_user_session_context(
76+
user_id: Union[str, int, None] = None,
77+
session_id: Union[str, None] = None,
78+
) -> None:
79+
"""Set user and session context for tracing (typically called in middleware).
80+
81+
This function should be called once per request in your middleware to establish
82+
default user_id and session_id values that will be automatically included in all traces.
83+
84+
Args:
85+
user_id: The user identifier
86+
session_id: The session identifier
87+
88+
Example:
89+
>>> from openlayer.lib.tracing import set_user_session_context
90+
>>>
91+
>>> # In your middleware or request handler
92+
>>> def middleware(request):
93+
... set_user_session_context(
94+
... user_id=request.user.id,
95+
... session_id=request.session.session_key
96+
... )
97+
... # Now all traced functions will automatically include these values
98+
"""
99+
if user_id is not None:
100+
UserSessionContext.set_user_id(user_id)
101+
if session_id is not None:
102+
UserSessionContext.set_session_id(session_id)
103+
104+
105+
def update_trace_user_session(
106+
user_id: Union[str, int, None] = _NOT_PROVIDED,
107+
session_id: Union[str, None] = _NOT_PROVIDED,
108+
) -> None:
109+
"""Update user_id and/or session_id for the current trace context.
110+
111+
This can be called anywhere in your traced code to override the user_id
112+
and/or session_id set in middleware. Inspired by Langfuse's updateActiveTrace pattern.
113+
114+
Args:
115+
user_id: The user identifier to set (optional). Pass None to clear.
116+
session_id: The session identifier to set (optional). Pass None to clear.
117+
118+
Example:
119+
>>> from openlayer.lib.tracing import update_trace_user_session
120+
>>>
121+
>>> @trace()
122+
>>> def process_request():
123+
... # Override user_id for this specific trace
124+
... update_trace_user_session(user_id="different_user_123")
125+
... return "result"
126+
>>>
127+
>>> @trace()
128+
>>> def start_new_session():
129+
... # Start a new session for this trace
130+
... update_trace_user_session(session_id="new_session_456")
131+
... return "result"
132+
>>>
133+
>>> @trace()
134+
>>> def switch_user_and_session():
135+
... # Update both at once
136+
... update_trace_user_session(
137+
... user_id="admin_user_789",
138+
... session_id="admin_session_abc"
139+
... )
140+
... return "result"
141+
>>>
142+
>>> @trace()
143+
>>> def clear_user():
144+
... # Clear user_id (set to None)
145+
... update_trace_user_session(user_id=None)
146+
... return "result"
147+
"""
148+
# Use sentinel object to distinguish between "not provided" and "explicitly None"
149+
if user_id is not _NOT_PROVIDED:
150+
UserSessionContext.set_user_id(user_id)
151+
if session_id is not _NOT_PROVIDED:
152+
UserSessionContext.set_session_id(session_id)
153+
154+
155+
def get_current_user_id() -> Optional[str]:
156+
"""Get the current user ID from context."""
157+
return UserSessionContext.get_user_id()
158+
159+
160+
def get_current_session_id() -> Optional[str]:
161+
"""Get the current session ID from context."""
162+
return UserSessionContext.get_session_id()
163+
164+
165+
def clear_user_session_context() -> None:
166+
"""Clear all user and session context."""
167+
UserSessionContext.clear_context()

src/openlayer/lib/tracing/tracer.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from .. import utils
1717
from . import enums, steps, traces
1818
from ..guardrails.base import GuardrailResult, GuardrailAction
19+
from .context import UserSessionContext
1920

2021
logger = logging.getLogger(__name__)
2122

@@ -923,6 +924,12 @@ def _handle_trace_completion(
923924
num_of_token_column_name="tokens",
924925
)
925926
)
927+
928+
# Add reserved column configurations for user context
929+
if "user_id" in trace_data:
930+
config.update({"user_id_column_name": "user_id"})
931+
if "session_id" in trace_data:
932+
config.update({"session_id_column_name": "session_id"})
926933
if "groundTruth" in trace_data:
927934
config.update({"ground_truth_column_name": "groundTruth"})
928935
if "context" in trace_data:
@@ -1184,7 +1191,16 @@ def post_process_trace(
11841191
if trace_obj.metadata is not None:
11851192
# Add each trace metadata key directly to the row/record level
11861193
trace_data.update(trace_obj.metadata)
1187-
1194+
1195+
# Add reserved columns for user and session context
1196+
user_id = UserSessionContext.get_user_id()
1197+
if user_id is not None:
1198+
trace_data["user_id"] = user_id
1199+
1200+
session_id = UserSessionContext.get_session_id()
1201+
if session_id is not None:
1202+
trace_data["session_id"] = session_id
1203+
11881204
if root_step.ground_truth:
11891205
trace_data["groundTruth"] = root_step.ground_truth
11901206
if input_variables:

src/openlayer/types/inference_pipelines/data_stream_params.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,12 @@ class ConfigLlmData(TypedDict, total=False):
8989
used.
9090
"""
9191

92+
user_id_column_name: Annotated[str, PropertyInfo(alias="userIdColumnName")]
93+
"""Name of the column with the user IDs."""
94+
95+
session_id_column_name: Annotated[str, PropertyInfo(alias="sessionIdColumnName")]
96+
"""Name of the column with the session IDs."""
97+
9298

9399
class ConfigTabularClassificationData(TypedDict, total=False):
94100
class_names: Required[Annotated[SequenceNotStr[str], PropertyInfo(alias="classNames")]]

0 commit comments

Comments
 (0)