tpt_serverless 0.2.1 → 0.4.1

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: 4e40167211120d2403b0f6c91770b43384ff2d9781cb0282e2fad5c34f0a1459
4
- data.tar.gz: 790516d5026b0a26123d929ddab13424e46ce56e27eb7de1dcd05d135cd7dba5
3
+ metadata.gz: bf37a0d47bf872ffbb9633925e4ddb37d6bfc38a570f84f75f3f61f492b8f100
4
+ data.tar.gz: 1038ef65a0ed86399a412fac44fcdb4ef037abfc491307dc3d92554f3a474424
5
5
  SHA512:
6
- metadata.gz: 4731b9cf978c054b73779578280757be12d8e7ecc08305bdf5e87a6b3d272ac3f6101eddd6fdf3f9d60371126b227603a6eb218156d73a05e2856a72fcf83d22
7
- data.tar.gz: f0f048fb5a0b0f02b34c2a06e5a157b8727ae13cfdb69c8fa2a3ebced4132c939e4a3b833a7d438c800fc00830e58f31279c1b2d75086c1ba6399a5694b51711
6
+ metadata.gz: 89ff6fca675780c16c77fbcbe976fcbbc67daeeaf01513a15f674ec97344047f8084fc7ae61ac62ec5c19f5c69f47830f319a00ffd578e433a9ff8950ef3ca20
7
+ data.tar.gz: d4c7ec90b2c42414f43391f1f7af4bab4f76f55521f3c42fd9810247bd1cdc4e04a06bf61861de57cf790bf4c76fb7dc1a20fcc0819c3fd5e19bb534cc99303a
@@ -0,0 +1,60 @@
1
+ require "dogapi"
2
+ require "timeout"
3
+
4
+ # TODO: Look into using Lambda Extensions for Datadog (and logging) instead to avoid blocking or
5
+ # affecting our application code.
6
+ module TptServerless
7
+ class Datadog
8
+ API_KEY = (ENV["DD_API_KEY"] && !ENV["DD_API_KEY"].strip.empty?) ? ENV["DD_API_KEY"] : nil
9
+ DATADOG_TIMEOUT = 2 # seconds
10
+ OUR_TIMEOUT = 3 # seconds. slightly longer than DATADOG_TIMEOUT.
11
+
12
+ def initialize(api_key: API_KEY, datadog_timeout: DATADOG_TIMEOUT, our_timeout: OUR_TIMEOUT)
13
+ @app_name = ENV.fetch("APP_SERVICE_NAME", "unknownService")
14
+ @stage = ENV.fetch("APP_STAGE", "unknown-stage")
15
+ @host = ENV.fetch("DD_HOSTNAME", nil)
16
+ @our_timeout = our_timeout
17
+
18
+ @client = if api_key
19
+ Dogapi::Client.new(
20
+ api_key, # api key
21
+ nil, # application_key, not required here
22
+ @host, # host
23
+ nil, # device
24
+ true, # silent
25
+ datadog_timeout, # timeout in seconds
26
+ )
27
+ end
28
+ end
29
+
30
+ def inc(metric, amount: 1, tags: [])
31
+ # prefix the metric with the app name
32
+ metric = "#{@app_name}.#{metric}"
33
+
34
+ tags += ["env:#{@stage}"]
35
+ # change characters not supported by datadog to underscores
36
+ tags.map! { |t| t.gsub(%r([^\w:./-]), "_") }
37
+
38
+ # log the metric so that we can correlate between metrics & logs more easily
39
+ logger.info("metric=#{metric} amount=#{amount} tags=#{tags.join(",")}")
40
+
41
+ if @client
42
+ begin
43
+ Timeout.timeout(@our_timeout) do
44
+ @client.emit_point(metric, amount, type: "count", tags: tags, host: "aws.lambda")
45
+ end
46
+ rescue Exception => e
47
+ logger.info("WARNING: Failed to emit metric to datadog: #{e.message}")
48
+ end
49
+ end
50
+
51
+ nil
52
+ end
53
+
54
+ private
55
+
56
+ def logger
57
+ ServerlessLogger.logger
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,152 @@
1
+ =begin
2
+ Receives logs from CloudWatch and forwards them to Datadog.
3
+
4
+ Example of how to add this to your `serverless.yml`:
5
+
6
+ forwardLogs:
7
+ name: ${self:service.name}-forwardLogs-${self:provider.stage}
8
+ # NOTE: Ensure that you update the path below if you update the tpt_serverless gem
9
+ handler: /opt/ruby/3.4.0/gems/tpt_serverless-X.X.X/lib/tpt_serverless/datadog_log_forwarder.DatadogLogForwarder.handler
10
+ timeout: 10 # seconds
11
+ environment:
12
+ DD_API_KEY: ${self:custom.datadogApiKey}
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
18
+ events:
19
+ - cloudwatchLog: /aws/lambda/${self:service.name}-functionOne-${self:provider.stage}
20
+ - cloudwatchLog: /aws/lambda/${self:service.name}-functionTwo-${self:provider.stage}
21
+ …etc…
22
+ reservedConcurrency: ${self:custom.datadogConcurrency}
23
+ =end
24
+
25
+ require 'zlib'
26
+ require 'base64'
27
+ require 'json'
28
+ require 'net/http'
29
+ require 'uri'
30
+
31
+ TIME_REGEX = /^20\d\d-\d\d-\d\dT\d\d:\d\d:\d\d\.\d\d\dZ/
32
+
33
+ class DatadogLogForwarder
34
+ class << self
35
+ # This handler receives CloudWatch log events, parses the events and forwards the extracted logs to
36
+ # Datadog.
37
+ #
38
+ # It performs a few helpful cleanup/prep functions as well.
39
+ def handler(event:, context:)
40
+ dd_api_key = ENV['DD_API_KEY']
41
+
42
+ if dd_api_key.nil? || dd_api_key.strip.empty?
43
+ puts 'ERROR: DD_API_KEY is not set. Skipping log forwarding.'
44
+ return
45
+ end
46
+
47
+ raw_data = event.fetch('awslogs').fetch('data')
48
+ unzipped_data = Zlib.gunzip(Base64.decode64(raw_data))
49
+ data = JSON.parse(unzipped_data)
50
+
51
+ message_type = data.fetch('messageType')
52
+ log_group = data.fetch('logGroup')
53
+ log_stream = data.fetch('logStream')
54
+ log_events = data.fetch('logEvents')
55
+
56
+ if message_type == 'CONTROL_MESSAGE'
57
+ puts 'skipping control message'
58
+ return
59
+ end
60
+
61
+ puts "message_count=#{log_events.length}"
62
+
63
+ region = 'unknown'
64
+ account_id = 'unknown'
65
+
66
+ if context&.respond_to?(:invoked_function_arn) && !context.invoked_function_arn.empty?
67
+ arn_parts = context.invoked_function_arn.split(':')
68
+ region = arn_parts[3] if arn_parts[3]
69
+ account_id = arn_parts[4] if arn_parts[4]
70
+ end
71
+
72
+ function_name = log_group&.split('/')&.last
73
+ function_arn = "arn:aws:lambda:#{region}:#{account_id}:function:#{function_name}"
74
+
75
+ normalized_data = log_events.map do |log_event|
76
+ normalized_msg = normalize_messages([log_event]).first
77
+
78
+ {
79
+ message: normalized_msg,
80
+ ddsource: ENV['DATADOG_SOURCE'] || 'cwl-aws-lambda',
81
+ service: ENV['DATADOG_SERVICE'] || function_name,
82
+ hostname: function_arn,
83
+ ddtags: [ENV['DATADOG_TAGS'], "region:#{region}"].compact.reject(&:empty?).join(','),
84
+ aws: {
85
+ awslogs: {
86
+ logGroup: log_group,
87
+ logStream: log_stream
88
+ }
89
+ },
90
+ id: log_event['id']
91
+ }
92
+ end
93
+
94
+ begin
95
+ send_json_to_datadog(normalized_data.to_json)
96
+ puts "Datadog upload successful: sent #{normalized_data.length} log(s) for #{function_name}"
97
+ rescue => e
98
+ puts e.message
99
+ puts "Error details: #{e.backtrace.first}"
100
+ end
101
+
102
+ nil
103
+ end
104
+
105
+ private
106
+
107
+ def normalize_messages(log_events)
108
+ log_events.map do |log_event|
109
+ message = log_event.fetch('message')
110
+ timestamp = Integer(log_event.fetch('timestamp'))
111
+
112
+ # Ensure the message starts with a time.
113
+ # E.g. START/END/REPORT log events don't.
114
+ if TIME_REGEX !~ message
115
+ time_string = Time.at(timestamp/1000.0).utc.strftime('%Y-%m-%dT%H:%M:%S.%LZ')
116
+ message = "#{time_string} #{message}"
117
+ end
118
+
119
+ # AWS replaces newlines with carriage returns in Lambda logs
120
+ message.gsub(/\r(?!\n)/, "\n")
121
+ end
122
+ end
123
+
124
+ 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")
127
+
128
+ response = Net::HTTP.start(datadog_url.host, datadog_url.port, use_ssl: datadog_url.scheme == 'https') do |http|
129
+ http.open_timeout = 3
130
+ http.read_timeout = 3
131
+
132
+ req = Net::HTTP::Post.new(
133
+ datadog_url,
134
+ {
135
+ 'Content-Type' => 'application/json',
136
+ 'DD-API-KEY' => ENV['DD_API_KEY'],
137
+ }
138
+ )
139
+
140
+ req.body = data
141
+
142
+ http.request(req)
143
+ end
144
+
145
+ unless ['200', '202'].include?(response.code)
146
+ raise "Datadog upload failed: #{response.code} - #{response.body}"
147
+ end
148
+
149
+ response
150
+ end
151
+ end
152
+ end
@@ -6,7 +6,7 @@ Example of how to add this to your `serverless.yml`:
6
6
  forwardLogs:
