tpt_serverless 0.5.0 → 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 +56 -28
- 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,17 +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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
20
21
|
# INIT_START / START / END / REPORT before forwarding to Datadog.
|
|
21
22
|
# Defaults to false.
|
|
22
|
-
|
|
23
|
+
DD_FILTER_LIFECYCLE_LOGS: "true" # optional
|
|
23
24
|
events:
|
|
24
25
|
- cloudwatchLog: /aws/lambda/${self:service.name}-functionOne-${self:provider.stage}
|
|
25
26
|
- cloudwatchLog: /aws/lambda/${self:service.name}-functionTwo-${self:provider.stage}
|
|
@@ -42,14 +43,10 @@ class DatadogLogForwarder
|
|
|
42
43
|
class << self
|
|
43
44
|
# This handler receives CloudWatch log events, parses the events and forwards the extracted logs to
|
|
44
45
|
# Datadog.
|
|
45
|
-
#
|
|
46
|
-
# It performs a few helpful cleanup/prep functions as well.
|
|
47
46
|
def handler(event:, context:)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if dd_api_key.nil? || dd_api_key.strip.empty?
|
|
47
|
+
unless ENV['DD_API_KEY']
|
|
51
48
|
puts 'ERROR: DD_API_KEY is not set. Skipping log forwarding.'
|
|
52
|
-
return
|
|
49
|
+
return nil
|
|
53
50
|
end
|
|
54
51
|
|
|
55
52
|
raw_data = event.fetch('awslogs').fetch('data')
|
|
@@ -63,11 +60,13 @@ class DatadogLogForwarder
|
|
|
63
60
|
|
|
64
61
|
if message_type == 'CONTROL_MESSAGE'
|
|
65
62
|
puts 'skipping control message'
|
|
66
|
-
return
|
|
63
|
+
return nil
|
|
67
64
|
end
|
|
68
65
|
|
|
66
|
+
# Normalize first so lifecycle filtering works even if the raw message didn't have a timestamp
|
|
69
67
|
normalized_log_events = normalize_messages(log_events)
|
|
70
68
|
|
|
69
|
+
# Drop AWS Lambda platform lifecycle boilerplate
|
|
71
70
|
filtered_log_events =
|
|
72
71
|
if should_filter_lifecycle_logs?
|
|
73
72
|
normalized_log_events.reject { |e| lambda_lifecycle_line?(e[:message]) }
|
|
@@ -80,7 +79,7 @@ class DatadogLogForwarder
|
|
|
80
79
|
region = 'unknown'
|
|
81
80
|
account_id = 'unknown'
|
|
82
81
|
|
|
83
|
-
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?
|
|
84
83
|
arn_parts = context.invoked_function_arn.split(':')
|
|
85
84
|
region = arn_parts[3] if arn_parts[3]
|
|
86
85
|
account_id = arn_parts[4] if arn_parts[4]
|
|
@@ -89,13 +88,22 @@ class DatadogLogForwarder
|
|
|
89
88
|
function_name = log_group&.split('/')&.last
|
|
90
89
|
function_arn = "arn:aws:lambda:#{region}:#{account_id}:function:#{function_name}"
|
|
91
90
|
|
|
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
|
+
|
|
92
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
|
+
)
|
|
100
|
+
|
|
93
101
|
{
|
|
94
102
|
message: log_event[:message],
|
|
95
|
-
ddsource: ENV['
|
|
96
|
-
service: ENV['
|
|
103
|
+
ddsource: ENV['DD_SOURCE'] || 'cwl-aws-lambda',
|
|
104
|
+
service: ENV['DD_SERVICE'] || ENV['APP_SERVICE_NAME'] || function_name,
|
|
97
105
|
hostname: function_arn,
|
|
98
|
-
ddtags: [
|
|
106
|
+
ddtags: (ddtags_array + ["region:#{region}"]).reject { |t| t.nil? || t.empty? }.join(','),
|
|
99
107
|
aws: {
|
|
100
108
|
awslogs: {
|
|
101
109
|
logGroup: log_group,
|
|
@@ -107,7 +115,7 @@ class DatadogLogForwarder
|
|
|
107
115
|
end
|
|
108
116
|
|
|
109
117
|
# If we filtered everything out, do nothing.
|
|
110
|
-
return if normalized_data.empty?
|
|
118
|
+
return nil if normalized_data.empty?
|
|
111
119
|
|
|
112
120
|
begin
|
|
113
121
|
send_json_to_datadog(normalized_data.to_json)
|
|
@@ -120,6 +128,26 @@ class DatadogLogForwarder
|
|
|
120
128
|
nil
|
|
121
129
|
end
|
|
122
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
|
+
|
|
123
151
|
private
|
|
124
152
|
|
|
125
153
|
def normalize_messages(log_events)
|
|
@@ -128,7 +156,7 @@ class DatadogLogForwarder
|
|
|
128
156
|
message = log_event.fetch('message')
|
|
129
157
|
timestamp_ms = Integer(log_event.fetch('timestamp'))
|
|
130
158
|
|
|
131
|
-
# Ensure the message starts with an ISO timestamp
|
|
159
|
+
# Ensure the message starts with an ISO timestamp
|
|
132
160
|
if TIME_REGEX !~ message
|
|
133
161
|
time_string = Time.at(timestamp_ms / 1000.0).utc.strftime('%Y-%m-%dT%H:%M:%S.%LZ')
|
|
134
162
|
message = "#{time_string} #{message}"
|
|
@@ -142,7 +170,7 @@ class DatadogLogForwarder
|
|
|
142
170
|
end
|
|
143
171
|
|
|
144
172
|
def should_filter_lifecycle_logs?
|
|
145
|
-
truthy_env?(ENV['
|
|
173
|
+
truthy_env?(ENV['DD_FILTER_LIFECYCLE_LOGS'])
|
|
146
174
|
end
|
|
147
175
|
|
|
148
176
|
def truthy_env?(value)
|
|
@@ -157,8 +185,8 @@ class DatadogLogForwarder
|
|
|
157
185
|
end
|
|
158
186
|
|
|
159
187
|
def send_json_to_datadog(data)
|
|
160
|
-
|
|
161
|
-
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")
|
|
162
190
|
|
|
163
191
|
response = Net::HTTP.start(datadog_url.host, datadog_url.port, use_ssl: datadog_url.scheme == 'https') do |http|
|
|
164
192
|
http.open_timeout = 3
|
|
@@ -168,7 +196,7 @@ class DatadogLogForwarder
|
|
|
168
196
|
datadog_url,
|
|
169
197
|
{
|
|
170
198
|
'Content-Type' => 'application/json',
|
|
171
|
-
'DD-API-KEY' => ENV['DD_API_KEY']
|
|
199
|
+
'DD-API-KEY' => ENV['DD_API_KEY']
|
|
172
200
|
}
|
|
173
201
|
)
|
|
174
202
|
|
|
@@ -176,8 +204,8 @@ class DatadogLogForwarder
|
|
|
176
204
|
http.request(req)
|
|
177
205
|
end
|
|
178
206
|
|
|
179
|
-
unless [
|
|
180
|
-
raise "Datadog upload failed
|
|
207
|
+
unless %w[200 202].include?(response.code)
|
|
208
|
+
raise "Datadog upload failed with status #{response.code}: #{response.body}"
|
|
181
209
|
end
|
|
182
210
|
|
|
183
211
|
response
|