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.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +50 -0
- data/LICENSE.txt +21 -0
- data/README.md +499 -0
- data/certs/stowzilla.pem +26 -0
- data/lib/lambda_loadout/error_notifier.rb +248 -0
- data/lib/lambda_loadout/errors.rb +218 -0
- data/lib/lambda_loadout/global.rb +83 -0
- data/lib/lambda_loadout/logger.rb +256 -0
- data/lib/lambda_loadout/metrics.rb +296 -0
- data/lib/lambda_loadout/middleware.rb +137 -0
- data/lib/lambda_loadout/version.rb +5 -0
- data/lib/lambda_loadout.rb +101 -0
- data.tar.gz.sig +3 -0
- metadata +168 -0
- metadata.gz.sig +0 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'cgi'
|
|
5
|
+
require 'time'
|
|
6
|
+
|
|
7
|
+
module LambdaLoadout
|
|
8
|
+
# ErrorNotifier sends detailed error notifications via SNS when Lambda functions fail
|
|
9
|
+
#
|
|
10
|
+
# @example Basic usage
|
|
11
|
+
# notifier = LambdaLoadout::ErrorNotifier.new(
|
|
12
|
+
# sns_topic_arn: ENV['ERROR_NOTIFICATION_TOPIC_ARN'],
|
|
13
|
+
# logger: logger
|
|
14
|
+
# )
|
|
15
|
+
# notifier.notify(error: exception, context: context, event: event)
|
|
16
|
+
#
|
|
17
|
+
# @example With custom region
|
|
18
|
+
# notifier = LambdaLoadout::ErrorNotifier.new(
|
|
19
|
+
# sns_topic_arn: 'arn:aws:sns:us-west-2:123456789012:alerts',
|
|
20
|
+
# logger: logger,
|
|
21
|
+
# region: 'us-west-2'
|
|
22
|
+
# )
|
|
23
|
+
class ErrorNotifier
|
|
24
|
+
attr_reader :sns_topic_arn, :logger, :region
|
|
25
|
+
|
|
26
|
+
# Initialize ErrorNotifier
|
|
27
|
+
#
|
|
28
|
+
# @param sns_topic_arn [String] ARN of SNS topic for error notifications
|
|
29
|
+
# @param logger [LambdaLoadout::Logger] Logger instance for logging notification status
|
|
30
|
+
# @param region [String, nil] AWS region (defaults to AWS_REGION env var or us-east-1)
|
|
31
|
+
def initialize(sns_topic_arn:, logger:, region: nil)
|
|
32
|
+
@sns_topic_arn = sns_topic_arn
|
|
33
|
+
@logger = logger
|
|
34
|
+
@region = region || ENV['AWS_REGION'] || 'us-east-1'
|
|
35
|
+
@sns_client = nil # Lazy initialize
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Send error notification via SNS
|
|
39
|
+
#
|
|
40
|
+
# @param error [Exception] The exception that occurred
|
|
41
|
+
# @param context [LambdaContext] AWS Lambda context object
|
|
42
|
+
# @param event [Hash] Lambda event hash
|
|
43
|
+
# @return [void]
|
|
44
|
+
def notify(error:, context:, event:)
|
|
45
|
+
return if sns_topic_arn.nil? || sns_topic_arn.empty?
|
|
46
|
+
|
|
47
|
+
begin
|
|
48
|
+
ensure_sns_client
|
|
49
|
+
|
|
50
|
+
subject = build_subject(error, context)
|
|
51
|
+
message = build_message(error, context, event)
|
|
52
|
+
|
|
53
|
+
@sns_client.publish(
|
|
54
|
+
topic_arn: sns_topic_arn,
|
|
55
|
+
subject: subject,
|
|
56
|
+
message: message
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
logger.info('Error notification sent',
|
|
60
|
+
topic_arn: sns_topic_arn,
|
|
61
|
+
error_class: error.class.name)
|
|
62
|
+
rescue StandardError => e
|
|
63
|
+
# Don't let notification errors prevent the original error from being raised
|
|
64
|
+
logger.warn('Failed to send error notification', e,
|
|
65
|
+
topic_arn: sns_topic_arn,
|
|
66
|
+
original_error: error.class.name)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
# Lazy initialize SNS client
|
|
73
|
+
def ensure_sns_client
|
|
74
|
+
@ensure_sns_client ||= begin
|
|
75
|
+
require 'aws-sdk-sns'
|
|
76
|
+
Aws::SNS::Client.new(region: region)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Build email subject line
|
|
81
|
+
def build_subject(error, context)
|
|
82
|
+
"🚨 Lambda Error: #{error.class.name} in #{context.function_name}"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Build detailed email message body
|
|
86
|
+
def build_message(error, context, event)
|
|
87
|
+
<<~MESSAGE
|
|
88
|
+
🚨 Lambda Function Error Alert
|
|
89
|
+
|
|
90
|
+
📋 Function Details
|
|
91
|
+
══════════════════
|
|
92
|
+
Name: #{context.function_name}
|
|
93
|
+
Environment: #{ENV['ENVIRONMENT'] || ENV['STAGE'] || 'unknown'}
|
|
94
|
+
Request ID: #{context.aws_request_id}
|
|
95
|
+
Region: #{region}
|
|
96
|
+
Timestamp: #{Time.now.utc.strftime('%Y-%m-%d %H:%M:%S UTC')}
|
|
97
|
+
Memory Limit: #{context.memory_limit_in_mb}MB
|
|
98
|
+
Remaining Time: #{context.get_remaining_time_in_millis}ms
|
|
99
|
+
|
|
100
|
+
❌ Error Details
|
|
101
|
+
════════════════
|
|
102
|
+
Type: #{error.class.name}
|
|
103
|
+
Message: #{truncate_message(error.message)}
|
|
104
|
+
|
|
105
|
+
🌐 Request Context
|
|
106
|
+
══════════════════
|
|
107
|
+
#{format_request_context(event)}
|
|
108
|
+
|
|
109
|
+
📚 Stack Trace (first 15 lines)
|
|
110
|
+
═══════════════════════════════
|
|
111
|
+
#{format_backtrace(error)}
|
|
112
|
+
|
|
113
|
+
🔍 View Full Logs
|
|
114
|
+
═════════════════
|
|
115
|
+
|
|
116
|
+
CloudWatch Logs Insights (filtered to this error):
|
|
117
|
+
#{build_logs_insights_url(context, error)}
|
|
118
|
+
|
|
119
|
+
CloudWatch Recent Logs:
|
|
120
|
+
#{build_logs_stream_url(context)}
|
|
121
|
+
|
|
122
|
+
📊 JSON Data
|
|
123
|
+
════════════
|
|
124
|
+
#{format_error_json(error, context, event)}
|
|
125
|
+
MESSAGE
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Truncate very long error messages
|
|
129
|
+
def truncate_message(message, max_length = 500)
|
|
130
|
+
return message if message.length <= max_length
|
|
131
|
+
|
|
132
|
+
"#{message[0...max_length]}... (truncated, #{message.length} chars total)"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Format request context based on event type
|
|
136
|
+
def format_request_context(event)
|
|
137
|
+
if event['httpMethod'] # API Gateway
|
|
138
|
+
<<~CONTEXT.strip
|
|
139
|
+
Path: #{event['path'] || 'N/A'}
|
|
140
|
+
Method: #{event['httpMethod']}
|
|
141
|
+
Source IP: #{event.dig('requestContext', 'identity', 'sourceIp') || 'N/A'}
|
|
142
|
+
User Agent: #{event.dig('headers', 'User-Agent') || event.dig('headers', 'user-agent') || 'N/A'}
|
|
143
|
+
CONTEXT
|
|
144
|
+
elsif event['Records'] # SQS, SNS, DynamoDB Streams, etc.
|
|
145
|
+
first_record = event['Records'].first || {}
|
|
146
|
+
event_source = first_record['eventSource'] || 'Unknown'
|
|
147
|
+
<<~CONTEXT.strip
|
|
148
|
+
Event Source: #{event_source}
|
|
149
|
+
Records Count: #{event['Records'].size}
|
|
150
|
+
First Record ID: #{first_record['messageId'] || first_record['eventID'] || 'N/A'}
|
|
151
|
+
CONTEXT
|
|
152
|
+
elsif event['source'] # EventBridge
|
|
153
|
+
<<~CONTEXT.strip
|
|
154
|
+
Event Source: #{event['source']}
|
|
155
|
+
Detail Type: #{event['detail-type'] || 'N/A'}
|
|
156
|
+
Resources: #{event['resources']&.join(', ') || 'N/A'}
|
|
157
|
+
CONTEXT
|
|
158
|
+
else # Unknown event type
|
|
159
|
+
"Event Type: Unknown\nEvent Keys: #{event.keys.sort.join(', ')}"
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Format exception backtrace
|
|
164
|
+
def format_backtrace(error)
|
|
165
|
+
backtrace = error.backtrace || []
|
|
166
|
+
if backtrace.empty?
|
|
167
|
+
'No backtrace available'
|
|
168
|
+
else
|
|
169
|
+
backtrace.first(15).join("\n")
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Build CloudWatch Logs Insights deep link with pre-filled query
|
|
174
|
+
# Note: This URL format is specific to the AWS Console as of 2025
|
|
175
|
+
# If AWS changes the console URL structure, this may need updates
|
|
176
|
+
def build_logs_insights_url(context, error)
|
|
177
|
+
log_group = "/aws/lambda/#{context.function_name}"
|
|
178
|
+
|
|
179
|
+
# Use current time minus 5 minutes to capture the error
|
|
180
|
+
start_time = (Time.now - 300).to_i * 1000 # milliseconds
|
|
181
|
+
end_time = Time.now.to_i * 1000
|
|
182
|
+
|
|
183
|
+
# CloudWatch Logs Insights query to find this specific error
|
|
184
|
+
error_class_escaped = Regexp.escape(error.class.name)
|
|
185
|
+
query = "fields @timestamp, @message | filter @message like /#{error_class_escaped}/ | sort @timestamp desc | limit 20"
|
|
186
|
+
|
|
187
|
+
# URL encode the query and log group
|
|
188
|
+
encoded_query = CGI.escape(query)
|
|
189
|
+
encoded_log_group = CGI.escape(log_group)
|
|
190
|
+
|
|
191
|
+
# Deep link to CloudWatch Logs Insights with pre-filled query
|
|
192
|
+
# Format: AWS Console uses a specific encoding scheme for Logs Insights URLs
|
|
193
|
+
"https://console.aws.amazon.com/cloudwatch/home?region=#{region}#logsV2:logs-insights$3FqueryDetail$3D~(end~#{end_time}~start~#{start_time}~timeType~'ABSOLUTE~tz~'UTC~editorString~'#{encoded_query}~isLiveTail~false~source~(~'#{encoded_log_group}))"
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Build CloudWatch Logs stream deep link
|
|
197
|
+
def build_logs_stream_url(context)
|
|
198
|
+
# CloudWatch uses special encoding for forward slashes in URLs
|
|
199
|
+
encoded_log_group = "/aws/lambda/#{CGI.escape(context.function_name)}".gsub('/', '$252F')
|
|
200
|
+
|
|
201
|
+
"https://console.aws.amazon.com/cloudwatch/home?region=#{region}#logsV2:log-groups/log-group/#{encoded_log_group}/log-events"
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Format error details as JSON
|
|
205
|
+
def format_error_json(error, context, event)
|
|
206
|
+
error_data = {
|
|
207
|
+
function: context.function_name,
|
|
208
|
+
request_id: context.aws_request_id,
|
|
209
|
+
error_class: error.class.name,
|
|
210
|
+
error_message: error.message,
|
|
211
|
+
timestamp: Time.now.utc.iso8601,
|
|
212
|
+
backtrace: error.backtrace&.first(15) || [],
|
|
213
|
+
event_summary: build_event_summary(event)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
JSON.pretty_generate(error_data)
|
|
217
|
+
rescue StandardError => e
|
|
218
|
+
# Fallback if JSON formatting fails
|
|
219
|
+
"{\"error\": \"Failed to format error data as JSON: #{e.message}\"}"
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Build compact event summary for JSON output
|
|
223
|
+
def build_event_summary(event)
|
|
224
|
+
if event['httpMethod']
|
|
225
|
+
{
|
|
226
|
+
type: 'API Gateway',
|
|
227
|
+
path: event['path'],
|
|
228
|
+
method: event['httpMethod']
|
|
229
|
+
}
|
|
230
|
+
elsif event['Records']
|
|
231
|
+
{
|
|
232
|
+
type: event['Records'].first&.dig('eventSource') || 'Records',
|
|
233
|
+
count: event['Records'].size
|
|
234
|
+
}
|
|
235
|
+
elsif event['source']
|
|
236
|
+
{
|
|
237
|
+
type: 'EventBridge',
|
|
238
|
+
source: event['source']
|
|
239
|
+
}
|
|
240
|
+
else
|
|
241
|
+
{
|
|
242
|
+
type: 'Unknown',
|
|
243
|
+
keys: event.keys.sort
|
|
244
|
+
}
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LambdaLoadout
|
|
4
|
+
# Custom error classes
|
|
5
|
+
module Errors
|
|
6
|
+
# Base error for all LambdaLoadout errors
|
|
7
|
+
class Error < StandardError; end
|
|
8
|
+
|
|
9
|
+
# Raised when metric validation fails
|
|
10
|
+
class MetricValidationError < Error; end
|
|
11
|
+
|
|
12
|
+
# Raised when EMF schema validation fails
|
|
13
|
+
class SchemaValidationError < Error; end
|
|
14
|
+
|
|
15
|
+
# Raised when configuration is invalid
|
|
16
|
+
class ConfigurationError < Error; end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Error handler for Lambda functions
|
|
20
|
+
#
|
|
21
|
+
# Automatically captures exceptions, logs them, and creates error metrics.
|
|
22
|
+
#
|
|
23
|
+
# @example Basic usage
|
|
24
|
+
# handler = LambdaLoadout::ErrorHandler.new(
|
|
25
|
+
# logger: logger,
|
|
26
|
+
# metrics: metrics,
|
|
27
|
+
# capture_stack_trace: true
|
|
28
|
+
# )
|
|
29
|
+
#
|
|
30
|
+
# def lambda_handler(event:, context:)
|
|
31
|
+
# handler.handle(context) do
|
|
32
|
+
# # Your code that might raise errors
|
|
33
|
+
# process_event(event)
|
|
34
|
+
# end
|
|
35
|
+
# end
|
|
36
|
+
class ErrorHandler
|
|
37
|
+
attr_reader :logger, :metrics
|
|
38
|
+
|
|
39
|
+
# Initialize error handler
|
|
40
|
+
#
|
|
41
|
+
# @param logger [LambdaLoadout::Logger] Logger instance
|
|
42
|
+
# @param metrics [LambdaLoadout::Metrics] Metrics instance
|
|
43
|
+
# @param capture_stack_trace [Boolean] Whether to include stack trace in logs
|
|
44
|
+
# @param error_metric_name [String] Name of error metric (default: "LambdaError")
|
|
45
|
+
def initialize(logger: nil, metrics: nil, capture_stack_trace: true, error_metric_name: 'LambdaError')
|
|
46
|
+
@logger = logger
|
|
47
|
+
@metrics = metrics
|
|
48
|
+
@capture_stack_trace = capture_stack_trace
|
|
49
|
+
@error_metric_name = error_metric_name
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Handle block execution with error capturing
|
|
53
|
+
#
|
|
54
|
+
# @param context [Object] Lambda context
|
|
55
|
+
# @yield Block to execute
|
|
56
|
+
# @return [Object] Result of block execution
|
|
57
|
+
# @raise Re-raises the original exception after logging and metrics
|
|
58
|
+
def handle(context = nil, &block)
|
|
59
|
+
block.call
|
|
60
|
+
rescue StandardError => e
|
|
61
|
+
capture_error(e, context)
|
|
62
|
+
raise
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Capture and log an error
|
|
66
|
+
#
|
|
67
|
+
# @param error [Exception] Exception to capture
|
|
68
|
+
# @param context [Object] Lambda context
|
|
69
|
+
# @param custom_fields [Hash] Additional fields to log
|
|
70
|
+
# @return [void]
|
|
71
|
+
def capture_error(error, context = nil, **custom_fields)
|
|
72
|
+
error_details = {
|
|
73
|
+
error_type: error.class.name,
|
|
74
|
+
error_message: error.message
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
error_details[:stack_trace] = error.backtrace&.first(20) if @capture_stack_trace
|
|
78
|
+
|
|
79
|
+
if context
|
|
80
|
+
error_details[:function_name] = context.function_name
|
|
81
|
+
error_details[:request_id] = context.aws_request_id
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
error_details.merge!(custom_fields)
|
|
85
|
+
|
|
86
|
+
# Log error
|
|
87
|
+
@logger&.error('Lambda execution error', **error_details)
|
|
88
|
+
|
|
89
|
+
# Add error metric
|
|
90
|
+
return unless @metrics
|
|
91
|
+
|
|
92
|
+
@metrics.add_metric(name: @error_metric_name, unit: 'Count', value: 1)
|
|
93
|
+
@metrics.add_dimension(name: 'error_type', value: error.class.name)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Alarm configuration helper
|
|
98
|
+
#
|
|
99
|
+
# Helps define CloudWatch Alarms for metrics created via EMF.
|
|
100
|
+
# Note: This generates CloudFormation/SAM template snippets, actual alarm
|
|
101
|
+
# creation should be done via IaC (CloudFormation, Terraform, etc.)
|
|
102
|
+
#
|
|
103
|
+
# @example Generate alarm configuration
|
|
104
|
+
# alarm = LambdaLoadout::AlarmConfig.new(
|
|
105
|
+
# metric_name: "LambdaError",
|
|
106
|
+
# namespace: "MyApp",
|
|
107
|
+
# threshold: 1,
|
|
108
|
+
# evaluation_periods: 1
|
|
109
|
+
# )
|
|
110
|
+
#
|
|
111
|
+
# puts alarm.to_cloudformation
|
|
112
|
+
class AlarmConfig
|
|
113
|
+
attr_reader :metric_name, :namespace, :threshold, :statistic,
|
|
114
|
+
:evaluation_periods, :period, :comparison_operator
|
|
115
|
+
|
|
116
|
+
# Initialize alarm configuration
|
|
117
|
+
#
|
|
118
|
+
# @param metric_name [String] CloudWatch metric name
|
|
119
|
+
# @param namespace [String] CloudWatch metric namespace
|
|
120
|
+
# @param threshold [Numeric] Alarm threshold
|
|
121
|
+
# @param statistic [String] Statistic type (Sum, Average, Maximum, etc.)
|
|
122
|
+
# @param evaluation_periods [Integer] Number of periods to evaluate
|
|
123
|
+
# @param period [Integer] Period in seconds
|
|
124
|
+
# @param comparison_operator [String] Comparison operator
|
|
125
|
+
def initialize(
|
|
126
|
+
metric_name:,
|
|
127
|
+
namespace:,
|
|
128
|
+
threshold: 1,
|
|
129
|
+
statistic: 'Sum',
|
|
130
|
+
evaluation_periods: 1,
|
|
131
|
+
period: 60,
|
|
132
|
+
comparison_operator: 'GreaterThanOrEqualToThreshold'
|
|
133
|
+
)
|
|
134
|
+
@metric_name = metric_name
|
|
135
|
+
@namespace = namespace
|
|
136
|
+
@threshold = threshold
|
|
137
|
+
@statistic = statistic
|
|
138
|
+
@evaluation_periods = evaluation_periods
|
|
139
|
+
@period = period
|
|
140
|
+
@comparison_operator = comparison_operator
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Generate CloudFormation YAML for alarm
|
|
144
|
+
#
|
|
145
|
+
# @param alarm_name [String] CloudFormation logical name
|
|
146
|
+
# @param sns_topic_arn [String] SNS topic ARN for notifications (optional)
|
|
147
|
+
# @param dimensions [Hash] Metric dimensions
|
|
148
|
+
# @return [String] CloudFormation YAML snippet
|
|
149
|
+
def to_cloudformation(alarm_name: "#{metric_name}Alarm", sns_topic_arn: nil, dimensions: {})
|
|
150
|
+
cf = {
|
|
151
|
+
alarm_name => {
|
|
152
|
+
'Type' => 'AWS::CloudWatch::Alarm',
|
|
153
|
+
'Properties' => {
|
|
154
|
+
'AlarmName' => alarm_name,
|
|
155
|
+
'AlarmDescription' => "Alarm for #{metric_name} in #{namespace}",
|
|
156
|
+
'MetricName' => metric_name,
|
|
157
|
+
'Namespace' => namespace,
|
|
158
|
+
'Statistic' => statistic,
|
|
159
|
+
'Period' => period,
|
|
160
|
+
'EvaluationPeriods' => evaluation_periods,
|
|
161
|
+
'Threshold' => threshold,
|
|
162
|
+
'ComparisonOperator' => comparison_operator,
|
|
163
|
+
'TreatMissingData' => 'notBreaching'
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# Add dimensions if provided
|
|
169
|
+
unless dimensions.empty?
|
|
170
|
+
dims = dimensions.map { |k, v| { 'Name' => k.to_s, 'Value' => v.to_s } }
|
|
171
|
+
cf[alarm_name]['Properties']['Dimensions'] = dims
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Add SNS notification if provided
|
|
175
|
+
cf[alarm_name]['Properties']['AlarmActions'] = [sns_topic_arn] if sns_topic_arn
|
|
176
|
+
|
|
177
|
+
require 'yaml'
|
|
178
|
+
YAML.dump(cf)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Generate Terraform HCL for alarm
|
|
182
|
+
#
|
|
183
|
+
# @param resource_name [String] Terraform resource name
|
|
184
|
+
# @param sns_topic_arn [String] SNS topic ARN for notifications (optional)
|
|
185
|
+
# @param dimensions [Hash] Metric dimensions
|
|
186
|
+
# @return [String] Terraform HCL snippet
|
|
187
|
+
def to_terraform(resource_name: metric_name.downcase, sns_topic_arn: nil, dimensions: {})
|
|
188
|
+
hcl = <<~HCL
|
|
189
|
+
resource "aws_cloudwatch_metric_alarm" "#{resource_name}" {
|
|
190
|
+
alarm_name = "#{resource_name}"
|
|
191
|
+
alarm_description = "Alarm for #{metric_name} in #{namespace}"
|
|
192
|
+
comparison_operator = "#{comparison_operator}"
|
|
193
|
+
evaluation_periods = #{evaluation_periods}
|
|
194
|
+
metric_name = "#{metric_name}"
|
|
195
|
+
namespace = "#{namespace}"
|
|
196
|
+
period = #{period}
|
|
197
|
+
statistic = "#{statistic}"
|
|
198
|
+
threshold = #{threshold}
|
|
199
|
+
treat_missing_data = "notBreaching"
|
|
200
|
+
HCL
|
|
201
|
+
|
|
202
|
+
# Add dimensions if provided
|
|
203
|
+
unless dimensions.empty?
|
|
204
|
+
hcl += "\n dimensions = {\n"
|
|
205
|
+
dimensions.each do |k, v|
|
|
206
|
+
hcl += " #{k} = \"#{v}\"\n"
|
|
207
|
+
end
|
|
208
|
+
hcl += " }\n"
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Add SNS notification if provided
|
|
212
|
+
hcl += "\n alarm_actions = [\"#{sns_topic_arn}\"]\n" if sns_topic_arn
|
|
213
|
+
|
|
214
|
+
hcl += "}\n"
|
|
215
|
+
hcl
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LambdaLoadout
|
|
4
|
+
# Module-level convenience methods for quick access
|
|
5
|
+
#
|
|
6
|
+
# Provides a simple API similar to common logging modules:
|
|
7
|
+
# LambdaLoadout.logger.info("message")
|
|
8
|
+
# LambdaLoadout.metrics.add_metric(...)
|
|
9
|
+
#
|
|
10
|
+
# @example Module-level usage
|
|
11
|
+
# LambdaLoadout.configure do |config|
|
|
12
|
+
# config.service = "my-service"
|
|
13
|
+
# config.namespace = "MyApp"
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# LambdaLoadout.logger.info("Processing request")
|
|
17
|
+
# LambdaLoadout.metrics.add_metric(name: "Request", unit: "Count", value: 1)
|
|
18
|
+
module Global
|
|
19
|
+
class << self
|
|
20
|
+
attr_writer :logger, :metrics
|
|
21
|
+
|
|
22
|
+
# Configuration
|
|
23
|
+
attr_accessor :service, :namespace, :log_level
|
|
24
|
+
|
|
25
|
+
# Get or create the global logger instance
|
|
26
|
+
#
|
|
27
|
+
# @return [LambdaLoadout::Logger]
|
|
28
|
+
def logger
|
|
29
|
+
@logger ||= LambdaLoadout::Logger.new(
|
|
30
|
+
service: service || ENV.fetch('POWERTOOLS_SERVICE_NAME', nil),
|
|
31
|
+
level: log_level || :info
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Get or create the global metrics instance
|
|
36
|
+
#
|
|
37
|
+
# @return [LambdaLoadout::Metrics]
|
|
38
|
+
def metrics
|
|
39
|
+
@metrics ||= LambdaLoadout::Metrics.new(
|
|
40
|
+
namespace: namespace || ENV['POWERTOOLS_METRICS_NAMESPACE'] || 'LambdaLoadout',
|
|
41
|
+
service: service || ENV.fetch('POWERTOOLS_SERVICE_NAME', nil)
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Reset global instances (useful for testing)
|
|
46
|
+
#
|
|
47
|
+
# @return [void]
|
|
48
|
+
def reset!
|
|
49
|
+
@logger = nil
|
|
50
|
+
@metrics = nil
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Configure LambdaLoadout globally
|
|
56
|
+
#
|
|
57
|
+
# @yield [Global] Configuration block
|
|
58
|
+
# @return [void]
|
|
59
|
+
#
|
|
60
|
+
# @example
|
|
61
|
+
# LambdaLoadout.configure do |config|
|
|
62
|
+
# config.service = "payment-api"
|
|
63
|
+
# config.namespace = "MyApp"
|
|
64
|
+
# config.log_level = :debug
|
|
65
|
+
# end
|
|
66
|
+
def self.configure
|
|
67
|
+
yield Global
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Access global logger
|
|
71
|
+
#
|
|
72
|
+
# @return [LambdaLoadout::Logger]
|
|
73
|
+
def self.logger
|
|
74
|
+
Global.logger
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Access global metrics
|
|
78
|
+
#
|
|
79
|
+
# @return [LambdaLoadout::Metrics]
|
|
80
|
+
def self.metrics
|
|
81
|
+
Global.metrics
|
|
82
|
+
end
|
|
83
|
+
end
|