Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use BoundedAttributes instead of raw dict to extract attributes from LogRecord #3114 #3310

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
3fd450d
Switched to using BoundedAttributes as per #3114
nstawski May 10, 2023
986ba7d
Switched to using BoundedAttributes as per #3114
nstawski May 10, 2023
6a3b62a
Code linting
nstawski May 10, 2023
2d88ce4
Code linting
nstawski May 10, 2023
1c5b20c
Code linting
nstawski May 10, 2023
bdae2f2
Comment
nstawski May 10, 2023
deff884
Moved the instantiating of BoundedAttributes into the LogRecord class…
nstawski May 11, 2023
58b734d
Properly serializing the items from BoundedAttributes in to_json sinc…
nstawski May 11, 2023
2dd23e5
Update version to 1.19.0.dev/0.40b0.dev (#3297)
opentelemetrybot May 12, 2023
13f2426
Merge branch 'open-telemetry:main' into ns-3114-bounded-attributes-in…
nstawski May 15, 2023
9f88cd5
Lint
nstawski May 15, 2023
f626411
Tests + changelog update
nstawski May 15, 2023
820eb09
Lint
nstawski May 15, 2023
4749c8e
Added limits + tests
nstawski May 19, 2023
7c39620
Updated changelog
nstawski May 19, 2023
cb17281
Added LogLimits and tests
nstawski May 19, 2023
387c928
Lint
nstawski May 19, 2023
3e6e5ea
Lint
nstawski May 19, 2023
b1a646d
Lint
nstawski May 19, 2023
95c9c22
Lint
nstawski May 19, 2023
8fba6c8
Lint
nstawski May 19, 2023
a8c6332
Lint
nstawski May 19, 2023
4946e18
Addressed pull request comments
nstawski May 19, 2023
08df03c
Merge branch 'main' into ns-3114-bounded-attributes-insteadof-dict-Lo…
ocelotl May 23, 2023
72fade9
Addressed pull request comment, removed private variable from __all__
nstawski May 23, 2023
93beb55
lint
nstawski May 23, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased

- 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))
## Version 1.18.0/0.39b0 (2023-05-04)

- Select histogram aggregation with an environment variable
Expand Down
2 changes: 2 additions & 0 deletions opentelemetry-sdk/src/opentelemetry/sdk/_logs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
Logger,
LoggerProvider,
LoggingHandler,
LogLimits,
LogRecord,
LogRecordProcessor,
)
Expand All @@ -27,6 +28,7 @@
"Logger",
"LoggerProvider",
"LoggingHandler",
"LogLimits",
"LogRecord",
"LogRecordProcessor",
]
119 changes: 117 additions & 2 deletions opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import logging
import threading
import traceback
from os import environ
from time import time_ns
from typing import Any, Callable, Optional, Tuple, Union

Expand All @@ -31,6 +32,11 @@
get_logger_provider,
std_to_otel,
)
from opentelemetry.attributes import BoundedAttributes
from opentelemetry.sdk.environment_variables import (
OTEL_ATTRIBUTE_COUNT_LIMIT,
OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT,
)
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.util import ns_to_iso_str
from opentelemetry.sdk.util.instrumentation import InstrumentationScope
Expand All @@ -45,6 +51,101 @@

_logger = logging.getLogger(__name__)

_DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT = 128
_ENV_VALUE_UNSET = ""


class LogLimits:
"""This class is based on a SpanLimits class in the Tracing module.
ocelotl marked this conversation as resolved.
Show resolved Hide resolved

This class represents the limits that should be enforced on recorded data such as events, links, attributes etc.

This class does not enforce any limits itself. It only provides a way to read limits from env,
default values and from user provided arguments.

All limit arguments must be either a non-negative integer, ``None`` or ``LogLimits.UNSET``.

- All limit arguments are optional.
- If a limit argument is not set, the class will try to read its value from the corresponding
environment variable.
- If the environment variable is not set, the default value, if any, will be used.

Limit precedence:

- If a model specific limit is set, it will be used.
- Else if the corresponding global limit is set, it will be used.
- Else if the model specific limit has a default value, the default value will be used.
- Else if the global limit has a default value, the default value will be used.

Args:
max_attributes: Maximum number of attributes that can be added to a span, event, and link.
Environment variable: ``OTEL_ATTRIBUTE_COUNT_LIMIT``
Default: {_DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT}
max_attribute_length: Maximum length an attribute value can have. Values longer than
the specified length will be truncated.
"""

UNSET = -1
ocelotl marked this conversation as resolved.
Show resolved Hide resolved

def __init__(
self,
max_attributes: Optional[int] = None,
max_attribute_length: Optional[int] = None,
):

# attribute count
global_max_attributes = self._from_env_if_absent(
max_attributes, OTEL_ATTRIBUTE_COUNT_LIMIT
)
self.max_attributes = (
global_max_attributes
if global_max_attributes is not None
else _DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT
)

# attribute length
self.max_attribute_length = self._from_env_if_absent(
max_attribute_length,
OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT,
)

def __repr__(self):
return f"{type(self).__name__}(max_attributes={self.max_attributes}, max_attribute_length={self.max_attribute_length})"

@classmethod
def _from_env_if_absent(
cls, value: Optional[int], env_var: str, default: Optional[int] = None
) -> Optional[int]:
if value == cls.UNSET:
return None

err_msg = "{0} must be a non-negative integer but got {}"

# if no value is provided for the limit, try to load it from env
if value is None:
# return default value if env var is not set
if env_var not in environ:
return default

str_value = environ.get(env_var, "").strip().lower()
if str_value == _ENV_VALUE_UNSET:
return None

try:
value = int(str_value)
except ValueError:
raise ValueError(err_msg.format(env_var, str_value))

