Skip to content

Commit a2a91a1

Browse files
v0.6.4 allow tagging traces (#114)
* v0.6.4 allow tagging traces * Update src/lmnr/sdk/client/asynchronous/resources/tags.py Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * ellipsis comment --------- Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
1 parent 8c782d9 commit a2a91a1

File tree

9 files changed

+222
-3
lines changed

9 files changed

+222
-3
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
[project]
88
name = "lmnr"
9-
version = "0.6.3"
9+
version = "0.6.4"
1010
description = "Python SDK for Laminar"
1111
authors = [
1212
{ name = "lmnr.ai", email = "[email protected]" }

src/lmnr/sdk/client/asynchronous/async_client.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
AsyncAgent,
1212
AsyncBrowserEvents,
1313
AsyncEvals,
14+
AsyncTags,
1415
)
1516
from lmnr.sdk.utils import from_env
1617

@@ -76,6 +77,7 @@ def __init__(
7677
self.__browser_events = AsyncBrowserEvents(
7778
self.__client, self.__base_url, self.__project_api_key
7879
)
80+
self.__tags = AsyncTags(self.__client, self.__base_url, self.__project_api_key)
7981

8082
@property
8183
def agent(self) -> AsyncAgent:
@@ -104,6 +106,15 @@ def _browser_events(self) -> AsyncBrowserEvents:
104106
"""
105107
return self.__browser_events
106108

109+
@property
110+
def tags(self) -> AsyncTags:
111+
"""Get the Tags resource.
112+
113+
Returns:
114+
AsyncTags: The Tags resource instance.
115+
"""
116+
return self.__tags
117+
107118
def is_closed(self) -> bool:
108119
return self.__client.is_closed
109120

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
from lmnr.sdk.client.asynchronous.resources.agent import AsyncAgent
22
from lmnr.sdk.client.asynchronous.resources.browser_events import AsyncBrowserEvents
33
from lmnr.sdk.client.asynchronous.resources.evals import AsyncEvals
4+
from lmnr.sdk.client.asynchronous.resources.tags import AsyncTags
45

56
__all__ = [
67
"AsyncAgent",
78
"AsyncEvals",
89
"AsyncBrowserEvents",
10+
"AsyncTags",
911
]
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
"""Resource for tagging traces."""
2+
3+
import json
4+
import uuid
5+
6+
from lmnr.sdk.client.asynchronous.resources.base import BaseAsyncResource
7+
from lmnr.sdk.log import get_default_logger
8+
9+
logger = get_default_logger(__name__)
10+
11+
12+
class AsyncTags(BaseAsyncResource):
13+
"""Resource for tagging traces."""
14+
15+
async def tag(
16+
self,
17+
trace_id: str | int | uuid.UUID,
18+
tags: list[str] | str,
19+
):
20+
"""Tag a trace with a list of tags. Note that the trace must be ended
21+
before tagging it. You may want to call `Laminar.flush()` after the
22+
trace that you want to tag.
23+
24+
Args:
25+
trace_id (str | int | uuid.UUID): The trace id to tag.
26+
tags (list[str] | str): The tag or list of tags to add to the trace.
27+
28+
Raises:
29+
ValueError: If the trace id is not a valid UUID.
30+
31+
Returns:
32+
list[dict]: The response from the server.
33+
34+
Example:
35+
```python
36+
from lmnr import Laminar, AsyncLaminarClient, observe
37+
38+
Laminar.initialize()
39+
client = AsyncLaminarClient()
40+
trace_id = None
41+
42+
@observe()
43+
def foo():
44+
trace_id = Laminar.get_trace_id()
45+
pass
46+
47+
# make sure `foo` is called outside a trace context
48+
foo()
49+
50+
# or make sure the trace is ended by this point
51+
Laminar.flush()
52+
53+
await client.tags.tag(trace_id, "my_tag")
54+
```
55+
"""
56+
trace_tags = tags if isinstance(tags, list) else [tags]
57+
if isinstance(trace_id, uuid.UUID):
58+
trace_id = str(trace_id)
59+
elif isinstance(trace_id, int):
60+
trace_id = str(uuid.UUID(int=trace_id))
61+
elif isinstance(trace_id, str):
62+
uuid.UUID(trace_id) # Will raise ValueError if invalid
63+
else:
64+
raise ValueError(f"Invalid trace id: {trace_id}")
65+
66+
url = self._base_url + "/v1/tag"
67+
payload = {
68+
"traceId": trace_id,
69+
"names": trace_tags,
70+
}
71+
response = await self._client.post(
72+
url,
73+
content=json.dumps(payload),
74+
headers={
75+
**self._headers(),
76+
},
77+
)
78+
79+
if response.status_code == 404:
80+
logger.warning(
81+
f"Trace {trace_id} not found. The trace may have not been ended yet."
82+
)
83+
return []
84+
85+
if response.status_code != 200:
86+
raise ValueError(
87+
f"Failed to tag trace: [{response.status_code}] {response.text}"
88+
)
89+
return response.json()
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from lmnr.sdk.client.synchronous.resources.agent import Agent
22
from lmnr.sdk.client.synchronous.resources.browser_events import BrowserEvents
33
from lmnr.sdk.client.synchronous.resources.evals import Evals
4+
from lmnr.sdk.client.synchronous.resources.tags import Tags
45

5-
__all__ = ["Agent", "Evals", "BrowserEvents"]
6+
__all__ = ["Agent", "Evals", "BrowserEvents", "Tags"]
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
"""Resource for tagging traces."""
2+
3+
import json
4+
import uuid
5+
6+
from lmnr.sdk.client.synchronous.resources.base import BaseResource
7+
from lmnr.sdk.log import get_default_logger
8+
9+
logger = get_default_logger(__name__)
10+
11+
12+
class Tags(BaseResource):
13+
"""Resource for tagging traces."""
14+
15+
def tag(
16+
self,
17+
trace_id: str | int | uuid.UUID,
18+
tags: list[str] | str,
19+
):
20+
"""Tag a trace with a list of tags. Note that the trace must be ended
21+
before tagging it. You may want to call `Laminar.flush()` after the
22+
trace that you want to tag.
23+
24+
Args:
25+
trace_id (str | int | uuid.UUID): The trace id to tag.
26+
tags (list[str] | str): The tag or list of tags to add to the trace.
27+
28+
Raises:
29+
ValueError: If the trace id is not a valid UUID.
30+
31+
Returns:
32+
list[dict]: The response from the server.
33+
34+
Example:
35+
```python
36+
from lmnr import Laminar, LaminarClient, observe
37+
38+
Laminar.initialize()
39+
client = LaminarClient()
40+
trace_id = None
41+
42+
@observe()
43+
def foo():
44+
trace_id = Laminar.get_trace_id()
45+
pass
46+
47+
# make sure `foo` is called outside a trace context
48+
foo()
49+
50+
# or make sure the trace is ended by this point
51+
Laminar.flush()
52+
53+
client.tags.tag(trace_id, "my_tag")
54+
```
55+
"""
56+
trace_tags = tags if isinstance(tags, list) else [tags]
57+
if isinstance(trace_id, uuid.UUID):
58+
trace_id = str(trace_id)
59+
elif isinstance(trace_id, int):
60+
trace_id = str(uuid.UUID(int=trace_id))
61+
elif isinstance(trace_id, str):
62+
uuid.UUID(trace_id)
63+
else:
64+
raise ValueError(f"Invalid trace id: {trace_id}")
65+
66+
url = self._base_url + "/v1/tag"
67+
payload = {
68+
"traceId": trace_id,
69+
"names": trace_tags,
70+
}
71+
response = self._client.post(
72+
url,
73+
content=json.dumps(payload),
74+
headers={
75+
**self._headers(),
76+
},
77+
)
78+
79+
if response.status_code == 404:
80+
logger.warning(
81+
f"Trace {trace_id} not found. The trace may have not been ended yet."
82+
)
83+
return []
84+
85+
if response.status_code != 200:
86+
raise ValueError(
87+
f"Failed to tag trace: [{response.status_code}] {response.text}"
88+
)
89+
return response.json()

src/lmnr/sdk/client/synchronous/sync_client.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
Agent,
1212
BrowserEvents,
1313
Evals,
14+
Tags,
1415
)
1516
from lmnr.sdk.utils import from_env
1617

@@ -25,6 +26,7 @@ class LaminarClient:
2526
# Resource properties
2627
__agent: Agent | None = None
2728
__evals: Evals | None = None
29+
__tags: Tags | None = None
2830

2931
def __init__(
3032
self,
@@ -75,6 +77,7 @@ def __init__(
7577
self.__browser_events = BrowserEvents(
7678
self.__client, self.__base_url, self.__project_api_key
7779
)
80+
self.__tags = Tags(self.__client, self.__base_url, self.__project_api_key)
7881

7982
@property
8083
def agent(self) -> Agent:
@@ -103,6 +106,15 @@ def _browser_events(self) -> BrowserEvents:
103106
"""
104107
return self.__browser_events
105108

109+
@property
110+
def tags(self) -> Tags:
111+
"""Get the Tags resource.
112+
113+
Returns:
114+
Tags: The Tags resource instance.
115+
"""
116+
return self.__tags
117+
106118
def shutdown(self):
107119
"""Shutdown the client by closing underlying connections."""
108120
self.__client.close()

src/lmnr/sdk/laminar.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from lmnr.opentelemetry_lib.decorators import json_dumps
1313
from opentelemetry import context as context_api, trace
1414
from opentelemetry.context import attach, detach
15+
from opentelemetry.trace import INVALID_TRACE_ID
1516
from opentelemetry.sdk.trace.id_generator import RandomIdGenerator
1617
from opentelemetry.util.types import AttributeValue
1718

@@ -703,6 +704,20 @@ def get_base_http_url(cls):
703704
def get_project_api_key(cls):
704705
return cls.__project_api_key
705706

707+
@classmethod
708+
def get_trace_id(cls) -> uuid.UUID | None:
709+
"""Get the trace id for the current active span represented as a UUID.
710+
Returns None if there is no active span.
711+
712+
Returns:
713+
uuid.UUID | None: The trace id for the current span, or None if\
714+
there is no active span.
715+
"""
716+
trace_id = trace.get_current_span().get_span_context().trace_id
717+
if trace_id == INVALID_TRACE_ID:
718+
return None
719+
return uuid.UUID(int=trace_id)
720+
706721
@classmethod
707722
def _headers(cls):
708723
assert cls.__project_api_key is not None, "Project API key is not set"

src/lmnr/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from packaging import version
44

55

6-
__version__ = "0.6.3"
6+
__version__ = "0.6.4"
77
PYTHON_VERSION = f"{sys.version_info.major}.{sys.version_info.minor}"
88

99

0 commit comments

Comments
 (0)