Skip to content

Commit ed247f5

Browse files
authored
Update logic in cloud logging exporter to handle various types in LogRecord.attributes (#432)
* Initial commit to handle all attrs types * Add changelog, log exception * Fix formatting
1 parent 837543f commit ed247f5

File tree

5 files changed

+138
-75
lines changed

5 files changed

+138
-75
lines changed

opentelemetry-exporter-gcp-logging/CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
## Unreleased
44

5-
- Added support for when a `Mapping[str, bytes]` or `Mapping[str, List[bytes]]` is in `LogRecord.body`.
6-
- Added support for when a `Mapping[str, List[Mapping]]` is in `LogRecord.body`.
5+
- Added support for when a `Mapping[str, bytes]` or `Mapping[str, List[bytes]]` is in `LogRecord.body` or `LogRecord.attributes`.
6+
- Added support for when a `Mapping[str, List[Mapping]]` is in `LogRecord.body` or `LogRecord.attributes`.
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

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

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import json
1919
import logging
2020
import re
21-
import urllib.parse
21+
from base64 import b64encode
2222
from typing import Any, Mapping, MutableMapping, Optional, Sequence
2323

2424
import google.auth
@@ -106,16 +106,30 @@
106106
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/-_."
107107

108108

109+
class _GenAiJsonEncoder(json.JSONEncoder):
110+
def default(self, o: Any) -> Any:
111+
if isinstance(o, bytes):
112+
return b64encode(o).decode()
113+
return super().default(o)
114+
115+
109116
def _convert_any_value_to_string(value: Any) -> str:
110117
if isinstance(value, bool):
111118
return "true" if value else "false"
112119
if isinstance(value, bytes):
113120
return base64.b64encode(value).decode()
114121
if isinstance(value, (int, float, str)):
115122
return str(value)
116-
if isinstance(value, (list, tuple)):
117-
return json.dumps(value)
118-
return ""
123+
if isinstance(value, (list, tuple, Mapping)):
124+
return json.dumps(value, separators=(",", ":"), cls=_GenAiJsonEncoder)
125+
try:
126+
return str(value)
127+
except Exception as exc: # pylint: disable=broad-except
128+
logging.exception(
129+
"Error mapping AnyValue to string, this field will not be added to the LogEntry: %s",
130+
exc,
131+
)
132+
return ""
119133

120134

121135
# Be careful not to mutate original body. Make copies of anything that needs to change.

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@
1414
"Ref": 164611595.0
1515
},
1616
"labels": {
17-
"boolArray": "[true, false, true, true]",
17+
"boolArray": "[true,false,true,true]",
1818
"float": "25.43231",
1919
"int": "25",
20-
"intArray": "[21, 18, 23, 17]"
20+
"intArray": "[21,18,23,17]"
2121
},
2222
"logName": "projects/fakeproject/logs/test",
2323
"resource": {
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
[
2+
{
3+
"entries": [
4+
{
5+
"labels": {
6+
"a": "[{\"key\":\"Ynl0ZXM=\"}]",
7+
"b": "[true,false,false,true]",
8+
"c": "{\"a_dict\":\"abcd\",\"akey\":1234}",
9+
"d": "{\"gen_ai.input.messages\":[{\"role\":\"user\",\"parts\":[{\"type\":\"text\",\"content\":\"Get weather details in New Delhi and San Francisco?\"}]},{\"role\":\"model\",\"parts\":[{\"type\":\"tool_call\",\"arguments\":{\"location\":\"New Delhi\"},\"name\":\"get_current_weather\",\"id\":\"get_current_weather_0\"},{\"type\":\"tool_call\",\"arguments\":{\"location\":\"San Francisco\"},\"name\":\"get_current_weather\",\"id\":\"get_current_weather_1\"}]},{\"role\":\"user\",\"parts\":[{\"type\":\"tool_call_response\",\"response\":{\"content\":\"{\\\"temperature\\\": 35, \\\"unit\\\": \\\"C\\\"}\"},\"id\":\"get_current_weather_0\"},{\"type\":\"tool_call_response\",\"response\":{\"content\":\"{\\\"temperature\\\": 25, \\\"unit\\\": \\\"C\\\"}\"},\"id\":\"get_current_weather_1\"}]}],\"gen_ai.system_instructions\":[{\"type\":\"text\",\"content\":\"You are a clever language model\"}],\"gen_ai.output.messages\":[{\"role\":\"model\",\"parts\":[{\"type\":\"text\",\"content\":\"The current temperature in New Delhi is 35\\u00b0C, and in San Francisco, it is 25\\u00b0C.\"}],\"finish_reason\":\"stop\"}]}"
10+
},
11+
"logName": "projects/fakeproject/logs/test",
12+
"resource": {
13+
"labels": {
14+
"location": "global",
15+
"namespace": "",
16+
"node_id": ""
17+
},
18+
"type": "generic_node"
19+
},
20+
"timestamp": "2025-01-15T21:25:10.997977393Z"
21+
}
22+
],
23+
"partialSuccess": true
24+
}
25+
]

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

Lines changed: 91 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,74 @@
4646

4747
PROJECT_ID = "fakeproject"
4848

49+
GEN_AI_DICT = {
50+
"gen_ai.input.messages": (
51+
{
52+
"role": "user",
53+
"parts": (
54+
{
55+
"type": "text",
56+
"content": "Get weather details in New Delhi and San Francisco?",
57+
},
58+
),
59+
},
60+
{
61+
"role": "model",
62+
"parts": (
63+
{
64+
"type": "tool_call",
65+
"arguments": {"location": "New Delhi"},
66+
"name": "get_current_weather",
67+
"id": "get_current_weather_0",
68+
},
69+
{
70+
"type": "tool_call",
71+
"arguments": {"location": "San Francisco"},
72+
"name": "get_current_weather",
73+
"id": "get_current_weather_1",
74+
},
75+
),
76+
},
77+
{
78+
"role": "user",
79+
"parts": (
80+
{
81+
"type": "tool_call_response",
82+
"response": {
83+
"content": '{"temperature": 35, "unit": "C"}'
84+
},
85+
"id": "get_current_weather_0",
86+
},
87+
{
88+
"type": "tool_call_response",
89+
"response": {
90+
"content": '{"temperature": 25, "unit": "C"}'
91+
},
92+
"id": "get_current_weather_1",
93+
},
94+
),
95+
},
96+
),
97+
"gen_ai.system_instructions": (
98+
{
99+
"type": "text",
100+
"content": "You are a clever language model",
101+
},
102+
),
103+
"gen_ai.output.messages": (
104+
{
105+
"role": "model",
106+
"parts": (
107+
{
108+
"type": "text",
109+
"content": "The current temperature in New Delhi is 35°C, and in San Francisco, it is 25°C.",
110+
},
111+
),
112+
"finish_reason": "stop",
113+
},
114+
),
115+
}
116+
49117

50118
def test_too_large_log_raises_warning(caplog) -> None:
51119
client = LoggingServiceV2Client(credentials=AnonymousCredentials())
@@ -165,73 +233,7 @@ def test_convert_gen_ai_body(
165233
log_record=LogRecord(
166234
event_name="gen_ai.client.inference.operation.details",
167235
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-
},
236+
body=GEN_AI_DICT,
235237
),
236238
instrumentation_scope=InstrumentationScope("test"),
237239
)
@@ -303,3 +305,25 @@ def test_convert_various_types_of_bodies(
303305
]
304306
cloudloggingfake.exporter.export(log_data)
305307
assert cloudloggingfake.get_calls() == snapshot_writelogentrycalls
308+
309+
310+
def test_convert_various_types_of_attributes(
311+
cloudloggingfake: CloudLoggingFake,
312+
snapshot_writelogentrycalls: List[WriteLogEntriesCall],
313+
) -> None:
314+
log_data = [
315+
LogData(
316+
log_record=LogRecord(
317+
attributes={
318+
"a": [{"key": b"bytes"}],
319+
"b": [True, False, False, True],
320+
"c": {"a_dict": "abcd", "akey": 1234},
321+
"d": GEN_AI_DICT,
322+
},
323+
timestamp=1736976310997977393,
324+
),
325+
instrumentation_scope=InstrumentationScope("test"),
326+
)
327+
]
328+
cloudloggingfake.exporter.export(log_data)
329+
assert cloudloggingfake.get_calls() == snapshot_writelogentrycalls

0 commit comments

Comments
 (0)