Skip to content

Commit 837543f

Browse files
authored
Use LogRecord.event_name to set the log ID when the gcp log name attribute is not present. (#417)
* Initial commit to handle event name attr key * Update logic to set log name * Edit changelog * Run precommit * fix mypy.. and setup constraints * Fix install requires block again * Update code to validate log id and name * Fix lint and broke test * use event.name instead of event_name * fix mypy issue
1 parent f4a864a commit 837543f

File tree

7 files changed

+242
-16
lines changed

7 files changed

+242
-16
lines changed

dev-constraints.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,5 @@ setuptools==69.5.1
1717

1818
# pinned for snapshot tests. this should be bumped regularly and snapshots updated by running
1919
# tox -f py311-test -- --snapshot-update
20-
opentelemetry-api==1.30.0
21-
opentelemetry-sdk==1.30.0
20+
opentelemetry-api==1.35.0
21+
opentelemetry-sdk==1.35.0

opentelemetry-exporter-gcp-logging/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
- Do not call `logging.warning` when `LogRecord.body` is of None type, instead leave `LogEntry.payload` empty.
88
- Update opentelemetry-api/sdk dependencies to 1.3.
99

10+
The suffix part of `LogEntry.log_name` will be the `LogRecord.event_name` when
11+
that is present and the `gcp.log_name` attribute is not.
12+
1013
## Version 1.9.0a0
1114

1215
Released 2025-02-03

opentelemetry-exporter-gcp-logging/setup.cfg

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ package_dir=
2626
packages=find_namespace:
2727
install_requires =
2828
google-cloud-logging ~= 3.0
29-
opentelemetry-sdk ~= 1.30
30-
opentelemetry-api ~= 1.30
29+
opentelemetry-sdk >= 1.35.0
30+
opentelemetry-api >= 1.35.0
3131
opentelemetry-resourcedetector-gcp >= 1.5.0dev0, == 1.*
3232

3333
[options.packages.find]

opentelemetry-exporter-gcp-logging/src/opentelemetry/exporter/cloud_logging/__init__.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import datetime
1818
import json
1919
import logging
20+
import re
2021
import urllib.parse
2122
from typing import Any, Mapping, MutableMapping, Optional, Sequence
2223

@@ -102,6 +103,8 @@
102103
24: LogSeverity.EMERGENCY,
103104
}
104105

106+
INVALID_LOG_NAME_MESSAGE = "%s is not a valid log name. log name must be <512 characters and only contain characters: A-Za-z0-9/-_."
107+
105108

106109
def _convert_any_value_to_string(value: Any) -> str:
107110
if isinstance(value, bool):
@@ -176,6 +179,12 @@ def _set_payload_in_log_entry(log_entry: LogEntry, body: AnyValue):
176179
log_entry.text_payload = _convert_any_value_to_string(body)
177180

178181

182+
def is_log_id_valid(log_id: str) -> bool:
183+
return len(log_id) < 512 and not bool(
184+
re.search(r"[^A-Za-z0-9\-_/\.]", log_id)
185+
)
186+
187+
179188
class CloudLoggingExporter(LogExporter):
180189
def __init__(
181190
self,
@@ -201,6 +210,15 @@ def __init__(
201210
)
202211
)
203212

