tpt_serverless 0.4.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/tpt_serverless/datadog_log_forwarder.rb +95 -33
- data/lib/tpt_serverless/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: eaf679b74f62f413493556247cd9111725a55dfc8346363b30700dd4f44e32a1
|
|
4
|
+
data.tar.gz: 7d0d3e2f07fc6c8d10cc0e7eb801506fe2c7e0f3066361679058643d07221d0a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9d52cc7aab4a205c3041575bb0f2ada2815ecfee469c3b19e4f627716f315582cf7ebccd6183ff322f5bc5a85d12fd926c7f3d85ae66fcee447ff505648344a1
|
|
7
|
+
data.tar.gz: 533fcc9a0611b56650c5ac97624fca8620eb3c6013b88196b18e7b4c64381daa7375ae83162c39c95dab0ced51b380c8414062c7568df00013e87497d6b0dc08
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
=begin
|
|
2
|
-
Receives logs from CloudWatch and forwards them to Datadog.
|
|
2
|
+
Receives logs from CloudWatch and forwards them to Datadog Logs intake.
|
|
3
3
|
|
|
4
4
|
Example of how to add this to your `serverless.yml`:
|
|
5
5
|
|
|
@@ -9,12 +9,18 @@ Example of how to add this to your `serverless.yml`:
|
|
|
9
9
|
handler: /opt/ruby/3.4.0/gems/tpt_serverless-X.X.X/lib/tpt_serverless/datadog_log_forwarder.DatadogLogForwarder.handler
|
|
10
10
|
timeout: 10 # seconds
|
|
11
11
|
environment:
|
|
12
|
-
DD_API_KEY: ${
|
|
12
|
+
DD_API_KEY: ${ssm:/path/to/DD_API_KEY}
|
|
13
13
|
# Optional:
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
DD_SITE: datadoghq.com # default
|
|
15
|
+
DD_SOURCE: cwl-aws-lambda # default
|
|
16
|
+
DD_ENV: ${self:provider.stage} # default
|
|
17
|
+
DD_SERVICE: ${self:service.name} # optional
|
|
18
|
+
DD_TAGS: stage:${self:provider.stage},team:platform # optional
|
|
19
|
+
|
|
20
|
+
# If true, filters AWS Lambda platform lifecycle boilerplate logs like
|
|
21
|
+
# INIT_START / START / END / REPORT before forwarding to Datadog.
|
|
22
|
+
# Defaults to false.
|
|
23
|
+
DD_FILTER_LIFECYCLE_LOGS: "true" # optional
|
|
18
24
|
events:
|
|
19
25
|
- cloudwatchLog: /aws/lambda/${self:service.name}-functionOne-${self:provider.stage}
|
|
20
26
|
- cloudwatchLog: /aws/lambda/${self:service.name}-functionTwo-${self:provider.stage}
|
|
@@ -31,17 +37,16 @@ require 'uri'
|
|
|
31
37
|
class DatadogLogForwarder
|
|
32
38
|
TIME_REGEX ||= /^20\d\d-\d\d-\d\dT\d\d:\d\d:\d\d\.\d\d\dZ/
|
|
33
39
|
|
|
40
|
+
# Matches AWS Lambda platform lifecycle lines that are usually noisy in Datadog
|
|
41
|
+
LAMBDA_LIFECYCLE_AFTER_TIMESTAMP_REGEX ||= /\A(?:INIT_START\b|START RequestId:|END RequestId:|REPORT RequestId:)/
|
|
42
|
+
|
|
34
43
|
class << self
|
|
35
44
|
# This handler receives CloudWatch log events, parses the events and forwards the extracted logs to
|
|
36
45
|
# Datadog.
|
|
37
|
-
#
|
|
38
|
-
# It performs a few helpful cleanup/prep functions as well.
|
|
39
46
|
def handler(event:, context:)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if dd_api_key.nil? || dd_api_key.strip.empty?
|
|
47
|
+
unless ENV['DD_API_KEY']
|
|
43
48
|
puts 'ERROR: DD_API_KEY is not set. Skipping log forwarding.'
|
|
44
|
-
return
|
|
49
|
+
return nil
|
|
45
50
|
end
|
|
46
51
|
|
|
47
52
|
raw_data = event.fetch('awslogs').fetch('data')
|
|
@@ -55,15 +60,26 @@ class DatadogLogForwarder
|
|
|
55
60
|
|
|
56
61
|
if message_type == 'CONTROL_MESSAGE'
|
|
57
62
|
puts 'skipping control message'
|
|
58
|
-
return
|
|
63
|
+
return nil
|
|
59
64
|
end
|
|
60
65
|
|
|
61
|
-
|
|
66
|
+
# Normalize first so lifecycle filtering works even if the raw message didn't have a timestamp
|
|
67
|
+
normalized_log_events = normalize_messages(log_events)
|
|
68
|
+
|
|
69
|
+
# Drop AWS Lambda platform lifecycle boilerplate
|
|
70
|
+
filtered_log_events =
|
|
71
|
+
if should_filter_lifecycle_logs?
|
|
72
|
+
normalized_log_events.reject { |e| lambda_lifecycle_line?(e[:message]) }
|
|
73
|
+
else
|
|
74
|
+
normalized_log_events
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
puts "message_count=#{log_events.length} forwarded_count=#{filtered_log_events.length}"
|
|
62
78
|
|
|
63
79
|
region = 'unknown'
|
|
64
80
|
account_id = 'unknown'
|
|
65
81
|
|
|
66
|
-
if context&.respond_to?(:invoked_function_arn) && !context.invoked_function_arn.empty?
|
|
82
|
+
if context&.respond_to?(:invoked_function_arn) && !context.invoked_function_arn.to_s.empty?
|
|
67
83
|
arn_parts = context.invoked_function_arn.split(':')
|
|
68
84
|
region = arn_parts[3] if arn_parts[3]
|
|
69
85
|
account_id = arn_parts[4] if arn_parts[4]
|
|
@@ -72,25 +88,35 @@ class DatadogLogForwarder
|
|
|
72
88
|
function_name = log_group&.split('/')&.last
|
|
73
89
|
function_arn = "arn:aws:lambda:#{region}:#{account_id}:function:#{function_name}"
|
|
74
90
|
|
|
75
|
-
|
|
76
|
-
|
|
91
|
+
name_parts = (function_name || '').split('-')
|
|
92
|
+
# Extract the stage (everything from index 2 onwards e.g., "dev" or "dev-ivan")
|
|
93
|
+
extracted_env = name_parts.length > 2 ? name_parts[2..-1].join('-') : 'unknown'
|
|
94
|
+
|
|
95
|
+
normalized_data = filtered_log_events.map do |log_event|
|
|
96
|
+
ddtags_array = get_datadog_reserved_tags(
|
|
97
|
+
fallback_env: extracted_env,
|
|
98
|
+
fallback_service: function_name
|
|
99
|
+
)
|
|
77
100
|
|
|
78
101
|
{
|
|
79
|
-
message:
|
|
80
|
-
ddsource: ENV['
|
|
81
|
-
service: ENV['
|
|
102
|
+
message: log_event[:message],
|
|
103
|
+
ddsource: ENV['DD_SOURCE'] || 'cwl-aws-lambda',
|
|
104
|
+
service: ENV['DD_SERVICE'] || ENV['APP_SERVICE_NAME'] || function_name,
|
|
82
105
|
hostname: function_arn,
|
|
83
|
-
ddtags: [
|
|
106
|
+
ddtags: (ddtags_array + ["region:#{region}"]).reject { |t| t.nil? || t.empty? }.join(','),
|
|
84
107
|
aws: {
|
|
85
108
|
awslogs: {
|
|
86
109
|
logGroup: log_group,
|
|
87
110
|
logStream: log_stream
|
|
88
111
|
}
|
|
89
112
|
},
|
|
90
|
-
id: log_event[
|
|
113
|
+
id: log_event[:id]
|
|
91
114
|
}
|
|
92
115
|
end
|
|
93
116
|
|
|
117
|
+
# If we filtered everything out, do nothing.
|
|
118
|
+
return nil if normalized_data.empty?
|
|
119
|
+
|
|
94
120
|
begin
|
|
95
121
|
send_json_to_datadog(normalized_data.to_json)
|
|
96
122
|
puts "Datadog upload successful: sent #{normalized_data.length} log(s) for #{function_name}"
|
|
@@ -102,28 +128,65 @@ class DatadogLogForwarder
|
|
|
102
128
|
nil
|
|
103
129
|
end
|
|
104
130
|
|
|
131
|
+
# Computes the Datadog unified service tagging reserved tags from environment
|
|
132
|
+
# variables, using the following precedence:
|
|
133
|
+
#
|
|
134
|
+
# - env: DD_ENV → APP_STAGE
|
|
135
|
+
# - service: DD_SERVICE → APP_SERVICE_NAME
|
|
136
|
+
# - version: DD_VERSION
|
|
137
|
+
# - preset: DD_TAGS
|
|
138
|
+
def get_datadog_reserved_tags(fallback_env: nil, fallback_service: nil)
|
|
139
|
+
env = ENV['DD_ENV'] || ENV['APP_STAGE'] || fallback_env
|
|
140
|
+
service = ENV['DD_SERVICE'] || ENV['APP_SERVICE_NAME'] || fallback_service
|
|
141
|
+
version = ENV['DD_VERSION']
|
|
142
|
+
preset = ENV['DD_TAGS'] ? ENV['DD_TAGS'].split(',').map(&:strip).reject(&:empty?) : []
|
|
143
|
+
|
|
144
|
+
tags = []
|
|
145
|
+
tags << "env:#{env}" if env
|
|
146
|
+
tags << "service:#{service}" if service
|
|
147
|
+
tags << "version:#{version}" if version
|
|
148
|
+
tags + preset
|
|
149
|
+
end
|
|
150
|
+
|
|
105
151
|
private
|
|
106
152
|
|
|
107
153
|
def normalize_messages(log_events)
|
|
108
154
|
log_events.map do |log_event|
|
|
155
|
+
id = log_event.fetch('id')
|
|
109
156
|
message = log_event.fetch('message')
|
|
110
|
-
|
|
157
|
+
timestamp_ms = Integer(log_event.fetch('timestamp'))
|
|
111
158
|
|
|
112
|
-
# Ensure the message starts with
|
|
113
|
-
# E.g. START/END/REPORT log events don't.
|
|
159
|
+
# Ensure the message starts with an ISO timestamp
|
|
114
160
|
if TIME_REGEX !~ message
|
|
115
|
-
time_string = Time.at(
|
|
161
|
+
time_string = Time.at(timestamp_ms / 1000.0).utc.strftime('%Y-%m-%dT%H:%M:%S.%LZ')
|
|
116
162
|
message = "#{time_string} #{message}"
|
|
117
163
|
end
|
|
118
164
|
|
|
119
165
|
# AWS replaces newlines with carriage returns in Lambda logs
|
|
120
|
-
message.gsub(/\r(?!\n)/, "\n")
|
|
166
|
+
message = message.gsub(/\r(?!\n)/, "\n")
|
|
167
|
+
|
|
168
|
+
{ id: id, message: message }
|
|
121
169
|
end
|
|
122
170
|
end
|
|
123
171
|
|
|
172
|
+
def should_filter_lifecycle_logs?
|
|
173
|
+
truthy_env?(ENV['DD_FILTER_LIFECYCLE_LOGS'])
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def truthy_env?(value)
|
|
177
|
+
return false if value.nil?
|
|
178
|
+
%w[1 true yes on].include?(value.strip.downcase)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def lambda_lifecycle_line?(message_with_timestamp)
|
|
182
|
+
# Strip the leading ISO timestamp and match on the remainder.
|
|
183
|
+
after_timestamp = message_with_timestamp.sub(TIME_REGEX, '').lstrip
|
|
184
|
+
LAMBDA_LIFECYCLE_AFTER_TIMESTAMP_REGEX.match?(after_timestamp)
|
|
185
|
+
end
|
|
186
|
+
|
|
124
187
|
def send_json_to_datadog(data)
|
|
125
|
-
|
|
126
|
-
datadog_url = URI("https://http-intake.logs.#{
|
|
188
|
+
dd_site = ENV['DD_SITE'] || 'datadoghq.com'
|
|
189
|
+
datadog_url = URI("https://http-intake.logs.#{dd_site}/api/v2/logs")
|
|
127
190
|
|
|
128
191
|
response = Net::HTTP.start(datadog_url.host, datadog_url.port, use_ssl: datadog_url.scheme == 'https') do |http|
|
|
129
192
|
http.open_timeout = 3
|
|
@@ -133,17 +196,16 @@ class DatadogLogForwarder
|
|
|
133
196
|
datadog_url,
|
|
134
197
|
{
|
|
135
198
|
'Content-Type' => 'application/json',
|
|
136
|
-
'DD-API-KEY' => ENV['DD_API_KEY']
|
|
199
|
+
'DD-API-KEY' => ENV['DD_API_KEY']
|
|
137
200
|
}
|
|
138
201
|
)
|
|
139
202
|
|
|
140
203
|
req.body = data
|
|
141
|
-
|
|
142
204
|
http.request(req)
|
|
143
205
|
end
|
|
144
206
|
|
|
145
|
-
unless [
|
|
146
|
-
raise "Datadog upload failed
|
|
207
|
+
unless %w[200 202].include?(response.code)
|
|
208
|
+
raise "Datadog upload failed with status #{response.code}: #{response.body}"
|
|
147
209
|
end
|
|
148
210
|
|
|
149
211
|
response
|