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.
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CloudwatchQuery
4
+ class Query
5
+ include Enumerable
6
+
7
+ DEFAULT_FIELDS = %w[@timestamp @message @logStream @log].freeze
8
+
9
+ def initialize
10
+ @log_groups = []
11
+ @conditions = []
12
+ @fields = DEFAULT_FIELDS.dup
13
+ @start_time = nil
14
+ @end_time = nil
15
+ @limit = nil
16
+ @sort_field = "@timestamp"
17
+ @sort_order = "desc"
18
+ end
19
+
20
+ # Log group selection
21
+ def logs(*groups)
22
+ @log_groups.concat(groups.flatten)
23
+ self
24
+ end
25
+ alias log_group logs
26
+
27
+ # Filtering
28
+ def where(**conditions)
29
+ conditions.each do |field, value|
30
+ field_name = field.to_s.start_with?("@") ? field.to_s : field.to_s
31
+ @conditions << "#{field_name} = '#{escape_value(value)}'"
32
+ end
33
+ self
34
+ end
35
+
36
+ def contains(text)
37
+ @conditions << "@message like /#{escape_regex(text)}/"
38
+ self
39
+ end
40
+
41
+ def matches(pattern)
42
+ @conditions << "@message like /#{pattern}/"
43
+ self
44
+ end
45
+
46
+ # Time range
47
+ def since(time)
48
+ @start_time = TimeHelpers.to_epoch(time)
49
+ self
50
+ end
51
+
52
+ def before(time)
53
+ @end_time = TimeHelpers.to_epoch(time)
54
+ self
55
+ end
56
+
57
+ def between(start_time, end_time)
58
+ @start_time = TimeHelpers.to_epoch(start_time)
59
+ @end_time = TimeHelpers.to_epoch(end_time)
60
+ self
61
+ end
62
+
63
+ def past(amount, unit)
64
+ seconds = TimeHelpers.duration_in_seconds(amount, unit)
65
+ @start_time = (Time.now - seconds).to_i
66
+ @end_time = Time.now.to_i
67
+ self
68
+ end
69
+
70
+ # Field selection
71
+ def fields(*field_list)
72
+ @fields = field_list.flatten.map { |f| f.to_s.start_with?("@") ? f.to_s : "@#{f}" }
73
+ self
74
+ end
75
+
76
+ def limit(n)
77
+ @limit = n
78
+ self
79
+ end
80
+
81
+ def sort(field, order = :desc)
82
+ @sort_field = field.to_s.start_with?("@") ? field.to_s : "@#{field}"
83
+ @sort_order = order.to_s
84
+ self
85
+ end
86
+
87
+ # Execution
88
+ def execute
89
+ validate!
90
+ client.execute(
91
+ query_string: to_insights_query,
92
+ log_group_names: @log_groups,
93
+ start_time: resolved_start_time,
94
+ end_time: resolved_end_time,
95
+ limit: resolved_limit
96
+ )
97
+ end
98
+ alias to_a execute
99
+
100
+ def each(&block)
101
+ execute.each(&block)
102
+ end
103
+
104
+ def to_insights_query
105
+ parts = []
106
+ parts << "fields #{@fields.join(', ')}"
107
+ @conditions.each { |c| parts << "filter #{c}" }
108
+ parts << "sort #{@sort_field} #{@sort_order}"
109
+ parts << "limit #{resolved_limit}" if resolved_limit
110
+ parts.join(" | ")
111
+ end
112
+
113
+ private
114
+
115
+ def client
116
+ @client ||= Client.new
117
+ end
118
+
119
+ def config
120
+ CloudwatchQuery.configuration
121
+ end
122
+
123
+ def resolved_start_time
124
+ @start_time || (Time.now - config.default_time_range).to_i
125
+ end
126
+
127
+ def resolved_end_time
128
+ @end_time || Time.now.to_i
129
+ end
130
+
131
+ def resolved_limit
132
+ @limit || config.default_limit
133
+ end
134
+
135
+ def validate!
136
+ raise ConfigError, "No log groups specified" if @log_groups.empty?
137
+ end
138
+
139
+ def escape_value(value)
140
+ value.to_s.gsub("'", "\\\\'")
141
+ end
142
+
143
+ def escape_regex(text)
144
+ Regexp.escape(text.to_s)
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CloudwatchQuery
4
+ class Result
5
+ attr_reader :data, :parsed, :parser_name
6
+
7
+ def initialize(data, registry: nil)
8
+ @data = data.transform_keys(&:to_sym)
9
+ @registry = registry
10
+ @parsed = nil
11
+ @parser_name = nil
12
+ parse_message! if @registry
13
+ end
14
+
15
+ def timestamp
16
+ @data[:timestamp]
17
+ end
18
+
19
+ def message
20
+ @data[:message]
21
+ end
22
+
23
+ def log_stream
24
+ @data[:logStream]
25
+ end
26
+
27
+ def log
28
+ @data[:log]
29
+ end
30
+
31
+ def [](key)
32
+ @data[key.to_sym]
33
+ end
34
+
35
+ def to_h
36
+ @data.dup
37
+ end
38
+
39
+ # Check if message was successfully parsed
40
+ def parsed?
41
+ !@parsed.nil?
42
+ end
43
+
44
+ # Get the type of parsed log (e.g., :rails_request, :sidekiq)
45
+ def log_type
46
+ @parsed&.type
47
+ end
48
+
49
+ def respond_to_missing?(method_name, include_private = false)
50
+ @data.key?(method_name.to_sym) || super
51
+ end
52
+
53
+ def method_missing(method_name, *args)
54
+ key = method_name.to_sym
55
+ return @data[key] if @data.key?(key)
56
+
57
+ super
58
+ end
59
+
60
+ private
61
+
62
+ def parse_message!
63
+ return unless message
64
+
65
+ @parsed, @parser_name = @registry.parse(message)
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CloudwatchQuery
4
+ class ResultSet
5
+ include Enumerable
6
+
7
+ attr_reader :results, :statistics
8
+
9
+ def initialize(results: [], statistics: {}, registry: nil)
10
+ @results = results.map { |r| Result.new(r, registry: registry) }
11
+ @statistics = statistics
12
+ end
13
+
14
+ def each(&block)
15
+ @results.each(&block)
16
+ end
17
+
18
+ def count
19
+ @results.count
20
+ end
21
+ alias size count
22
+ alias length count
23
+
24
+ def empty?
25
+ @results.empty?
26
+ end
27
+
28
+ def first(n = nil)
29
+ n ? @results.first(n) : @results.first
30
+ end
31
+
32
+ def last(n = nil)
33
+ n ? @results.last(n) : @results.last
34
+ end
35
+
36
+ def to_a
37
+ @results
38
+ end
39
+
40
+ # Filter results by log type
41
+ def by_type(type)
42
+ @results.select { |r| r.log_type == type }
43
+ end
44
+
45
+ # Get only parsed results
46
+ def parsed
47
+ @results.select(&:parsed?)
48
+ end
49
+
50
+ # Get only unparsed results
51
+ def unparsed
52
+ @results.reject(&:parsed?)
53
+ end
54
+
55
+ # Group results by request_id (for Rails logs)
56
+ def group_by_request
57
+ @results
58
+ .select { |r| r.parsed&.respond_to?(:request_id) && r.parsed.request_id }
59
+ .group_by { |r| r.parsed.request_id }
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CloudwatchQuery
4
+ module TimeHelpers
5
+ UNIT_MULTIPLIERS = {
6
+ seconds: 1,
7
+ second: 1,
8
+ minutes: 60,
9
+ minute: 60,
10
+ hours: 3600,
11
+ hour: 3600,
12
+ days: 86400,
13
+ day: 86400,
14
+ weeks: 604800,
15
+ week: 604800
16
+ }.freeze
17
+
18
+ def self.duration_in_seconds(amount, unit)
19
+ multiplier = UNIT_MULTIPLIERS[unit.to_sym]
20
+ raise ConfigError, "Unknown time unit: #{unit}" unless multiplier
21
+
22
+ amount * multiplier
23
+ end
24
+
25
+ def self.to_epoch(time)
26
+ case time
27
+ when Time, DateTime
28
+ time.to_i
29
+ when Integer
30
+ time
31
+ when String
32
+ Time.parse(time).to_i
33
+ else
34
+ raise ConfigError, "Cannot convert #{time.class} to epoch time"
35
+ end
36
+ end
37
+
38
+ def self.parse_relative_time(str)
39
+ return nil unless str.is_a?(String)
40
+
41
+ match = str.match(/^(\d+)(s|m|h|d|w)$/)
42
+ return nil unless match
43
+
44
+ amount = match[1].to_i
45
+ unit = case match[2]
46
+ when "s" then :seconds
47
+ when "m" then :minutes
48
+ when "h" then :hours
49
+ when "d" then :days
50
+ when "w" then :weeks
51
+ end
52
+
53
+ Time.now - duration_in_seconds(amount, unit)
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CloudwatchQuery
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "cloudwatch_query/version"
4
+ require_relative "cloudwatch_query/errors"
5
+ require_relative "cloudwatch_query/configuration"
6
+ require_relative "cloudwatch_query/time_helpers"
7
+ require_relative "cloudwatch_query/parsers"
8
+ require_relative "cloudwatch_query/result"
9
+ require_relative "cloudwatch_query/result_set"
10
+ require_relative "cloudwatch_query/client"
11
+ require_relative "cloudwatch_query/query"
12
+
13
+ module CloudwatchQuery
14
+ class << self
15
+ def configuration
16
+ @configuration ||= Configuration.new
17
+ end
18
+
19
+ def configure
20
+ yield(configuration)
21
+ end
22
+
23
+ def reset_configuration!
24
+ @configuration = Configuration.new
25
+ end
26
+
27
+ # Parser registry - manages log message parsers
28
+ def parsers
29
+ @parsers ||= begin
30
+ registry = Parsers::Registry.new
31
+ registry.register(*Parsers.default_parsers)
32
+ registry
33
+ end
34
+ end
35
+
36
+ # Reset parsers to defaults
37
+ def reset_parsers!
38
+ @parsers = nil
39
+ parsers
40
+ end
41
+
42
+ # Start a query for the specified log groups
43
+ def logs(*groups)
44
+ Query.new.logs(*groups)
45
+ end
46
+ alias log_group logs
47
+
48
+ # Quick search shorthand
49
+ def search(term, groups:, since: nil, limit: nil)
50
+ query = Query.new.logs(*Array(groups)).contains(term)
51
+ query = query.since(since) if since
52
+ query = query.limit(limit) if limit
53
+ query.execute
54
+ end
55
+
56
+ # List available log groups
57
+ def list_log_groups(prefix: nil)
58
+ Client.new.list_log_groups(prefix: prefix)
59
+ end
60
+ end
61
+ end
metadata ADDED
@@ -0,0 +1,112 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cloudwatch_query
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Igor Irianto
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-02-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: aws-sdk-cloudwatchlogs
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: webmock
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ description: Query AWS CloudWatch Logs using a fluent, ActiveRecord-style interface.
56
+ Supports chainable queries, automatic Insights query generation, and enumerable
57
+ results.
58
+ email:
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - LICENSE.txt
64
+ - README.md
65
+ - Rakefile
66
+ - cloudwatch_query.gemspec
67
+ - lib/cloudwatch_query.rb
68
+ - lib/cloudwatch_query/client.rb
69
+ - lib/cloudwatch_query/configuration.rb
70
+ - lib/cloudwatch_query/errors.rb
71
+ - lib/cloudwatch_query/parsers.rb
72
+ - lib/cloudwatch_query/parsers/base.rb
73
+ - lib/cloudwatch_query/parsers/rails/rails_log.rb
74
+ - lib/cloudwatch_query/parsers/rails/sub_parsers.rb
75
+ - lib/cloudwatch_query/parsers/rails_parser.rb
76
+ - lib/cloudwatch_query/parsers/registry.rb
77
+ - lib/cloudwatch_query/parsers/sidekiq/sidekiq_log.rb
78
+ - lib/cloudwatch_query/parsers/sidekiq/sub_parsers.rb
79
+ - lib/cloudwatch_query/parsers/sidekiq_parser.rb
80
+ - lib/cloudwatch_query/query.rb
81
+ - lib/cloudwatch_query/result.rb
82
+ - lib/cloudwatch_query/result_set.rb
83
+ - lib/cloudwatch_query/time_helpers.rb
84
+ - lib/cloudwatch_query/version.rb
85
+ homepage: https://github.com/iggredible/cloudwatch_query
86
+ licenses:
87
+ - MIT
88
+ metadata:
89
+ homepage_uri: https://github.com/iggredible/cloudwatch_query
90
+ source_code_uri: https://github.com/iggredible/cloudwatch_query
91
+ changelog_uri: https://github.com/iggredible/cloudwatch_query/blob/main/CHANGELOG.md
92
+ rubygems_mfa_required: 'true'
93
+ post_install_message:
94
+ rdoc_options: []
95
+ require_paths:
96
+ - lib
97
+ required_ruby_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: 2.7.0
102
+ required_rubygems_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ requirements: []
108
+ rubygems_version: 3.1.6
109
+ signing_key:
110
+ specification_version: 4
111
+ summary: A Ruby gem for querying AWS CloudWatch Logs with a simple, chainable interface
112
+ test_files: []