7
7
  name: ${self:service.name}-forwardLogs-${self:provider.stage}
8
8
  # NOTE: Ensure that you update the path below if you update the tpt_serverless gem
9
- handler: /opt/ruby/2.5.0/gems/tpt_serverless-X.X.X/lib/tpt_serverless/sumo_log_forwarder.SumoLogForwarder.handler
9
+ handler: /opt/ruby/3.4.0/gems/tpt_serverless-X.X.X/lib/tpt_serverless/sumo_log_forwarder.SumoLogForwarder.handler
10
10
  timeout: 10 # seconds
11
11
  environment:
12
12
  SUMO_ENDPOINT: ${self:custom.sumoEndpoint}
@@ -19,6 +19,9 @@ Example of how to add this to your `serverless.yml`:
19
19
 
20
20
  require 'zlib'
21
21
  require 'base64'
22
+ require 'json'
23
+ require 'net/http'
24
+ require 'uri'
22
25
 
23
26
  TIME_REGEX = /^20\d\d-\d\d-\d\dT\d\d:\d\d:\d\d\.\d\d\dZ/
24
27
 
@@ -1,3 +1,3 @@
1
1
  module TptServerless
2
- VERSION = "0.2.1"
2
+ VERSION = "0.4.1"
3
3
  end
@@ -1,5 +1,7 @@
1
1
  require "tpt_serverless/version"
