Skip to content

Commit 46757ff

Browse files
nstawskiocelotlopentelemetrybot
authored
Use BoundedAttributes instead of raw dict to extract attributes from LogRecord #3114 (#3310)
Co-authored-by: Diego Hurtado <[email protected]> Co-authored-by: OpenTelemetry Bot <[email protected]>
1 parent 4d0de54 commit 46757ff

File tree

6 files changed

+228
-4
lines changed

6 files changed

+228
-4
lines changed

Diff for: CHANGELOG.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88
## Unreleased
9-
9+
- Use BoundedAttributes instead of raw dict to extract attributes from LogRecord and Support dropped_attributes_count in LogRecord ([#3310](https://github.com/open-telemetry/opentelemetry-python/pull/3310))
1010
## Version 1.18.0/0.39b0 (2023-05-04)
1111

1212
- Select histogram aggregation with an environment variable

Diff for: opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
Logger,
1919
LoggerProvider,
2020
LoggingHandler,
21+
LogLimits,
2122
LogRecord,
2223
LogRecordProcessor,
2324
)
@@ -27,6 +28,7 @@
2728
"Logger",
2829
"LoggerProvider",
2930
"LoggingHandler",
31+
"LogLimits",
3032
"LogRecord",
3133
"LogRecordProcessor",
3234
]

Diff for: opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py

+117-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import logging
2020
import threading
2121
import traceback
22+
from os import environ
2223
from time import time_ns
2324
from typing import Any, Callable, Optional, Tuple, Union
2425

@@ -31,6 +32,11 @@
3132
get_logger_provider,
3233
std_to_otel,
3334
)
35+
from opentelemetry.attributes import BoundedAttributes
36+
from opentelemetry.sdk.environment_variables import (
37+
OTEL_ATTRIBUTE_COUNT_LIMIT,
38+
OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT,
39+
)
3440
from opentelemetry.sdk.resources import Resource
3541
from opentelemetry.sdk.util import ns_to_iso_str
3642
from opentelemetry.sdk.util.instrumentation import InstrumentationScope
@@ -45,6 +51,101 @@
4551

4652
_logger = logging.getLogger(__name__)
4753

54+
_DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT = 128
55+
_ENV_VALUE_UNSET = ""
56+
57+
58+
class LogLimits:
59+
"""This class is based on a SpanLimits class in the Tracing module.
60+
61+
This class represents the limits that should be enforced on recorded data such as events, links, attributes etc.
62+
63+
This class does not enforce any limits itself. It only provides a way to read limits from env,
64+
default values and from user provided arguments.
65+
66+
All limit arguments must be either a non-negative integer, ``None`` or ``LogLimits.UNSET``.
67+
68+
- All limit arguments are optional.
69+
- If a limit argument is not set, the class will try to read its value from the corresponding
70+
environment variable.
71+
- If the environment variable is not set, the default value, if any, will be used.
72+
73+
Limit precedence:
74+
75+
- If a model specific limit is set, it will be used.
76+
- Else if the corresponding global limit is set, it will be used.
77+
- Else if the model specific limit has a default value, the default value will be used.
78+
- Else if the global limit has a default value, the default value will be used.
79+
80+
Args:
81+
max_attributes: Maximum number of attributes that can be added to a span, event, and link.
82+
Environment variable: ``OTEL_ATTRIBUTE_COUNT_LIMIT``
83+
Default: {_DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT}
84+
max_attribute_length: Maximum length an attribute value can have. Values longer than
85+
the specified length will be truncated.
86+
"""
87+
88+
UNSET = -1
89+
90+
def __init__(
91+
self,
92+
max_attributes: Optional[int] = None,
93+
max_attribute_length: Optional[int] = None,
94+
):
95+
96+
# attribute count
97+
global_max_attributes = self._from_env_if_absent(
98+
max_attributes, OTEL_ATTRIBUTE_COUNT_LIMIT
99+
)
100+
self.max_attributes = (
101+
global_max_attributes
102+
if global_max_attributes is not None
103+
else _DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT
104+
)
105+
106+
# attribute length
107+
self.max_attribute_length = self._from_env_if_absent(
108+
max_attribute_length,
109+
OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT,
110+
)
111+
112+
def __repr__(self):
113+
return f"{type(self).__name__}(max_attributes={self.max_attributes}, max_attribute_length={self.max_attribute_length})"
114+
115+
@classmethod
116+
def _from_env_if_absent(
117+
cls, value: Optional[int], env_var: str, default: Optional[int] = None
118+
) -> Optional[int]:
119+
if value == cls.UNSET:
120+
return None
121+
122+
err_msg = "{0} must be a non-negative integer but got {}"
123+
124+
# if no value is provided for the limit, try to load it from env
125+
if value is None:
126+
# return default value if env var is not set
127+
if env_var not in environ:
128+
return default
129+
130+
str_value = environ.get(env_var, "").strip().lower()
131+
if str_value == _ENV_VALUE_UNSET:
132+
return None
133+
134+
try:
135+
value = int(str_value)
136+
except ValueError:
137+
raise ValueError(err_msg.format(env_var, str_value))
138+
139+
if value < 0:
140+
raise ValueError(err_msg.format(env_var, value))
141+
return value
142+
143+
144+
_UnsetLogLimits = LogLimits(
145+
max_attributes=LogLimits.UNSET,
146+
max_attribute_length=LogLimits.UNSET,
147+
)
148+
48149

