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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f873d98101203807387f9ad586751351d2a3144b62069ddb2543a171f25c31b3
4
- data.tar.gz: 817156c5725a80e393e778acb2cf5a1e73bc28718c1a39d07282e0a7776120f3
3
+ metadata.gz: eaf679b74f62f413493556247cd9111725a55dfc8346363b30700dd4f44e32a1
4
+ data.tar.gz: 7d0d3e2f07fc6c8d10cc0e7eb801506fe2c7e0f3066361679058643d07221d0a
5
5
  SHA512:
6
- metadata.gz: 543cf9dad7a1c194148c1a46a9daa50af26b67fe78ae302c3c7fb412a4600dd1564b188ecbfb0ddab173dcda6f63bb984be28ad756886987c76e1ca1df4446b5
7
- data.tar.gz: 8d9208dde57f413777eb9ed073feca37ec0b8e18578d1f90ade3c1c2cb1230018b724be92b23fd9567972ad4c9b03f14152f702beb6b6d198ac83be67732bc60
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: ${self:custom.datadogApiKey}
12
+ DD_API_KEY: ${ssm:/path/to/DD_API_KEY}
13
13
  # Optional:
14
- DATADOG_SITE: datadoghq.com # default
15
- DATADOG_SOURCE: cwl-aws-lambda # default
16
- DATADOG_SERVICE: ${self:service.name} # optional
17
- DATADOG_TAGS: stage:${self:provider.stage},team:platform # optional
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
- dd_api_key = ENV['DD_API_KEY']
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
- puts "message_count=#{log_events.length}"
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
- normalized_data = log_events.map do |log_event|
76
- normalized_msg = normalize_messages([log_event]).first
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: normalized_msg,
80
- ddsource: ENV['DATADOG_SOURCE'] || 'cwl-aws-lambda',
81
- service: ENV['DATADOG_SERVICE'] || function_name,
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: [ENV['DATADOG_TAGS'], "region:#{region}"].compact.reject(&:empty?).join(','),
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['id']
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
- timestamp = Integer(log_event.fetch('timestamp'))
157
+ timestamp_ms = Integer(log_event.fetch('timestamp'))
111
158
 
112
- # Ensure the message starts with a time.
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(timestamp/1000.0).utc.strftime('%Y-%m-%dT%H:%M:%S.%LZ')
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
- datadog_site = ENV['DATADOG_SITE'] || 'datadoghq.com'
126
- datadog_url = URI("https://http-intake.logs.#{datadog_site}/api/v2/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 ['200', '202'].include?(response.code)
146
- raise "Datadog upload failed: #{response.code} - #{response.body}"
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
@@ -1,3 +1,3 @@
1
1
  module TptServerless
2
- VERSION = "0.4.2"
2
+ VERSION = "0.6.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tpt_serverless
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.2
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - TpT