213+
def pick_log_id(self, log_name_attr: Any, event_name: str | None) -> str:
214+
if log_name_attr and isinstance(log_name_attr, str):
215+
if is_log_id_valid(log_name_attr):
216+
return log_name_attr.replace("/", "%2F")
217+
logging.warning(INVALID_LOG_NAME_MESSAGE, log_name_attr)
218+
if event_name and is_log_id_valid(event_name):
219+
return event_name.replace("/", "%2F")
220+
return self.default_log_name
221+
204222
def export(self, batch: Sequence[LogData]):
205223
now = datetime.datetime.now()
206224
log_entries = []
@@ -211,14 +229,7 @@ def export(self, batch: Sequence[LogData]):
211229
project_id = str(
212230
attributes.get(PROJECT_ID_ATTRIBUTE_KEY, self.project_id)
213231
)
214-
log_suffix = urllib.parse.quote_plus(
215-
str(
216-
attributes.get(
217-
LOG_NAME_ATTRIBUTE_KEY, self.default_log_name
218-
)
219-
)
220-
)
221-
log_entry.log_name = f"projects/{project_id}/logs/{log_suffix}"
232+
log_entry.log_name = f"projects/{project_id}/logs/{self.pick_log_id(attributes.get(LOG_NAME_ATTRIBUTE_KEY), log_record.event_name)}"
222233
# If timestamp is unset fall back to observed_time_unix_nano as recommended,
223234
# see https://github.com/open-telemetry/opentelemetry-proto/blob/4abbb78/opentelemetry/proto/logs/v1/logs.proto#L176-L179
224235
ts = Timestamp()
@@ -256,6 +267,8 @@ def export(self, batch: Sequence[LogData]):
256267
k: _convert_any_value_to_string(v)
257268
for k, v in attributes.items()
258269
}
270+
if log_record.event_name:
271+
log_entry.labels["event.name"] = log_record.event_name
259272
_set_payload_in_log_entry(log_entry, log_record.body)
260273
log_entries.append(log_entry)
261274

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
[
2+
{
3+
"entries": [
4+
{
5+
"jsonPayload": {
6+
"gen_ai.input.messages": [
7+
{
8+
"parts": [
9+
{
10+
"content": "Get weather details in New Delhi and San Francisco?",
11+
"type": "text"
12+
}
13+
],
14+
"role": "user"
15+
},
16+
{
17+
"parts": [
18+
{
19+
"arguments": {
20+
"location": "New Delhi"
21+
},
22+
"id": "get_current_weather_0",
23+
"name": "get_current_weather",
24+
"type": "tool_call"
25+
},
26+
{
27+
"arguments": {
28+
"location": "San Francisco"
29+
},
30+
"id": "get_current_weather_1",
31+
"name": "get_current_weather",
32+
"type": "tool_call"
33+
}
34+
],
35+
"role": "model"
36+
},
37+
{
38+
"parts": [
39+
{
40+
"id": "get_current_weather_0",
41+
"response": {
42+
"content": "{\"temperature\": 35, \"unit\": \"C\"}"
43+
},
44+
"type": "tool_call_response"
45+
},
46+
{
47+
"id": "get_current_weather_1",
48+
"response": {
49+
"content": "{\"temperature\": 25, \"unit\": \"C\"}"
50+
},
51+
"type": "tool_call_response"
52+
}
53+
],
54+
"role": "user"
55+
}
56+
],
57+
"gen_ai.output.messages": [
58+
{
59+
"finish_reason": "stop",
60+
"parts": [
61+
{
62+
"content": "The current temperature in New Delhi is 35°C, and in San Francisco, it is 25°C.",
63+
"type": "text"
64+
}
65+
],
66+
"role": "model"
67+
}
68+
],
69+
"gen_ai.system_instructions": [
70+
{
71+
"content": "You are a clever language model",
72+
"type": "text"
73+
}
74+
]
75+
},
76+
"labels": {
77+
"event.name": "gen_ai.client.inference.operation.details"
78+
},
79+
"logName": "projects/fakeproject/logs/gen_ai.client.inference.operation.details",
80+
"resource": {
81+
"labels": {
82+
"location": "global",
83+
"namespace": "",
84+
"node_id": ""
85+
},
86+
"type": "generic_node"
87+
},
88+
"timestamp": "2025-01-15T21:25:10.997977393Z"
89+
}
90+
],
91+
"partialSuccess": true
92+
}
93+
]