49150
class LogRecord(APILogRecord):
50151
"""A LogRecord instance represents an event being logged.
@@ -66,6 +167,7 @@ def __init__(
66167
body: Optional[Any] = None,
67168
resource: Optional[Resource] = None,
68169
attributes: Optional[Attributes] = None,
170+
limits: Optional[LogLimits] = _UnsetLogLimits,
69171
):
70172
super().__init__(
71173
**{
@@ -77,7 +179,12 @@ def __init__(
77179
"severity_text": severity_text,
78180
"severity_number": severity_number,
79181
"body": body,
80-
"attributes": attributes,
182+
"attributes": BoundedAttributes(
183+
maxlen=limits.max_attributes,
184+
attributes=attributes if bool(attributes) else None,
185+
immutable=False,
186+
max_value_len=limits.max_attribute_length,
187+
),
81188
}
82189
)
83190
self.resource = resource
@@ -93,7 +200,9 @@ def to_json(self, indent=4) -> str:
93200
"body": self.body,
94201
"severity_number": repr(self.severity_number),
95202
"severity_text": self.severity_text,
96-
"attributes": self.attributes,
203+
"attributes": dict(self.attributes)
204+
if bool(self.attributes)
205+
else None,
97206
"timestamp": ns_to_iso_str(self.timestamp),
98207
"trace_id": f"0x{format_trace_id(self.trace_id)}"
99208
if self.trace_id is not None
@@ -109,6 +218,12 @@ def to_json(self, indent=4) -> str:
109218
indent=indent,
110219
)
111220

221+
@property
222+
def dropped_attributes(self) -> int:
223+
if self.attributes:
224+
return self.attributes.dropped
225+
return 0
226+
112227

113228
class LogData:
114229
"""Readable LogRecord data plus associated InstrumentationLibrary."""

Diff for: opentelemetry-sdk/tests/logs/test_handler.py

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
from opentelemetry._logs import SeverityNumber
1919
from opentelemetry._logs import get_logger as APIGetLogger
20+
from opentelemetry.attributes import BoundedAttributes
2021
from opentelemetry.sdk import trace
2122
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
2223
from opentelemetry.semconv.trace import SpanAttributes
@@ -91,6 +92,7 @@ def test_log_record_user_attributes(self):
9192

9293
self.assertIsNotNone(log_record)
9394
self.assertEqual(log_record.attributes, {"http.status_code": 200})
95+
self.assertTrue(isinstance(log_record.attributes, BoundedAttributes))
9496

9597
def test_log_record_exception(self):
9698
"""Exception information will be included in attributes"""

Diff for: opentelemetry-sdk/tests/logs/test_log_limits.py

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import unittest
16+
17+
from opentelemetry.sdk._logs import LogLimits
18+
from opentelemetry.sdk._logs._internal import (
19+
_DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT,
20+
)
21+
22+
23+
class TestLogLimits(unittest.TestCase):
24+
def test_log_limits_repr_unset(self):
25+
expected = f"LogLimits(max_attributes={_DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT}, max_attribute_length=None)"
26+
limits = str(LogLimits())
27+
28+
self.assertEqual(expected, limits)
29+
30+
def test_log_limits_max_attributes(self):
31+
expected = 1
32+
limits = LogLimits(max_attributes=1)
33+
34+
self.assertEqual(expected, limits.max_attributes)
35+
36+
def test_log_limits_max_attribute_length(self):
37+
expected = 1
38+
limits = LogLimits(max_attribute_length=1)
39+
40+
self.assertEqual(expected, limits.max_attribute_length)

Diff for: opentelemetry-sdk/tests/logs/test_log_record.py

+66-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
import json
1616
import unittest
1717

18-
from opentelemetry.sdk._logs import LogRecord
18+
from opentelemetry.attributes import BoundedAttributes
19+
from opentelemetry.sdk._logs import LogLimits, LogRecord
1920

2021

2122
class TestLogRecord(unittest.TestCase):
@@ -39,3 +40,67 @@ def test_log_record_to_json(self):
3940
body="a log line",
4041
).to_json()
4142
self.assertEqual(expected, actual)
43+
44+
def test_log_record_bounded_attributes(self):
45+
attr = {"key": "value"}
46+
47+
result = LogRecord(timestamp=0, body="a log line", attributes=attr)
48+
49+
self.assertTrue(isinstance(result.attributes, BoundedAttributes))
50+
51+
def test_log_record_dropped_attributes_empty_limits(self):
52+
attr = {"key": "value"}
53+
54+
result = LogRecord(timestamp=0, body="a log line", attributes=attr)
55+
56+
self.assertTrue(result.dropped_attributes == 0)
57+
58+
def test_log_record_dropped_attributes_set_limits_max_attribute(self):
59+
attr = {"key": "value", "key2": "value2"}
60+
limits = LogLimits(
61+
max_attributes=1,
62+
)
63+
64+
result = LogRecord(
65+
timestamp=0, body="a log line", attributes=attr, limits=limits
66+
)
67+
self.assertTrue(result.dropped_attributes == 1)
68+
69+
def test_log_record_dropped_attributes_set_limits_max_attribute_length(
70+
self,
71+
):
72+
attr = {"key": "value", "key2": "value2"}
73+
expected = {"key": "v", "key2": "v"}
74+
limits = LogLimits(
75+
max_attribute_length=1,
76+
)
77+
78+
result = LogRecord(
79+
timestamp=0, body="a log line", attributes=attr, limits=limits
80+
)
81+
self.assertTrue(result.dropped_attributes == 0)
82+
self.assertEqual(expected, result.attributes)
83+
84+
def test_log_record_dropped_attributes_set_limits(self):
85+
attr = {"key": "value", "key2": "value2"}
86+
expected = {"key2": "v"}
87+
limits = LogLimits(
88+
max_attributes=1,
89+
max_attribute_length=1,
90+
)
91+
92+
result = LogRecord(
93+
timestamp=0, body="a log line", attributes=attr, limits=limits
94+
)
95+
self.assertTrue(result.dropped_attributes == 1)
96+
self.assertEqual(expected, result.attributes)
97+
98+
def test_log_record_dropped_attributes_unset_limits(self):
99+
attr = {"key": "value", "key2": "value2"}
100+
limits = LogLimits()
101+
102+
result = LogRecord(
103+
timestamp=0, body="a log line", attributes=attr, limits=limits
104+
)
105+
self.assertTrue(result.dropped_attributes == 0)
106+
self.assertEqual(attr, result.attributes)

0 commit comments

Comments
 (0)