cloudwatch_query 0.1.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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +298 -0
- data/Rakefile +8 -0
- data/cloudwatch_query.gemspec +31 -0
- data/lib/cloudwatch_query/client.rb +111 -0
- data/lib/cloudwatch_query/configuration.rb +14 -0
- data/lib/cloudwatch_query/errors.rb +9 -0
- data/lib/cloudwatch_query/parsers/base.rb +24 -0
- data/lib/cloudwatch_query/parsers/rails/rails_log.rb +72 -0
- data/lib/cloudwatch_query/parsers/rails/sub_parsers.rb +175 -0
- data/lib/cloudwatch_query/parsers/rails_parser.rb +144 -0
- data/lib/cloudwatch_query/parsers/registry.rb +93 -0
- data/lib/cloudwatch_query/parsers/sidekiq/sidekiq_log.rb +53 -0
- data/lib/cloudwatch_query/parsers/sidekiq/sub_parsers.rb +78 -0
- data/lib/cloudwatch_query/parsers/sidekiq_parser.rb +130 -0
- data/lib/cloudwatch_query/parsers.rb +19 -0
- data/lib/cloudwatch_query/query.rb +147 -0
- data/lib/cloudwatch_query/result.rb +68 -0
- data/lib/cloudwatch_query/result_set.rb +62 -0
- data/lib/cloudwatch_query/time_helpers.rb +56 -0
- data/lib/cloudwatch_query/version.rb +5 -0
- data/lib/cloudwatch_query.rb +61 -0
- metadata +112 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 467d68c5e50ddf28a0be2b7a5e54879c4a2ff0083146a71b359d8d7d7537d485
|
|
4
|
+
data.tar.gz: 64475bd16a828fb65cd29f18449c81c4bec31e09c9257c35a1f8e635e0bd639b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: aaa0b07437366f8d047b214391e427eaeffd7b9de295c4f80dacd0e57c5462706e21a81beee0050c7bf9259808140be99cd89af1c0635a92329ee1a11afdd534
|
|
7
|
+
data.tar.gz: 23f498ab01846a21e60e821eb51415ffdf758f5720d7930e014689a4f058509bb1f3134d10ca4f0db4c82b98ba5458c8939ba90fabefdba83ff5f27479e5e066
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 cloudwatch_query contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# CloudwatchQuery
|
|
2
|
+
|
|
3
|
+
A Ruby gem for querying AWS CloudWatch Logs with a simple, chainable interface.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add to your Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem "cloudwatch_query"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or install directly:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
gem install cloudwatch_query
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Prerequisites
|
|
20
|
+
|
|
21
|
+
AWS credentials must be configured. This gem uses the standard AWS credential chain:
|
|
22
|
+
|
|
23
|
+
1. Environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`)
|
|
24
|
+
2. Shared credentials file (`~/.aws/credentials`)
|
|
25
|
+
3. EC2 instance metadata
|
|
26
|
+
|
|
27
|
+
## Configuration
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
CloudwatchQuery.configure do |config|
|
|
31
|
+
config.region = "us-west-1" # Default: ENV["AWS_REGION"] or "us-west-1"
|
|
32
|
+
config.default_limit = 100 # Default result limit
|
|
33
|
+
config.default_time_range = 3600 # Default lookback in seconds
|
|
34
|
+
end
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
### Basic Query
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
CloudwatchQuery
|
|
43
|
+
.logs("my-app-rails-log")
|
|
44
|
+
.where(level: "ERROR")
|
|
45
|
+
.past(30, :minutes)
|
|
46
|
+
.each { |event| puts event.message }
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Quick Search
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
CloudwatchQuery.search(
|
|
53
|
+
"OutOfMemoryError",
|
|
54
|
+
groups: "my-app-rails-log",
|
|
55
|
+
since: 2.hours.ago,
|
|
56
|
+
limit: 50
|
|
57
|
+
)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Multiple Log Groups
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
CloudwatchQuery
|
|
64
|
+
.logs(
|
|
65
|
+
"my-app-rails-log",
|
|
66
|
+
"my-app-sidekiq-log"
|
|
67
|
+
)
|
|
68
|
+
.contains("database")
|
|
69
|
+
.where(level: "ERROR")
|
|
70
|
+
.past(1, :hours)
|
|
71
|
+
.execute
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Time Range Options
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
query = CloudwatchQuery.logs("my-app-rails-log")
|
|
78
|
+
|
|
79
|
+
# Relative time
|
|
80
|
+
query.past(30, :minutes)
|
|
81
|
+
query.past(2, :hours)
|
|
82
|
+
query.past(7, :days)
|
|
83
|
+
|
|
84
|
+
# Absolute time
|
|
85
|
+
query.since(Time.now - 3600)
|
|
86
|
+
query.since(1.hour.ago) # With ActiveSupport
|
|
87
|
+
query.between(start_time, end_time)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Available time units (singular and plural forms accepted):
|
|
91
|
+
|
|
92
|
+
| Unit | Symbols |
|
|
93
|
+
|------|---------|
|
|
94
|
+
| Seconds | `:second`, `:seconds` |
|
|
95
|
+
| Minutes | `:minute`, `:minutes` |
|
|
96
|
+
| Hours | `:hour`, `:hours` |
|
|
97
|
+
| Days | `:day`, `:days` |
|
|
98
|
+
| Weeks | `:week`, `:weeks` |
|
|
99
|
+
|
|
100
|
+
### Field Selection
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
CloudwatchQuery
|
|
104
|
+
.logs("my-app-rails-log")
|
|
105
|
+
.fields(:timestamp, :message, :logStream)
|
|
106
|
+
.past(1, :hours)
|
|
107
|
+
.execute
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Query Inspection
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
query = CloudwatchQuery
|
|
114
|
+
.logs("my-app-rails-log")
|
|
115
|
+
.where(level: "ERROR")
|
|
116
|
+
.contains("timeout")
|
|
117
|
+
.limit(50)
|
|
118
|
+
|
|
119
|
+
puts query.to_insights_query
|
|
120
|
+
# => fields @timestamp, @message, @logStream, @log | filter level = 'ERROR' | filter @message like /timeout/ | sort @timestamp desc | limit 50
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Working with Results
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
results = CloudwatchQuery
|
|
127
|
+
.logs("my-app-rails-log")
|
|
128
|
+
.past(1, :hours)
|
|
129
|
+
.execute
|
|
130
|
+
|
|
131
|
+
# Enumerable methods
|
|
132
|
+
results.each { |r| puts r.message }
|
|
133
|
+
results.count
|
|
134
|
+
results.empty?
|
|
135
|
+
|
|
136
|
+
# Access fields
|
|
137
|
+
results.first.timestamp
|
|
138
|
+
results.first.message
|
|
139
|
+
results.first[:logStream]
|
|
140
|
+
results.first.to_h
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Parsers
|
|
144
|
+
|
|
145
|
+
Results can be automatically parsed into structured objects. The gem ships with two built-in parsers: `RailsParser` and `SidekiqParser`. Parsers are registered globally and run automatically when results are returned.
|
|
146
|
+
|
|
147
|
+
#### How It Works
|
|
148
|
+
|
|
149
|
+
Each `Result` has a `parsed` attribute. When a query executes, every result's `message` is passed through the parser registry. The first parser whose `matches?` returns true parses the message into a structured log object (`RailsLog` or `SidekiqLog`).
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
results = CloudwatchQuery
|
|
153
|
+
.logs("my-app-rails-log")
|
|
154
|
+
.past(30, :minutes)
|
|
155
|
+
.execute
|
|
156
|
+
|
|
157
|
+
result = results.first
|
|
158
|
+
result.parsed? # => true
|
|
159
|
+
result.parser_name # => "rails"
|
|
160
|
+
result.log_type # => :rails
|
|
161
|
+
result.parsed # => RailsLog instance
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
#### Rails Parser
|
|
165
|
+
|
|
166
|
+
Recognizes log lines containing a Rails request UUID (`[abc-123-def-456]`). Each line is further classified by sub-parsers:
|
|
167
|
+
|
|
168
|
+
| Sub-parser | Matches | Fields |
|
|
169
|
+
|------------|---------|--------|
|
|
170
|
+
| `:request` | `Started GET "/path" for 1.2.3.4` | `http_method`, `path`, `ip_address` |
|
|
171
|
+
| `:parameters` | `Parameters: {...}` | `params` |
|
|
172
|
+
| `:processing` | `Processing by Controller#action as HTML` | `controller`, `action`, `format` |
|
|
173
|
+
| `:completed` | `Completed 200 OK in 123ms` | `status_code`, `duration_ms` |
|
|
174
|
+
| `:redirect` | `Redirected to https://...` | `redirect_url` |
|
|
175
|
+
| `:active_job` | `[ActiveJob] Enqueued JobClass (Job ID: ...)` | `job_class`, `job_id`, `queue` |
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
result.parsed.line_type # => :request
|
|
179
|
+
result.parsed.http_method # => "GET"
|
|
180
|
+
result.parsed.path # => "/users/1"
|
|
181
|
+
result.parsed.request_id # => "abc-123-def-456"
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Use only specific sub-parsers:
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
CloudwatchQuery.parsers.clear
|
|
188
|
+
CloudwatchQuery.parsers.register(
|
|
189
|
+
CloudwatchQuery::Parsers::RailsParser.new(:request, :completed)
|
|
190
|
+
)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
#### Sidekiq Parser
|
|
194
|
+
|
|
195
|
+
Recognizes Sidekiq log lines (`2026-02-04T20:46:15.201Z pid=123 tid=abc class=Job jid=xyz`). Sub-parsers:
|
|
196
|
+
|
|
197
|
+
| Sub-parser | Matches | Fields |
|
|
198
|
+
|------------|---------|--------|
|
|
199
|
+
| `:start` | `INFO: start` | `status` |
|
|
200
|
+
| `:done` | `INFO: done` | `status`, `elapsed` |
|
|
201
|
+
| `:fail` | `INFO: fail` or `ERROR:` | `status` |
|
|
202
|
+
|
|
203
|
+
```ruby
|
|
204
|
+
result.parsed.line_type # => :done
|
|
205
|
+
result.parsed.job_class # => "SendEmailJob"
|
|
206
|
+
result.parsed.elapsed # => 0.152
|
|
207
|
+
result.parsed.jid # => "abc123"
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
#### Filtering Parsed Results
|
|
211
|
+
|
|
212
|
+
```ruby
|
|
213
|
+
results.parsed # only successfully parsed results
|
|
214
|
+
results.unparsed # results that no parser matched
|
|
215
|
+
results.by_type(:rails) # filter by log type
|
|
216
|
+
results.group_by_request # group Rails logs by request_id
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
#### Custom Parsers
|
|
220
|
+
|
|
221
|
+
Create a parser that responds to `matches?` and `parse`:
|
|
222
|
+
|
|
223
|
+
```ruby
|
|
224
|
+
class MyParser
|
|
225
|
+
def matches?(message)
|
|
226
|
+
message.include?("[CUSTOM]")
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def parse(message)
|
|
230
|
+
OpenStruct.new(type: :custom, body: message)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def parser_name
|
|
234
|
+
"custom"
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
CloudwatchQuery.parsers.register(MyParser.new)
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Registry methods: `register`, `prepend`, `insert`, `unregister`, `clear`, `list`.
|
|
242
|
+
|
|
243
|
+
### List Log Groups
|
|
244
|
+
|
|
245
|
+
```ruby
|
|
246
|
+
CloudwatchQuery.list_log_groups(prefix: "production-")
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## API Reference
|
|
250
|
+
|
|
251
|
+
### Query Builder Methods
|
|
252
|
+
|
|
253
|
+
| Method | Description |
|
|
254
|
+
|--------|-------------|
|
|
255
|
+
| `.logs(*groups)` | Select log groups to query |
|
|
256
|
+
| `.where(**conditions)` | Filter by field equality |
|
|
257
|
+
| `.contains(text)` | Filter messages containing text |
|
|
258
|
+
| `.matches(pattern)` | Filter messages matching regex |
|
|
259
|
+
| `.since(time)` | Set start time |
|
|
260
|
+
| `.before(time)` | Set end time |
|
|
261
|
+
| `.between(start, end)` | Set time range |
|
|
262
|
+
| `.past(amount, unit)` | Relative time (e.g., `past(30, :minutes)`) |
|
|
263
|
+
| `.fields(*fields)` | Select fields to return |
|
|
264
|
+
| `.limit(n)` | Limit number of results |
|
|
265
|
+
| `.execute` | Run query and return ResultSet |
|
|
266
|
+
| `.to_insights_query` | Return generated query string |
|
|
267
|
+
|
|
268
|
+
### Result Methods
|
|
269
|
+
|
|
270
|
+
| Method | Description |
|
|
271
|
+
|--------|-------------|
|
|
272
|
+
| `.timestamp` | Log event timestamp |
|
|
273
|
+
| `.message` | Log message content |
|
|
274
|
+
| `.[](key)` | Access field by name |
|
|
275
|
+
| `.to_h` | Convert to hash |
|
|
276
|
+
|
|
277
|
+
## Error Handling
|
|
278
|
+
|
|
279
|
+
```ruby
|
|
280
|
+
begin
|
|
281
|
+
results = CloudwatchQuery
|
|
282
|
+
.logs("my-log-group")
|
|
283
|
+
.past(1, :hours)
|
|
284
|
+
.execute
|
|
285
|
+
rescue CloudwatchQuery::AuthError => e
|
|
286
|
+
puts "Authentication failed: #{e.message}"
|
|
287
|
+
rescue CloudwatchQuery::QueryError => e
|
|
288
|
+
puts "Query failed: #{e.message}"
|
|
289
|
+
rescue CloudwatchQuery::TimeoutError => e
|
|
290
|
+
puts "Query timed out: #{e.message}"
|
|
291
|
+
rescue CloudwatchQuery::Error => e
|
|
292
|
+
puts "Error: #{e.message}"
|
|
293
|
+
end
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## License
|
|
297
|
+
|
|
298
|
+
MIT
|
data/Rakefile
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/cloudwatch_query/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "cloudwatch_query"
|
|
7
|
+
spec.version = CloudwatchQuery::VERSION
|
|
8
|
+
spec.authors = ["Igor Irianto"]
|
|
9
|
+
|
|
10
|
+
spec.summary = "A Ruby gem for querying AWS CloudWatch Logs with a simple, chainable interface"
|
|
11
|
+
spec.description = "Query AWS CloudWatch Logs using a fluent, ActiveRecord-style interface. " \
|
|
12
|
+
"Supports chainable queries, automatic Insights query generation, and enumerable results."
|
|
13
|
+
spec.homepage = "https://github.com/iggredible/cloudwatch_query"
|
|
14
|
+
spec.license = "MIT"
|
|
15
|
+
spec.required_ruby_version = ">= 2.7.0"
|
|
16
|
+
|
|
17
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
18
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
|
19
|
+
spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
|
|
20
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
21
|
+
|
|
22
|
+
spec.files = Dir.chdir(__dir__) do
|
|
23
|
+
Dir["lib/**/*", "LICENSE.txt", "README.md", "Rakefile", "cloudwatch_query.gemspec"]
|
|
24
|
+
end
|
|
25
|
+
spec.require_paths = ["lib"]
|
|
26
|
+
|
|
27
|
+
spec.add_dependency "aws-sdk-cloudwatchlogs", "~> 1.0"
|
|
28
|
+
|
|
29
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
|
30
|
+
spec.add_development_dependency "webmock", "~> 3.0"
|
|
31
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "aws-sdk-cloudwatchlogs"
|
|
4
|
+
|
|
5
|
+
module CloudwatchQuery
|
|
6
|
+
class Client
|
|
7
|
+
DEFAULT_POLL_INTERVAL = 1
|
|
8
|
+
DEFAULT_TIMEOUT = 60
|
|
9
|
+
|
|
10
|
+
attr_reader :aws_client
|
|
11
|
+
|
|
12
|
+
def initialize(region: nil, profile: nil)
|
|
13
|
+
config = CloudwatchQuery.configuration
|
|
14
|
+
client_options = {
|
|
15
|
+
region: region || config.region
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
# AWS SDK automatically picks up credentials from:
|
|
19
|
+
# 1. Environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)
|
|
20
|
+
# 2. Shared credentials file (~/.aws/credentials) - set by saml2aws
|
|
21
|
+
# 3. EC2 instance metadata
|
|
22
|
+
@aws_client = Aws::CloudWatchLogs::Client.new(client_options)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def execute(query_string:, log_group_names:, start_time:, end_time:, limit:)
|
|
26
|
+
query_id = start_query(
|
|
27
|
+
query_string: query_string,
|
|
28
|
+
log_group_names: log_group_names,
|
|
29
|
+
start_time: start_time,
|
|
30
|
+
end_time: end_time,
|
|
31
|
+
limit: limit
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
poll_results(query_id)
|
|
35
|
+
rescue Aws::CloudWatchLogs::Errors::ServiceError => e
|
|
36
|
+
handle_aws_error(e)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def list_log_groups(prefix: nil)
|
|
40
|
+
options = {}
|
|
41
|
+
options[:log_group_name_prefix] = prefix if prefix
|
|
42
|
+
|
|
43
|
+
groups = []
|
|
44
|
+
aws_client.describe_log_groups(options).each_page do |page|
|
|
45
|
+
groups.concat(page.log_groups.map(&:log_group_name))
|
|
46
|
+
end
|
|
47
|
+
groups
|
|
48
|
+
rescue Aws::CloudWatchLogs::Errors::ServiceError => e
|
|
49
|
+
handle_aws_error(e)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def start_query(query_string:, log_group_names:, start_time:, end_time:, limit:)
|
|
55
|
+
response = aws_client.start_query(
|
|
56
|
+
log_group_names: Array(log_group_names),
|
|
57
|
+
start_time: start_time,
|
|
58
|
+
end_time: end_time,
|
|
59
|
+
query_string: query_string,
|
|
60
|
+
limit: limit
|
|
61
|
+
)
|
|
62
|
+
response.query_id
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def poll_results(query_id, timeout: DEFAULT_TIMEOUT)
|
|
66
|
+
deadline = Time.now + timeout
|
|
67
|
+
|
|
68
|
+
loop do
|
|
69
|
+
raise TimeoutError, "Query timed out after #{timeout} seconds" if Time.now > deadline
|
|
70
|
+
|
|
71
|
+
response = aws_client.get_query_results(query_id: query_id)
|
|
72
|
+
|
|
73
|
+
case response.status
|
|
74
|
+
when "Complete"
|
|
75
|
+
return build_result_set(response)
|
|
76
|
+
when "Failed", "Cancelled"
|
|
77
|
+
raise QueryError, "Query #{response.status.downcase}"
|
|
78
|
+
when "Running", "Scheduled"
|
|
79
|
+
sleep(DEFAULT_POLL_INTERVAL)
|
|
80
|
+
else
|
|
81
|
+
raise QueryError, "Unknown query status: #{response.status}"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def build_result_set(response)
|
|
87
|
+
results = response.results.map do |row|
|
|
88
|
+
row.each_with_object({}) do |field, hash|
|
|
89
|
+
key = field.field.sub(/^@/, "")
|
|
90
|
+
hash[key] = field.value
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
ResultSet.new(
|
|
95
|
+
results: results,
|
|
96
|
+
statistics: response.statistics&.to_h || {},
|
|
97
|
+
registry: CloudwatchQuery.parsers
|
|
98
|
+
)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def handle_aws_error(error)
|
|
102
|
+
case error
|
|
103
|
+
when Aws::CloudWatchLogs::Errors::AccessDeniedException,
|
|
104
|
+
Aws::CloudWatchLogs::Errors::UnauthorizedException
|
|
105
|
+
raise AuthError, "AWS authentication failed: #{error.message}"
|
|
106
|
+
else
|
|
107
|
+
raise QueryError, "AWS error: #{error.message}"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CloudwatchQuery
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :region, :profile, :default_limit, :default_time_range
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@region = ENV["AWS_REGION"] || "us-west-1"
|
|
9
|
+
@profile = ENV["AWS_PROFILE"]
|
|
10
|
+
@default_limit = 100
|
|
11
|
+
@default_time_range = 3600
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CloudwatchQuery
|
|
4
|
+
module Parsers
|
|
5
|
+
class Base
|
|
6
|
+
class << self
|
|
7
|
+
# Override in subclass - return true if this parser can handle the message
|
|
8
|
+
def matches?(message)
|
|
9
|
+
raise NotImplementedError, "#{name} must implement .matches?(message)"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Override in subclass - parse the message and return a structured object
|
|
13
|
+
def parse(message)
|
|
14
|
+
raise NotImplementedError, "#{name} must implement .parse(message)"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Human-readable name for this parser
|
|
18
|
+
def parser_name
|
|
19
|
+
name.split("::").last.sub(/Parser$/, "").gsub(/([a-z])([A-Z])/, '\1_\2').downcase
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CloudwatchQuery
|
|
4
|
+
module Parsers
|
|
5
|
+
module Rails
|
|
6
|
+
class RailsLog
|
|
7
|
+
attr_reader :request_id, :line_type, :server, :process_id,
|
|
8
|
+
# Request fields
|
|
9
|
+
:http_method, :path, :ip_address, :request_timestamp,
|
|
10
|
+
# Parameters fields
|
|
11
|
+
:params,
|
|
12
|
+
# Redirect fields
|
|
13
|
+
:redirect_url,
|
|
14
|
+
# ActiveJob fields
|
|
15
|
+
:job_class, :job_id, :queue, :arguments,
|
|
16
|
+
# Processing fields
|
|
17
|
+
:controller, :action, :format,
|
|
18
|
+
# Completed fields
|
|
19
|
+
:status_code, :duration_ms,
|
|
20
|
+
# Raw message for unparsed line types
|
|
21
|
+
:raw_message
|
|
22
|
+
|
|
23
|
+
def initialize(attributes = {})
|
|
24
|
+
attributes.each do |key, value|
|
|
25
|
+
instance_variable_set("@#{key}", value) if respond_to?(key)
|
|
26
|
+
end
|
|
27
|
+
@line_type ||= :unknown
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def type
|
|
31
|
+
:rails
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def request?
|
|
35
|
+
line_type == :request
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def parameters?
|
|
39
|
+
line_type == :parameters
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def redirect?
|
|
43
|
+
line_type == :redirect
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def active_job?
|
|
47
|
+
line_type == :active_job
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def processing?
|
|
51
|
+
line_type == :processing
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def completed?
|
|
55
|
+
line_type == :completed
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def to_h
|
|
59
|
+
instance_variables.each_with_object({}) do |var, hash|
|
|
60
|
+
key = var.to_s.delete("@").to_sym
|
|
61
|
+
value = instance_variable_get(var)
|
|
62
|
+
hash[key] = value unless value.nil?
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def [](key)
|
|
67
|
+
instance_variable_get("@#{key}")
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|