19
19
import logging
20
20
import threading
21
21
import traceback
22
+ from os import environ
22
23
from time import time_ns
23
24
from typing import Any , Callable , Optional , Tuple , Union
24
25
31
32
get_logger_provider ,
32
33
std_to_otel ,
33
34
)
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
+ )
34
40
from opentelemetry .sdk .resources import Resource
35
41
from opentelemetry .sdk .util import ns_to_iso_str
36
42
from opentelemetry .sdk .util .instrumentation import InstrumentationScope
45
51
46
52
_logger = logging .getLogger (__name__ )
47
53
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
+
48
149
49
150
class LogRecord (APILogRecord ):
50
151
"""A LogRecord instance represents an event being logged.
@@ -66,6 +167,7 @@ def __init__(
66
167
body : Optional [Any ] = None ,
67
168
resource : Optional [Resource ] = None ,
68
169
attributes : Optional [Attributes ] = None ,
170
+ limits : Optional [LogLimits ] = _UnsetLogLimits ,
69
171
):
70
172
super ().__init__ (
71
173
** {
@@ -77,7 +179,12 @@ def __init__(
77
179
"severity_text" : severity_text ,
78
180
"severity_number" : severity_number ,
79
181
"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
+ ),
81
188
}
82
189
)
83
190
self .resource = resource
@@ -93,7 +200,9 @@ def to_json(self, indent=4) -> str:
93
200
"body" : self .body ,
94
201
"severity_number" : repr (self .severity_number ),
95
202
"severity_text" : self .severity_text ,
96
- "attributes" : self .attributes ,
203
+ "attributes" : dict (self .attributes )
204
+ if bool (self .attributes )
205
+ else None ,
97
206
"timestamp" : ns_to_iso_str (self .timestamp ),
98
207
"trace_id" : f"0x{ format_trace_id (self .trace_id )} "
99
208
if self .trace_id is not None
@@ -109,6 +218,12 @@ def to_json(self, indent=4) -> str:
109
218
indent = indent ,
110
219
)
111
220
221
+ @property
222
+ def dropped_attributes (self ) -> int :
223
+ if self .attributes :
224
+ return self .attributes .dropped
225
+ return 0
226
+
112
227
113
228
class LogData :
114
229
"""Readable LogRecord data plus associated InstrumentationLibrary."""
0 commit comments