2
2
  require 'tpt_serverless/sumo_log_forwarder'
3
+ require 'tpt_serverless/datadog_log_forwarder'
4
+ require 'tpt_serverless/datadog'
3
5
 
4
6
  module TptServerless
5
7
  class Error < StandardError; end
metadata CHANGED
@@ -1,28 +1,40 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tpt_serverless
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - TpT
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2020-03-02 00:00:00.000000000 Z
12
- dependencies: []
13
- description:
14
- email:
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: dogapi
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.45'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.45'
15
26
  executables: []
16
27
  extensions: []
17
28
  extra_rdoc_files: []
18
29
  files:
19
30
  - lib/tpt_serverless.rb
31
+ - lib/tpt_serverless/datadog.rb
32
+ - lib/tpt_serverless/datadog_log_forwarder.rb
20
33
  - lib/tpt_serverless/sumo_log_forwarder.rb
21
34
  - lib/tpt_serverless/version.rb
22
35
  homepage: https://github.com/TeachersPayTeachers/tpt_serverless-ruby
23
36
  licenses: []
24
37
  metadata: {}
25
- post_install_message:
26
38
  rdoc_options: []
27
39
  require_paths:
28
40
  - lib
@@ -37,8 +49,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
37
49
  - !ruby/object:Gem::Version
38
50
  version: '0'
39
51
  requirements: []
40
- rubygems_version: 3.1.2
41
- signing_key:
52
+ rubygems_version: 3.6.9
42
53
  specification_version: 4
43
54
  summary: TpT serverless utils for Ruby
44
55
  test_files: []