if value < 0:
raise ValueError(err_msg.format(env_var, value))
return value


_UnsetLogLimits = LogLimits(
max_attributes=LogLimits.UNSET,
max_attribute_length=LogLimits.UNSET,
)


class LogRecord(APILogRecord):
"""A LogRecord instance represents an event being logged.
Expand All @@ -66,6 +167,7 @@ def __init__(
body: Optional[Any] = None,
resource: Optional[Resource] = None,
attributes: Optional[Attributes] = None,
limits: Optional[LogLimits] = _UnsetLogLimits,
):
super().__init__(
**{
Expand All @@ -77,7 +179,12 @@ def __init__(
"severity_text": severity_text,
"severity_number": severity_number,
"body": body,
"attributes": attributes,
"attributes": BoundedAttributes(
maxlen=limits.max_attributes,
attributes=attributes if bool(attributes) else None,
nstawski marked this conversation as resolved.
Show resolved Hide resolved
immutable=False,
max_value_len=limits.max_attribute_length,
),
}
)
self.resource = resource
Expand All @@ -93,7 +200,9 @@ def to_json(self, indent=4) -> str:
"body": self.body,
"severity_number": repr(self.severity_number),
"severity_text": self.severity_text,
"attributes": self.attributes,
"attributes": dict(self.attributes)
if bool(self.attributes)
else None,
"timestamp": ns_to_iso_str(self.timestamp),
"trace_id": f"0x{format_trace_id(self.trace_id)}"
if self.trace_id is not None
Expand All @@ -109,6 +218,12 @@ def to_json(self, indent=4) -> str:
indent=indent,
)

@property
def dropped_attributes(self) -> int:
if self.attributes:
return self.attributes.dropped
return 0


class LogData:
"""Readable LogRecord data plus associated InstrumentationLibrary."""
Expand Down
2 changes: 2 additions & 0 deletions opentelemetry-sdk/tests/logs/test_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

from opentelemetry._logs import SeverityNumber
from opentelemetry._logs import get_logger as APIGetLogger
from opentelemetry.attributes import BoundedAttributes
from opentelemetry.sdk import trace
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
from opentelemetry.semconv.trace import SpanAttributes
Expand Down Expand Up @@ -91,6 +92,7 @@ def test_log_record_user_attributes(self):

self.assertIsNotNone(log_record)
self.assertEqual(log_record.attributes, {"http.status_code": 200})
self.assertTrue(isinstance(log_record.attributes, BoundedAttributes))

def test_log_record_exception(self):
"""Exception information will be included in attributes"""
Expand Down
40 changes: 40 additions & 0 deletions opentelemetry-sdk/tests/logs/test_log_limits.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import unittest

from opentelemetry.sdk._logs import LogLimits
from opentelemetry.sdk._logs._internal import (
_DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT,
)


class TestLogLimits(unittest.TestCase):
def test_log_limits_repr_unset(self):
expected = f"LogLimits(max_attributes={_DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT}, max_attribute_length=None)"
limits = str(LogLimits())

self.assertEqual(expected, limits)

def test_log_limits_max_attributes(self):
expected = 1
limits = LogLimits(max_attributes=1)

self.assertEqual(expected, limits.max_attributes)

def test_log_limits_max_attribute_length(self):
expected = 1
limits = LogLimits(max_attribute_length=1)

self.assertEqual(expected, limits.max_attribute_length)
67 changes: 66 additions & 1 deletion opentelemetry-sdk/tests/logs/test_log_record.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
import json
import unittest

from opentelemetry.sdk._logs import LogRecord
from opentelemetry.attributes import BoundedAttributes
from opentelemetry.sdk._logs import LogLimits, LogRecord


class TestLogRecord(unittest.TestCase):
Expand All @@ -39,3 +40,67 @@ def test_log_record_to_json(self):
body="a log line",
).to_json()
self.assertEqual(expected, actual)

def test_log_record_bounded_attributes(self):
attr = {"key": "value"}

result = LogRecord(timestamp=0, body="a log line", attributes=attr)

self.assertTrue(isinstance(result.attributes, BoundedAttributes))

def test_log_record_dropped_attributes_empty_limits(self):
attr = {"key": "value"}

result = LogRecord(timestamp=0, body="a log line", attributes=attr)

self.assertTrue(result.dropped_attributes == 0)

def test_log_record_dropped_attributes_set_limits_max_attribute(self):
attr = {"key": "value", "key2": "value2"}
limits = LogLimits(
max_attributes=1,
)

result = LogRecord(
timestamp=0, body="a log line", attributes=attr, limits=limits
)
self.assertTrue(result.dropped_attributes == 1)

def test_log_record_dropped_attributes_set_limits_max_attribute_length(
self,
):
attr = {"key": "value", "key2": "value2"}
expected = {"key": "v", "key2": "v"}
limits = LogLimits(
max_attribute_length=1,
)

result = LogRecord(
timestamp=0, body="a log line", attributes=attr, limits=limits
)
self.assertTrue(result.dropped_attributes == 0)
self.assertEqual(expected, result.attributes)

def test_log_record_dropped_attributes_set_limits(self):
attr = {"key": "value", "key2": "value2"}
expected = {"key2": "v"}
limits = LogLimits(
max_attributes=1,
max_attribute_length=1,
)

result = LogRecord(
timestamp=0, body="a log line", attributes=attr, limits=limits
)
self.assertTrue(result.dropped_attributes == 1)
self.assertEqual(expected, result.attributes)

def test_log_record_dropped_attributes_unset_limits(self):
attr = {"key": "value", "key2": "value2"}
limits = LogLimits()

result = LogRecord(
timestamp=0, body="a log line", attributes=attr, limits=limits
)
self.assertTrue(result.dropped_attributes == 0)
self.assertEqual(attr, result.attributes)