lambda_loadout 0.0.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.
@@ -0,0 +1,256 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'logger'
5
+
6
+ module LambdaLoadout
7
+ # Structured logger with Lambda context enrichment
8
+ #
9
+ # Provides JSON structured logging with automatic Lambda context injection,
10
+ # correlation IDs, and support for custom fields.
11
+ #
12
+ # @example Basic usage
13
+ # logger = LambdaLoadout::Logger.new(service: "payment")
14
+ # logger.info("Processing payment", order_id: "12345")
15
+ # # => {"level":"INFO","timestamp":"2025-01-01T12:00:00.000Z","message":"Processing payment","service":"payment","order_id":"12345"}
16
+ #
17
+ # @example With Lambda context
18
+ # def lambda_handler(event:, context:)
19
+ # logger = LambdaLoadout::Logger.new(service: "payment")
20
+ # logger.inject_lambda_context(context)
21
+ # logger.info("Handler invoked")
22
+ # # => Includes function_name, request_id, etc.
23
+ # end
24
+ class Logger
25
+ attr_reader :service, :level, :output
26
+ attr_accessor :lambda_context
27
+
28
+ LOG_LEVELS = {
29
+ debug: 0,
30
+ info: 1,
31
+ warn: 2,
32
+ error: 3,
33
+ fatal: 4
34
+ }.freeze
35
+
36
+ # Initialize a new structured logger
37
+ #
38
+ # @param service [String] Service name (adds 'service' field to all logs)
39
+ # @param level [Symbol, String] Log level (:debug, :info, :warn, :error, :fatal)
40
+ # @param output [IO] Output stream (defaults to $stdout)
41
+ # @param sampling_rate [Float] Sampling rate for debug logs (0.0-1.0)
42
+ def initialize(service: nil, level: :info, output: $stdout, sampling_rate: nil)
43
+ @service = service || ENV.fetch('POWERTOOLS_SERVICE_NAME', nil)
44
+ @level = LOG_LEVELS[level.to_sym] || LOG_LEVELS[:info]
45
+ @output = output
46
+ @sampling_rate = sampling_rate&.to_f
47
+ @lambda_context = nil
48
+ @persistent_fields = {}
49
+ @persistent_fields['service'] = @service if @service
50
+ end
51
+
52
+ # Inject Lambda context into logs
53
+ #
54
+ # @param context [Object] Lambda context object
55
+ # @return [void]
56
+ def inject_lambda_context(context)
57
+ @lambda_context = context
58
+ end
59
+
60
+ # Add persistent fields that will be included in all log entries
61
+ #
62
+ # @param fields [Hash] Fields to add to all logs
63
+ # @return [void]
64
+ def append_keys(**fields)
65
+ @persistent_fields.merge!(fields.transform_keys(&:to_s))
66
+ end
67
+
68
+ # Remove persistent fields
69
+ #
70
+ # @param keys [Array<Symbol, String>] Keys to remove
71
+ # @return [void]
72
+ def remove_keys(*keys)
73
+ keys.each { |key| @persistent_fields.delete(key.to_s) }
74
+ end
75
+
76
+ # Log debug message
77
+ #
78
+ # @param message [String] Log message
79
+ # @param exception [Exception, nil] Optional exception object
80
+ # @param fields [Hash] Additional fields
81
+ # @return [void]
82
+ def debug(message, exception = nil, **fields)
83
+ return unless should_log?(:debug)
84
+
85
+ if exception.is_a?(Exception)
86
+ fields[:error] = exception.message
87
+ fields[:error_class] = exception.class.name
88
+ fields[:backtrace] = exception.backtrace&.first(10)
89
+ end
90
+
91
+ log(:debug, message, **fields)
92
+ end
93
+
94
+ # Log info message
95
+ #
96
+ # @param message [String] Log message
97
+ # @param exception [Exception, nil] Optional exception object
98
+ # @param fields [Hash] Additional fields
99
+ # @return [void]
100
+ def info(message, exception = nil, **fields)
101
+ return unless should_log?(:info)
102
+
103
+ if exception.is_a?(Exception)
104
+ fields[:error] = exception.message
105
+ fields[:error_class] = exception.class.name
106
+ fields[:backtrace] = exception.backtrace&.first(10)
107
+ end
108
+
109
+ log(:info, message, **fields)
110
+ end
111
+
112
+ # Log warning message
113
+ #
114
+ # @param message [String] Log message
115
+ # @param exception [Exception, nil] Optional exception object
116
+ # @param fields [Hash] Additional fields
117
+ # @return [void]
118
+ def warn(message, exception = nil, **fields)
119
+ return unless should_log?(:warn)
120
+
121
+ if exception.is_a?(Exception)
122
+ fields[:error] = exception.message
123
+ fields[:error_class] = exception.class.name
124
+ fields[:backtrace] = exception.backtrace&.first(10)
125
+ end
126
+
127
+ log(:warn, message, **fields)
128
+ end
129
+
130
+ # Log error message
131
+ #
132
+ # @param message [String] Log message
133
+ # @param exception [Exception, nil] Optional exception object
134
+ # @param fields [Hash] Additional fields
135
+ # @return [void]
136
+ def error(message, exception = nil, **fields)
137
+ return unless should_log?(:error)
138
+
139
+ if exception.is_a?(Exception)
140
+ fields[:error] = exception.message
141
+ fields[:error_class] = exception.class.name
142
+ fields[:backtrace] = exception.backtrace&.first(10)
143
+ end
144
+
145
+ log(:error, message, **fields)
146
+ end
147
+
148
+ # Log fatal message
149
+ #
150
+ # @param message [String] Log message
151
+ # @param fields [Hash] Additional fields
152
+ # @return [void]
153
+ def fatal(message, **fields)
154
+ return unless should_log?(:fatal)
155
+
156
+ log(:fatal, message, **fields)
157
+ end
158
+
159
+ # Log an exception with full details
160
+ #
161
+ # @param exception [Exception] Exception to log
162
+ # @param message [String] Additional message
163
+ # @param fields [Hash] Additional fields
164
+ # @return [void]
165
+ def exception(exception, message: nil, **fields)
166
+ error(
167
+ message || exception.message,
168
+ exception: exception.class.name,
169
+ error_message: exception.message,
170
+ backtrace: exception.backtrace&.first(10),
171
+ **fields
172
+ )
173
+ end
174
+
175
+ private
176
+
177
+ # Check if log level should be logged
178
+ #
179
+ # @param log_level [Symbol] Level to check
180
+ # @return [Boolean]
181
+ def should_log?(log_level)
182
+ level_value = LOG_LEVELS[log_level]
183
+ return false if level_value < @level
184
+
185
+ # Apply sampling for debug logs if configured
186
+ if log_level == :debug && @sampling_rate
187
+ rand <= @sampling_rate
188
+ else
189
+ true
190
+ end
191
+ end
192
+
193
+ # Core logging method
194
+ #
195
+ # @param level [Symbol] Log level
196
+ # @param message [String] Log message
197
+ # @param fields [Hash] Additional fields
198
+ # @return [void]
199
+ def log(level, message, **fields)
200
+ log_entry = build_log_entry(level, message, fields)
201
+ @output.puts JSON.generate(log_entry)
202
+ rescue StandardError => e
203
+ # Fallback to basic output if JSON serialization fails
204
+ @output.puts "[LambdaLoadout::Logger] Error serializing log: #{e.message}"
205
+ @output.puts "Original message: #{message}"
206
+ end
207
+
208
+ # Build structured log entry
209
+ #
210
+ # @param level [Symbol] Log level
211
+ # @param message [String] Log message
212
+ # @param fields [Hash] Additional fields
213
+ # @return [Hash]
214
+ def build_log_entry(level, message, fields)
215
+ entry = {
216
+ 'level' => level.to_s.upcase,
217
+ 'timestamp' => Time.now.utc.strftime('%Y-%m-%dT%H:%M:%S.%3NZ'),
218
+ 'message' => message
219
+ }
220
+
221
+ # Add persistent fields
222
+ entry.merge!(@persistent_fields)
223
+
224
+ # Add Lambda context if available
225
+ entry.merge!(lambda_context_fields) if @lambda_context
226
+
227
+ # Add custom fields
228
+ entry.merge!(stringify_keys(fields))
229
+
230
+ entry
231
+ end
232
+
233
+ # Extract fields from Lambda context
234
+ #
235
+ # @return [Hash]
236
+ def lambda_context_fields
237
+ {
238
+ 'function_name' => @lambda_context.function_name,
239
+ 'function_version' => @lambda_context.function_version,
240
+ 'function_memory_size' => @lambda_context.memory_limit_in_mb,
241
+ 'function_request_id' => @lambda_context.aws_request_id,
242
+ 'cold_start' => LambdaLoadout.cold_start?
243
+ }.compact
244
+ rescue StandardError
245
+ {}
246
+ end
247
+
248
+ # Convert hash keys to strings
249
+ #
250
+ # @param hash [Hash] Hash with symbol or string keys
251
+ # @return [Hash] Hash with string keys
252
+ def stringify_keys(hash)
253
+ hash.transform_keys(&:to_s)
254
+ end
255
+ end
256
+ end
@@ -0,0 +1,296 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'time'
5
+
6
+ module LambdaLoadout
7
+ # CloudWatch Metrics using Embedded Metric Format (EMF)
8
+ #
9
+ # Creates custom metrics asynchronously by outputting metrics to stdout
10
+ # following Amazon CloudWatch Embedded Metric Format (EMF).
11
+ #
12
+ # @example Basic usage
13
+ # metrics = LambdaLoadout::Metrics.new(namespace: "MyApp", service: "payment")
14
+ # metrics.add_metric(name: "PaymentProcessed", unit: "Count", value: 1)
15
+ # metrics.add_dimension(name: "region", value: "us-east-1")
16
+ # metrics.flush
17
+ #
18
+ # @example With middleware
19
+ # def lambda_handler(event:, context:)
20
+ # metrics = LambdaLoadout::Metrics.new(namespace: "MyApp")
21
+ #
22
+ # begin
23
+ # metrics.add_metric(name: "RequestReceived", unit: "Count", value: 1)
24
+ # # Your logic here
25
+ # ensure
26
+ # metrics.flush
27
+ # end
28
+ # end
29
+ class Metrics
30
+ # CloudWatch metric units
31
+ UNITS = %w[
32
+ Seconds Microseconds Milliseconds
33
+ Bytes Kilobytes Megabytes Gigabytes Terabytes
34
+ Bits Kilobits Megabits Gigabits Terabits
35
+ Percent Count
36
+ Bytes/Second Kilobytes/Second Megabytes/Second Gigabytes/Second Terabytes/Second
37
+ Bits/Second Kilobits/Second Megabits/Second Gigabits/Second Terabits/Second
38
+ Count/Second None
39
+ ].freeze
40
+
41
+ # Metric resolutions in seconds
42
+ RESOLUTIONS = [1, 60].freeze
43
+
44
+ MAX_DIMENSIONS = 29
45
+ MAX_METRICS = 100
46
+
47
+ attr_reader :namespace, :service, :output
48
+
49
+ # Initialize metrics
50
+ #
51
+ # @param namespace [String] CloudWatch metric namespace (required)
52
+ # @param service [String] Service name (added as default dimension)
53
+ # @param output [IO] Output stream for EMF (defaults to $stdout)
54
+ def initialize(namespace: nil, service: nil, output: $stdout)
55
+ @namespace = namespace || ENV.fetch('POWERTOOLS_METRICS_NAMESPACE', nil)
56
+ @service = service || ENV.fetch('POWERTOOLS_SERVICE_NAME', nil)
57
+ @output = output
58
+
59
+ raise ArgumentError, 'Namespace is required' if @namespace.nil? || @namespace.empty?
60
+
61
+ @metrics = {}
62
+ @dimensions = {}
63
+ @metadata = {}
64
+ @default_dimensions = {}
65
+ @timestamp = nil
66
+ @cold_start_captured = false
67
+
68
+ # Add service as default dimension if provided
69
+ add_dimension(name: 'service', value: @service) if @service
70
+ end
71
+
72
+ # Add a metric
73
+ #
74
+ # @param name [String] Metric name
75
+ # @param unit [String] Metric unit (must be valid CloudWatch unit)
76
+ # @param value [Numeric] Metric value
77
+ # @param resolution [Integer] Storage resolution in seconds (1 or 60)
78
+ # @return [void]
79
+ # @raise [ArgumentError] if parameters are invalid
80
+ def add_metric(name:, unit:, value:, resolution: 60)
81
+ validate_metric_name!(name)
82
+ validate_metric_unit!(unit)
83
+ validate_metric_value!(value)
84
+ validate_metric_resolution!(resolution)
85
+
86
+ # Auto-flush if we've hit the max metrics limit
87
+ flush_metrics if @metrics.size >= MAX_METRICS
88
+
89
+ metric_key = name.to_s
90
+ @metrics[metric_key] ||= { 'Unit' => unit.to_s, 'StorageResolution' => resolution, 'Values' => [] }
91
+ @metrics[metric_key]['Values'] << value.to_f
92
+ end
93
+
94
+ # Add a dimension to all metrics
95
+ #
96
+ # @param name [String] Dimension name
97
+ # @param value [String] Dimension value
98
+ # @return [void]
99
+ # @raise [ArgumentError] if max dimensions exceeded
100
+ def add_dimension(name:, value:)
101
+ raise ArgumentError, "Maximum number of dimensions (#{MAX_DIMENSIONS}) exceeded" if @dimensions.size >= MAX_DIMENSIONS
102
+
103
+ @dimensions[name.to_s] = value.to_s
104
+ end
105
+
106
+ # Add metadata (searchable in CloudWatch Logs but not as a metric dimension)
107
+ #
108
+ # @param key [String] Metadata key
109
+ # @param value [Object] Metadata value
110
+ # @return [void]
111
+ def add_metadata(key:, value:)
112
+ @metadata[key.to_s] = value
113
+ end
114
+
115
+ # Set default dimensions that persist across flushes
116
+ #
117
+ # @param dimensions [Hash] Default dimensions
118
+ # @return [void]
119
+ def set_default_dimensions(**dimensions)
120
+ @default_dimensions.merge!(dimensions.transform_keys(&:to_s))
121
+ @dimensions.merge!(@default_dimensions)
122
+ end
123
+
124
+ # Set custom timestamp for metrics
125
+ #
126
+ # @param timestamp [Time, Integer] Timestamp (Time object or epoch in milliseconds)
127
+ # @return [void]
128
+ def set_timestamp(timestamp)
129
+ @timestamp = case timestamp
130
+ when Time
131
+ (timestamp.to_f * 1000).to_i
132
+ when Integer
133
+ timestamp
134
+ else
135
+ raise ArgumentError, 'Timestamp must be Time object or Integer (epoch ms)'
136
+ end
137
+
138
+ # Validate timestamp is within CloudWatch limits (14 days past, 2 hours future)
139
+ now = (Time.now.to_f * 1000).to_i
140
+ min_time = now - (14 * 24 * 60 * 60 * 1000)
141
+ max_time = now + (2 * 60 * 60 * 1000)
142
+
143
+ return unless @timestamp < min_time || @timestamp > max_time
144
+
145
+ warn '[LambdaLoadout::Metrics] Timestamp outside CloudWatch limits (14 days past, 2 hours future)'
146
+ end
147
+
148
+ # Add cold start metric
149
+ #
150
+ # @param context [Object] Lambda context
151
+ # @return [void]
152
+ def add_cold_start_metric(context)
153
+ return if @cold_start_captured
154
+ return unless LambdaLoadout.cold_start?
155
+
156
+ @cold_start_captured = true
157
+
158
+ # Create a separate EMF blob for cold start metric
159
+ cold_start_metrics = self.class.new(namespace: @namespace, output: @output)
160
+ cold_start_metrics.add_metric(name: 'ColdStart', unit: 'Count', value: 1)
161
+ cold_start_metrics.add_dimension(name: 'function_name', value: context.function_name)
162
+ cold_start_metrics.add_dimension(name: 'service', value: @service) if @service
163
+ cold_start_metrics.flush
164
+ rescue StandardError => e
165
+ warn "[LambdaLoadout::Metrics] Failed to add cold start metric: #{e.message}"
166
+ end
167
+
168
+ # Serialize and flush all metrics
169
+ #
170
+ # Outputs metrics in CloudWatch Embedded Metric Format (EMF) to stdout.
171
+ # Metrics are automatically ingested by CloudWatch when running in Lambda.
172
+ #
173
+ # @param raise_on_empty [Boolean] Raise error if no metrics to flush
174
+ # @return [void]
175
+ def flush(raise_on_empty: false)
176
+ if @metrics.empty?
177
+ warn '[LambdaLoadout::Metrics] No metrics to flush' unless raise_on_empty
178
+ raise 'No metrics to flush' if raise_on_empty
179
+
180
+ return
181
+ end
182
+
183
+ flush_metrics
184
+ end
185
+
186
+ # Clear all metrics, dimensions, and metadata
187
+ #
188
+ # @return [void]
189
+ def clear
190
+ @metrics.clear
191
+ @dimensions.clear
192
+ @metadata.clear
193
+ @dimensions.merge!(@default_dimensions)
194
+ @timestamp = nil
195
+ end
196
+
197
+ private
198
+
199
+ # Flush metrics to output in EMF format
200
+ #
201
+ # @return [void]
202
+ def flush_metrics
203
+ return if @metrics.empty?
204
+
205
+ emf_output = serialize_metrics
206
+ @output.puts JSON.generate(emf_output)
207
+
208
+ # Clear metrics but keep dimensions and metadata
209
+ @metrics.clear
210
+ @timestamp = nil
211
+ rescue StandardError => e
212
+ warn "[LambdaLoadout::Metrics] Failed to serialize metrics: #{e.message}"
213
+ end
214
+
215
+ # Serialize metrics to EMF format
216
+ #
217
+ # @return [Hash] EMF formatted metrics
218
+ def serialize_metrics
219
+ # Build metric definitions for EMF
220
+ metric_definitions = @metrics.map do |name, metric|
221
+ definition = {
222
+ 'Name' => name,
223
+ 'Unit' => metric['Unit']
224
+ }
225
+
226
+ # Add storage resolution for high-resolution metrics
227
+ definition['StorageResolution'] = metric['StorageResolution'] if metric['StorageResolution'] == 1
228
+
229
+ definition
230
+ end
231
+
232
+ # Build metric values
233
+ metric_values = @metrics.transform_values do |metric|
234
+ values = metric['Values']
235
+ values.size == 1 ? values.first : values
236
+ end
237
+
238
+ # Build EMF output
239
+ {
240
+ '_aws' => {
241
+ 'Timestamp' => @timestamp || (Time.now.to_f * 1000).to_i,
242
+ 'CloudWatchMetrics' => [
243
+ {
244
+ 'Namespace' => @namespace,
245
+ 'Dimensions' => [@dimensions.keys],
246
+ 'Metrics' => metric_definitions
247
+ }
248
+ ]
249
+ }
250
+ }.merge(@dimensions)
251
+ .merge(@metadata)
252
+ .merge(metric_values)
253
+ end
254
+
255
+ # Validate metric name
256
+ #
257
+ # @param name [String] Metric name
258
+ # @raise [ArgumentError] if invalid
259
+ def validate_metric_name!(name)
260
+ name_str = name.to_s
261
+ return unless name_str.empty? || name_str.length > 255
262
+
263
+ raise ArgumentError, 'Metric name must be between 1 and 255 characters'
264
+ end
265
+
266
+ # Validate metric unit
267
+ #
268
+ # @param unit [String] Metric unit
269
+ # @raise [ArgumentError] if invalid
270
+ def validate_metric_unit!(unit)
271
+ return if UNITS.include?(unit.to_s)
272
+
273
+ raise ArgumentError, "Invalid metric unit: #{unit}. Must be one of: #{UNITS.join(', ')}"
274
+ end
275
+
276
+ # Validate metric value
277
+ #
278
+ # @param value [Numeric] Metric value
279
+ # @raise [ArgumentError] if invalid
280
+ def validate_metric_value!(value)
281
+ return if value.is_a?(Numeric)
282
+
283
+ raise ArgumentError, 'Metric value must be a number'
284
+ end
285
+
286
+ # Validate metric resolution
287
+ #
288
+ # @param resolution [Integer] Storage resolution
289
+ # @raise [ArgumentError] if invalid
290
+ def validate_metric_resolution!(resolution)
291
+ return if RESOLUTIONS.include?(resolution)
292
+
293
+ raise ArgumentError, "Invalid resolution: #{resolution}. Must be 1 or 60 seconds"
294
+ end
295
+ end
296
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LambdaLoadout
4
+ # Middleware for Ruby Lambda handlers
5
+ #
6
+ # Provides a clean way to wrap Lambda handlers with logging, metrics,
7
+ # and error handling without repetitive boilerplate code.
8
+ #
9
+ # @example Using as a wrapper
10
+ # class MyHandler
11
+ # include LambdaLoadout::Middleware
12
+ #
13
+ # def initialize
14
+ # @logger = LambdaLoadout::Logger.new(service: "payment")
15
+ # @metrics = LambdaLoadout::Metrics.new(namespace: "MyApp", service: "payment")
16
+ # end
17
+ #
18
+ # def call(event:, context:)
19
+ # with_observability(context) do
20
+ # @logger.info("Processing payment", event: event)
21
+ # @metrics.add_metric(name: "PaymentProcessed", unit: "Count", value: 1)
22
+ #
23
+ # { statusCode: 200, body: "Success" }
24
+ # end
25
+ # end
26
+ # end
27
+ module Middleware
28
+ # Execute block with full observability stack
29
+ #
30
+ # Automatically:
31
+ # - Injects Lambda context into logger
32
+ # - Captures cold start metric
33
+ # - Flushes metrics after execution
34
+ # - Handles errors with proper logging and metrics
35
+ #
36
+ # @param context [Object] Lambda context
37
+ # @param capture_cold_start [Boolean] Whether to capture cold start
38
+ # @param logger [LambdaLoadout::Logger] Logger instance (uses @logger if not provided)
39
+ # @param metrics [LambdaLoadout::Metrics] Metrics instance (uses @metrics if not provided)
40
+ # @yield Block containing handler logic
41
+ # @return [Object] Result of block execution
42
+ def with_observability(context, capture_cold_start: true, logger: nil, metrics: nil, &block)
43
+ logger ||= @logger
44
+ metrics ||= @metrics
45
+
46
+ logger&.inject_lambda_context(context)
47
+
48
+ begin
49
+ result = block.call
50
+ metrics&.add_cold_start_metric(context) if capture_cold_start
51
+ result
52
+ rescue StandardError => e
53
+ logger&.exception(e, message: 'Lambda execution failed')
54
+ metrics&.add_metric(name: 'LambdaError', unit: 'Count', value: 1)
55
+ raise
56
+ ensure
57
+ begin
58
+ metrics&.flush
59
+ rescue StandardError
60
+ nil
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ # Decorator-style wrapper for Lambda handlers
67
+ #
68
+ # Provides a simple DSL for wrapping Lambda handlers with observability.
69
+ #
70
+ # @example Basic usage
71
+ # LambdaLoadout::Handler.configure do |config|
72
+ # config.service = "payment"
73
+ # config.namespace = "MyApp"
74
+ # end
75
+ #
76
+ # def lambda_handler(event:, context:)
77
+ # LambdaLoadout::Handler.call(event, context) do |logger, metrics|
78
+ # logger.info("Processing event")
79
+ # metrics.add_metric(name: "EventProcessed", unit: "Count", value: 1)
80
+ #
81
+ # { statusCode: 200 }
82
+ # end
83
+ # end
84
+ class Handler
85
+ class << self
86
+ attr_accessor :service, :namespace, :log_level, :capture_cold_start
87
+
88
+ # Configure global handler defaults
89
+ #
90
+ # @yield [self] Configuration block
91
+ # @return [void]
92
+ def configure
93
+ yield self
94
+ end
95
+
96
+ # Call handler with observability
97
+ #
98
+ # @param event [Hash] Lambda event
99
+ # @param context [Object] Lambda context
100
+ # @param capture_cold_start [Boolean] Override cold start capture
101
+ # @yield [logger, metrics] Block with logger and metrics instances
102
+ # @return [Object] Result of block execution
103
+ def call(event, context, capture_cold_start: @capture_cold_start || true, &block)
104
+ logger = LambdaLoadout::Logger.new(
105
+ service: @service,
106
+ level: @log_level || :info
107
+ )
108
+
109
+ metrics = LambdaLoadout::Metrics.new(
110
+ namespace: @namespace || 'LambdaLoadout',
111
+ service: @service
112
+ )
113
+
114
+ logger.inject_lambda_context(context)
115
+ logger.info('Lambda invocation started', event_keys: event.keys)
116
+
117
+ begin
118
+ result = block.call(logger, metrics)
119
+ metrics.add_cold_start_metric(context) if capture_cold_start
120
+ logger.info('Lambda invocation completed successfully')
121
+ result
122
+ rescue StandardError => e
123
+ logger.exception(e, message: 'Lambda invocation failed')
124
+ metrics.add_metric(name: 'LambdaError', unit: 'Count', value: 1)
125
+ metrics.add_dimension(name: 'error_type', value: e.class.name)
126
+ raise
127
+ ensure
128
+ begin
129
+ metrics.flush
130
+ rescue StandardError
131
+ nil
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LambdaLoadout
4
+ VERSION = '0.0.1'
5
+ end