opentelemetry-exporter-gcp-logging/tests/__snapshots__/test_cloud_logging/test_convert_otlp_dict_body.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@
2121
}
2222
},
2323
"labels": {
24-
"event.name": "gen_ai.system.message",
24+
"event.name": "random.genai.event",
2525
"gen_ai.system": "true",
2626
"test": "23"
2727
},
28-
"logName": "projects/fakeproject/logs/test",
28+
"logName": "projects/fakeproject/logs/random.genai.event",
2929
"resource": {
3030
"labels": {
3131
"location": "global",

opentelemetry-exporter-gcp-logging/tests/test_cloud_logging.py

Lines changed: 119 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@
3535
LoggingServiceV2Client,
3636
)
3737
from opentelemetry._logs.severity import SeverityNumber
38-
from opentelemetry.exporter.cloud_logging import CloudLoggingExporter
38+
from opentelemetry.exporter.cloud_logging import (
39+
CloudLoggingExporter,
40+
is_log_id_valid,
41+
)
3942
from opentelemetry.sdk._logs import LogData
4043
from opentelemetry.sdk._logs._internal import LogRecord
4144
from opentelemetry.sdk.resources import Resource
@@ -74,14 +77,14 @@ def test_convert_otlp_dict_body(
7477
log_data = [
7578
LogData(
7679
log_record=LogRecord(
80+
event_name="random.genai.event",
7781
timestamp=1736976310997977393,
7882
severity_number=SeverityNumber(20),
7983
trace_id=25,
8084
span_id=22,
8185
attributes={
8286
"gen_ai.system": True,
8387
"test": 23,
84-
"event.name": "gen_ai.system.message",
8588
},
8689
body={
8790
"kvlistValue": {
@@ -153,6 +156,120 @@ def test_convert_non_json_dict_bytes(
153156
assert cloudloggingfake.get_calls() == snapshot_writelogentrycalls
154157

155158

159+
def test_convert_gen_ai_body(
160+
cloudloggingfake: CloudLoggingFake,
161+
snapshot_writelogentrycalls: List[WriteLogEntriesCall],
162+
) -> None:
163+
log_data = [
164+
LogData(
165+
log_record=LogRecord(
166+
event_name="gen_ai.client.inference.operation.details",
167+
timestamp=1736976310997977393,
168+
body={
169+
"gen_ai.input.messages": (
170+
{
171+
"role": "user",
172+
"parts": (
173+
{
174+
"type": "text",
175+
"content": "Get weather details in New Delhi and San Francisco?",
176+
},
177+
),
178+
},
179+
{
180+
"role": "model",
181+
"parts": (
182+
{
183+
"type": "tool_call",
184+
"arguments": {"location": "New Delhi"},
185+
"name": "get_current_weather",
186+
"id": "get_current_weather_0",
187+
},
188+
{
189+
"type": "tool_call",
190+
"arguments": {"location": "San Francisco"},
191+
"name": "get_current_weather",
192+
"id": "get_current_weather_1",
193+
},
194+
),
195+
},
196+
{
197+
"role": "user",
198+
"parts": (
199+
{
200+
"type": "tool_call_response",
201+
"response": {
202+
"content": '{"temperature": 35, "unit": "C"}'
203+
},
204+
"id": "get_current_weather_0",
205+
},
206+
{
207+
"type": "tool_call_response",
208+
"response": {
209+
"content": '{"temperature": 25, "unit": "C"}'
210+
},
211+
"id": "get_current_weather_1",
212+
},
213+
),
214+
},
215+
),
216+
"gen_ai.system_instructions": (
217+
{
218+
"type": "text",
219+
"content": "You are a clever language model",
220+
},
221+
),
222+
"gen_ai.output.messages": (
223+
{
224+
"role": "model",
225+
"parts": (
226+
{
227+
"type": "text",
228+
"content": "The current temperature in New Delhi is 35°C, and in San Francisco, it is 25°C.",
229+
},
230+
),
231+
"finish_reason": "stop",
232+
},
233+
),
234+
},
235+
),
236+
instrumentation_scope=InstrumentationScope("test"),
237+
)
238+
]
239+
cloudloggingfake.exporter.export(log_data)
240+
assert cloudloggingfake.get_calls() == snapshot_writelogentrycalls
241+
242+
243+
def test_is_log_id_valid():
244+
assert is_log_id_valid(";") is False
245+
assert is_log_id_valid("aB12//..--__") is True
246+
assert is_log_id_valid("a" * 512) is False
247+
assert is_log_id_valid("abc1212**") is False
248+
assert is_log_id_valid("gen_ai.client.inference.operation.details") is True
249+
250+
251+
def test_pick_log_id() -> None:
252+
exporter = CloudLoggingExporter(
253+
client=LoggingServiceV2Client(credentials=AnonymousCredentials()),
254+
project_id=PROJECT_ID,
255+
default_log_name="test",
256+
)
257+
assert (
258+
exporter.pick_log_id("valid_log_name_attr", "event_name_str")
259+
== "valid_log_name_attr"
260+
)
261+
assert (
262+
exporter.pick_log_id("invalid_attr**2", "event_name_str")
263+
== "event_name_str"
264+
)
265+
assert exporter.pick_log_id(None, "event_name_str") == "event_name_str"
266+
assert exporter.pick_log_id(None, None) == exporter.default_log_name
267+
assert (
268+
exporter.pick_log_id(None, "invalid_event_name_id24$")
269+
== exporter.default_log_name
270+
)
271+
272+
156273
@pytest.mark.parametrize(
157274
"body",
158275
[

0 commit comments

Comments
 (0)