1
1
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
2
# SPDX-License-Identifier: Apache-2.0
3
3
4
- from typing import Dict , Optional
4
+ import gzip
5
+ import logging
6
+ from io import BytesIO
7
+ from time import sleep
8
+ from typing import Dict , Optional , Sequence
9
+
10
+ import requests
5
11
6
12
from amazon .opentelemetry .distro .exporter .otlp .aws .common .aws_auth_session import AwsAuthSession
13
+ from opentelemetry .exporter .otlp .proto .common ._internal import (
14
+ _create_exp_backoff_generator ,
15
+ )
16
+ from opentelemetry .exporter .otlp .proto .common ._log_encoder import encode_logs
7
17
from opentelemetry .exporter .otlp .proto .http import Compression
8
18
from opentelemetry .exporter .otlp .proto .http ._log_exporter import OTLPLogExporter
19
+ from opentelemetry .sdk ._logs import (
20
+ LogData ,
21
+ )
22
+ from opentelemetry .sdk ._logs .export import (
23
+ LogExportResult ,
24
+ )
25
+
26
+ _logger = logging .getLogger (__name__ )
9
27
10
28
11
29
class OTLPAwsLogExporter (OTLPLogExporter ):
30
+ _LARGE_LOG_HEADER = "x-aws-truncatable-fields"
31
+ _LARGE_GEN_AI_LOG_PATH_HEADER = (
32
+ "\\ $['resourceLogs'][0]['scopeLogs'][0]['logRecords'][0]['body']"
33
+ "['kvlistValue']['values'][*]['value']['kvlistValue']['values'][*]"
34
+ "['value']['arrayValue']['values'][*]['kvlistValue']['values'][*]"
35
+ "['value']['stringValue']"
36
+ )
37
+ _RETRY_AFTER_HEADER = "Retry-After" # https://opentelemetry.io/docs/specs/otlp/#otlphttp-throttling
38
+
12
39
def __init__ (
13
40
self ,
14
41
endpoint : Optional [str ] = None ,
@@ -18,6 +45,7 @@ def __init__(
18
45
headers : Optional [Dict [str , str ]] = None ,
19
46
timeout : Optional [int ] = None ,
20
47
):
48
+ self ._gen_ai_log_flag = False
21
49
self ._aws_region = None
22
50
23
51
if endpoint :
@@ -34,3 +62,134 @@ def __init__(
34
62
compression = Compression .Gzip ,
35
63
session = AwsAuthSession (aws_region = self ._aws_region , service = "logs" ),
36
64
)
65
+
66
+ # https://github.com/open-telemetry/opentelemetry-python/blob/main/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py#L167
67
+ def export (self , batch : Sequence [LogData ]) -> LogExportResult :
68
+ """
69
+ Exports the given batch of OTLP log data.
70
+ Behaviors of how this export will work -
71
+
72
+ 1. Always compresses the serialized data into gzip before sending.
73
+
74
+ 2. If self._gen_ai_log_flag is enabled, the log data is > 1 MB a
75
+ and the assumption is that the log is a normalized gen.ai LogEvent.
76
+ - inject the {LARGE_LOG_HEADER} into the header.
77
+
78
+ 3. Retry behavior is now the following:
79
+ - if the response contains a status code that is retryable and the response contains Retry-After in its
80
+ headers, the serialized data will be exported after that set delay
81
+
82
+ - if the response does not contain that Retry-After header, default back to the current iteration of the
83
+ exponential backoff delay
84
+ """
85
+
86
+ if self ._shutdown :
87
+ _logger .warning ("Exporter already shutdown, ignoring batch" )
88
+ return LogExportResult .FAILURE
89
+
90
+ serialized_data = encode_logs (batch ).SerializeToString ()
91
+
92
+ gzip_data = BytesIO ()
93
+ with gzip .GzipFile (fileobj = gzip_data , mode = "w" ) as gzip_stream :
94
+ gzip_stream .write (serialized_data )
95
+
96
+ data = gzip_data .getvalue ()
97
+
98
+ backoff = _create_exp_backoff_generator (max_value = self ._MAX_RETRY_TIMEOUT )
99
+
100
+ while True :
101
+ resp = self ._send (data )
102
+
103
+ if resp .ok :
104
+ return LogExportResult .SUCCESS
105
+
106
+ if not self ._retryable (resp ):
107
+ _logger .error (
108
+ "Failed to export logs batch code: %s, reason: %s" ,
109
+ resp .status_code ,
110
+ resp .text ,
111
+ )
112
+ self ._gen_ai_log_flag = False
113
+ return LogExportResult .FAILURE
114
+
115
+ # https://opentelemetry.io/docs/specs/otlp/#otlphttp-throttling
116
+ maybe_retry_after = resp .headers .get (self ._RETRY_AFTER_HEADER , None )
117
+
118
+ # Set the next retry delay to the value of the Retry-After response in the headers.
119
+ # If Retry-After is not present in the headers, default to the next iteration of the
120
+ # exponential backoff strategy.
121
+
122
+ delay = self ._parse_retryable_header (maybe_retry_after )
123
+
124
+ if delay == - 1 :
125
+ delay = next (backoff , self ._MAX_RETRY_TIMEOUT )
126
+
127
+ if delay == self ._MAX_RETRY_TIMEOUT :
128
+ _logger .error (
129
+ "Transient error %s encountered while exporting logs batch. "
130
+ "No Retry-After header found and all backoff retries exhausted. "
131
+ "Logs will not be exported." ,
132
+ resp .reason ,
133
+ )
134
+ self ._gen_ai_log_flag = False
135
+ return LogExportResult .FAILURE
136
+
137
+ _logger .warning (
138
+ "Transient error %s encountered while exporting logs batch, retrying in %ss." ,
139
+ resp .reason ,
140
+ delay ,
141
+ )
142
+
143
+ sleep (delay )
144
+
145
+ def set_gen_ai_log_flag (self ):
146
+ """
147
+ Sets a flag that indicates the current log batch contains
148
+ a generative AI log record that exceeds the CloudWatch Logs size limit (1MB).
149
+ """
150
+ self ._gen_ai_log_flag = True
151
+
152
+ def _send (self , serialized_data : bytes ):
153
+ try :
154
+ response = self ._session .post (
155
+ url = self ._endpoint ,
156
+ headers = {self ._LARGE_LOG_HEADER : self ._LARGE_GEN_AI_LOG_PATH_HEADER } if self ._gen_ai_log_flag else None ,
157
+ data = serialized_data ,
158
+ verify = self ._certificate_file ,
159
+ timeout = self ._timeout ,
160
+ cert = self ._client_cert ,
161
+ )
162
+ return response
163
+ except ConnectionError :
164
+ response = self ._session .post (
165
+ url = self ._endpoint ,
166
+ headers = {self ._LARGE_LOG_HEADER : self ._LARGE_GEN_AI_LOG_PATH_HEADER } if self ._gen_ai_log_flag else None ,
167
+ data = serialized_data ,
168
+ verify = self ._certificate_file ,
169
+ timeout = self ._timeout ,
170
+ cert = self ._client_cert ,
171
+ )
172
+ return response
173
+
174
+ @staticmethod
175
+ def _retryable (resp : requests .Response ) -> bool :
176
+ """
177
+ Is it a retryable response?
178
+ """
179
+
180
+ return resp .status_code in (429 , 503 ) or OTLPLogExporter ._retryable (resp )
181
+
182
+ @staticmethod
183
+ def _parse_retryable_header (retry_header : Optional [str ]) -> float :
184
+ """
185
+ Converts the given retryable header into a delay in seconds, returns -1 if there's no header
186
+ or error with the parsing
187
+ """
188
+ if not retry_header :
189
+ return - 1
190
+
191
+ try :
192
+ val = float (retry_header )
193
+ return val if val >= 0 else - 1
194
+ except ValueError :
195
+ return - 1
0 commit comments