1919import logging
2020import threading
2121import traceback
22+ from os import environ
2223from time import time_ns
2324from typing import Any , Callable , Optional , Tuple , Union
2425
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+ )
3440from opentelemetry .sdk .resources import Resource
3541from opentelemetry .sdk .util import ns_to_iso_str
3642from opentelemetry .sdk .util .instrumentation import InstrumentationScope
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
49150class 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
113228class LogData :
114229 """Readable LogRecord data plus associated InstrumentationLibrary."""
0 commit comments