event_meter 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 +1081 -0
- data/exe/event_meter +5 -0
- data/lib/event_meter/auto_cleanup.rb +93 -0
- data/lib/event_meter/cli.rb +124 -0
- data/lib/event_meter/configuration.rb +244 -0
- data/lib/event_meter/errors.rb +9 -0
- data/lib/event_meter/event.rb +180 -0
- data/lib/event_meter/event_payload.rb +103 -0
- data/lib/event_meter/hash_input.rb +20 -0
- data/lib/event_meter/index_key.rb +19 -0
- data/lib/event_meter/keys.rb +63 -0
- data/lib/event_meter/path_name.rb +37 -0
- data/lib/event_meter/processor.rb +305 -0
- data/lib/event_meter/rails.rb +79 -0
- data/lib/event_meter/report_definition.rb +184 -0
- data/lib/event_meter/reports.rb +143 -0
- data/lib/event_meter/rollup.rb +148 -0
- data/lib/event_meter/stores/cleanup_helpers.rb +76 -0
- data/lib/event_meter/stores/file_helpers.rb +47 -0
- data/lib/event_meter/stores/lock_refresher.rb +75 -0
- data/lib/event_meter/stores/namespace.rb +14 -0
- data/lib/event_meter/stores/redis_lock.rb +77 -0
- data/lib/event_meter/stores/rollup/active_record_postgres.rb +135 -0
- data/lib/event_meter/stores/rollup/file.rb +736 -0
- data/lib/event_meter/stores/rollup/postgres.rb +813 -0
- data/lib/event_meter/stores/rollup/redis.rb +349 -0
- data/lib/event_meter/stores/stream/file.rb +98 -0
- data/lib/event_meter/stores/stream/redis.rb +79 -0
- data/lib/event_meter/time_buckets.rb +56 -0
- data/lib/event_meter/version.rb +3 -0
- data/lib/event_meter/write_result.rb +26 -0
- data/lib/event_meter.rb +150 -0
- data/lib/generators/event_meter/install_generator.rb +57 -0
- data/lib/generators/event_meter/templates/create_event_meter_tables.rb.erb +12 -0
- data/lib/generators/event_meter/templates/event_meter.rb.erb +12 -0
- metadata +156 -0
data/exe/event_meter
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
require "time"
|
|
2
|
+
|
|
3
|
+
module EventMeter
|
|
4
|
+
class AutoCleanup
|
|
5
|
+
WATERMARK_KEY_SUFFIX = "auto_cleanup:history:last_run"
|
|
6
|
+
|
|
7
|
+
attr_reader :configuration, :rollup_storage
|
|
8
|
+
|
|
9
|
+
def initialize(configuration:, rollup_storage:)
|
|
10
|
+
@configuration = configuration
|
|
11
|
+
@rollup_storage = rollup_storage
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def run
|
|
15
|
+
return false unless configuration.auto_cleanup_history
|
|
16
|
+
return false unless rollup_storage.respond_to?(:cleanup_history)
|
|
17
|
+
|
|
18
|
+
with_cleanup_lock { cleanup_if_due }
|
|
19
|
+
rescue StandardError => error
|
|
20
|
+
report_error(error)
|
|
21
|
+
false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def with_cleanup_lock
|
|
27
|
+
return yield unless rollup_storage.respond_to?(:with_lock)
|
|
28
|
+
|
|
29
|
+
result = false
|
|
30
|
+
locked = rollup_storage.with_lock(ttl: configuration.lock_ttl) do
|
|
31
|
+
result = yield
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
locked ? result : false
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def cleanup_if_due
|
|
38
|
+
return false unless cleanup_due?
|
|
39
|
+
|
|
40
|
+
result = rollup_storage.cleanup_history(
|
|
41
|
+
before: cleanup_before,
|
|
42
|
+
events: nil,
|
|
43
|
+
interval_state: true
|
|
44
|
+
)
|
|
45
|
+
write_watermark
|
|
46
|
+
result
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def cleanup_due?
|
|
50
|
+
last_cleanup_at.nil? ||
|
|
51
|
+
last_cleanup_at <= current_time - configuration.cleanup_history_interval
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def last_cleanup_at
|
|
55
|
+
return unless rollup_storage.respond_to?(:cleanup_watermark)
|
|
56
|
+
|
|
57
|
+
parse_time(rollup_storage.cleanup_watermark(watermark_key))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def write_watermark
|
|
61
|
+
return unless rollup_storage.respond_to?(:write_cleanup_watermark)
|
|
62
|
+
|
|
63
|
+
rollup_storage.write_cleanup_watermark(watermark_key, current_time.iso8601(6))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def cleanup_before
|
|
67
|
+
current_time - configuration.cleanup_history_retention
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def watermark_key
|
|
71
|
+
[configuration.namespace, WATERMARK_KEY_SUFFIX].join(":")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def current_time
|
|
75
|
+
Time.now.utc
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def parse_time(value)
|
|
79
|
+
return if value.nil?
|
|
80
|
+
|
|
81
|
+
Time.parse(value.to_s).utc
|
|
82
|
+
rescue ArgumentError, TypeError, RangeError
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def report_error(error)
|
|
87
|
+
handler = configuration.auto_cleanup_error_handler
|
|
88
|
+
handler&.call(error)
|
|
89
|
+
rescue StandardError
|
|
90
|
+
nil
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
require "optparse"
|
|
2
|
+
|
|
3
|
+
require_relative "../event_meter"
|
|
4
|
+
|
|
5
|
+
module EventMeter
|
|
6
|
+
class CLI
|
|
7
|
+
def self.call(argv, out: $stdout, err: $stderr)
|
|
8
|
+
new(argv, out: out, err: err).call
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(argv, out:, err:)
|
|
12
|
+
@argv = argv.dup
|
|
13
|
+
@out = out
|
|
14
|
+
@err = err
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call
|
|
18
|
+
case argv.shift
|
|
19
|
+
when "postgres"
|
|
20
|
+
postgres
|
|
21
|
+
when "help", "-h", "--help", nil
|
|
22
|
+
out.puts usage
|
|
23
|
+
0
|
|
24
|
+
else
|
|
25
|
+
err.puts usage
|
|
26
|
+
1
|
|
27
|
+
end
|
|
28
|
+
rescue OptionParser::ParseError, ArgumentError => error
|
|
29
|
+
err.puts error.message
|
|
30
|
+
1
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
attr_reader :argv, :out, :err
|
|
36
|
+
|
|
37
|
+
def postgres
|
|
38
|
+
case argv.shift
|
|
39
|
+
when "install"
|
|
40
|
+
postgres_install
|
|
41
|
+
when "schema"
|
|
42
|
+
postgres_schema
|
|
43
|
+
when "help", "-h", "--help", nil
|
|
44
|
+
out.puts postgres_usage
|
|
45
|
+
0
|
|
46
|
+
else
|
|
47
|
+
err.puts postgres_usage
|
|
48
|
+
1
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def postgres_install
|
|
53
|
+
options = postgres_options
|
|
54
|
+
parse_postgres_options!(options)
|
|
55
|
+
|
|
56
|
+
url = options.fetch(:url) || ENV["DATABASE_URL"]
|
|
57
|
+
raise ArgumentError, "missing database URL; pass --url or set DATABASE_URL" if url.to_s.strip.empty?
|
|
58
|
+
|
|
59
|
+
require_pg!
|
|
60
|
+
|
|
61
|
+
connection = PG.connect(url)
|
|
62
|
+
EventMeter::Stores::Rollup::Postgres.install!(
|
|
63
|
+
connection: connection,
|
|
64
|
+
table_prefix: options.fetch(:table_prefix)
|
|
65
|
+
)
|
|
66
|
+
out.puts "Installed EventMeter PostgreSQL tables with prefix #{options.fetch(:table_prefix)}"
|
|
67
|
+
0
|
|
68
|
+
ensure
|
|
69
|
+
connection&.close
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def postgres_schema
|
|
73
|
+
options = postgres_options
|
|
74
|
+
parse_postgres_options!(options)
|
|
75
|
+
|
|
76
|
+
out.puts EventMeter::Stores::Rollup::Postgres.schema_sql(
|
|
77
|
+
table_prefix: options.fetch(:table_prefix)
|
|
78
|
+
)
|
|
79
|
+
0
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def postgres_options
|
|
83
|
+
{
|
|
84
|
+
table_prefix: "event_meter",
|
|
85
|
+
url: nil
|
|
86
|
+
}
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def parse_postgres_options!(options)
|
|
90
|
+
parser = OptionParser.new do |parser_config|
|
|
91
|
+
parser_config.on("--table-prefix PREFIX", "PostgreSQL table prefix") do |value|
|
|
92
|
+
options[:table_prefix] = value
|
|
93
|
+
end
|
|
94
|
+
parser_config.on("--url URL", "PostgreSQL connection URL") do |value|
|
|
95
|
+
options[:url] = value
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
parser.parse!(argv)
|
|
99
|
+
raise OptionParser::InvalidArgument, argv.join(" ") unless argv.empty?
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def require_pg!
|
|
103
|
+
require "pg"
|
|
104
|
+
rescue LoadError
|
|
105
|
+
raise ArgumentError, "pg is required for postgres install; add gem \"pg\" to your app"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def usage
|
|
109
|
+
<<~TEXT
|
|
110
|
+
Usage:
|
|
111
|
+
event_meter postgres schema [--table-prefix PREFIX]
|
|
112
|
+
event_meter postgres install [--url DATABASE_URL] [--table-prefix PREFIX]
|
|
113
|
+
TEXT
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def postgres_usage
|
|
117
|
+
<<~TEXT
|
|
118
|
+
Usage:
|
|
119
|
+
event_meter postgres schema [--table-prefix PREFIX]
|
|
120
|
+
event_meter postgres install [--url DATABASE_URL] [--table-prefix PREFIX]
|
|
121
|
+
TEXT
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
require "monitor"
|
|
2
|
+
|
|
3
|
+
module EventMeter
|
|
4
|
+
class Configuration
|
|
5
|
+
DEFAULT_NAMESPACE = "event_meter:v1"
|
|
6
|
+
DEFAULT_REDIS_READ_LIMIT = nil
|
|
7
|
+
DEFAULT_ROLLUP_TTL = 31 * 24 * 60 * 60
|
|
8
|
+
DEFAULT_LOCK_TTL = 30
|
|
9
|
+
DEFAULT_AUTO_CLEANUP_HISTORY = false
|
|
10
|
+
DEFAULT_CLEANUP_HISTORY_RETENTION = DEFAULT_ROLLUP_TTL
|
|
11
|
+
DEFAULT_CLEANUP_HISTORY_INTERVAL = 60 * 60
|
|
12
|
+
DEFAULT_SUMMARY_KEY_LIMIT = 10_000
|
|
13
|
+
DEFAULT_AUTO_CLEANUP_ERROR_HANDLER = lambda do |error|
|
|
14
|
+
warn "EventMeter auto cleanup failed: #{error.class}: #{error.message}"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
attr_reader :namespace, :redis, :redis_read_limit, :rollup_ttl, :lock_ttl,
|
|
18
|
+
:auto_cleanup_history, :cleanup_history_retention, :cleanup_history_interval,
|
|
19
|
+
:summary_key_limit, :auto_cleanup_error_handler
|
|
20
|
+
|
|
21
|
+
def initialize
|
|
22
|
+
@configuration_lock = Monitor.new
|
|
23
|
+
@namespace = DEFAULT_NAMESPACE
|
|
24
|
+
@redis_read_limit = DEFAULT_REDIS_READ_LIMIT
|
|
25
|
+
@rollup_ttl = DEFAULT_ROLLUP_TTL
|
|
26
|
+
@lock_ttl = DEFAULT_LOCK_TTL
|
|
27
|
+
@auto_cleanup_history = DEFAULT_AUTO_CLEANUP_HISTORY
|
|
28
|
+
@cleanup_history_retention = DEFAULT_CLEANUP_HISTORY_RETENTION
|
|
29
|
+
@cleanup_history_interval = DEFAULT_CLEANUP_HISTORY_INTERVAL
|
|
30
|
+
@summary_key_limit = DEFAULT_SUMMARY_KEY_LIMIT
|
|
31
|
+
@auto_cleanup_error_handler = DEFAULT_AUTO_CLEANUP_ERROR_HANDLER
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def namespace=(value)
|
|
35
|
+
namespace = value.to_s
|
|
36
|
+
raise ArgumentError, "namespace cannot be blank" if namespace.strip.empty?
|
|
37
|
+
|
|
38
|
+
synchronize do
|
|
39
|
+
@namespace = namespace
|
|
40
|
+
reset_default_storages
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def redis=(client_or_factory)
|
|
45
|
+
synchronize do
|
|
46
|
+
@redis = client_or_factory
|
|
47
|
+
reset_redis_client_cache
|
|
48
|
+
reset_default_storages
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def redis_read_limit=(value)
|
|
53
|
+
redis_read_limit = positive_integer_or_nil(value, "redis_read_limit")
|
|
54
|
+
|
|
55
|
+
synchronize do
|
|
56
|
+
@redis_read_limit = redis_read_limit
|
|
57
|
+
reset_default_storages
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def rollup_ttl=(value)
|
|
62
|
+
rollup_ttl = positive_integer(value, "rollup_ttl")
|
|
63
|
+
|
|
64
|
+
synchronize do
|
|
65
|
+
@rollup_ttl = rollup_ttl
|
|
66
|
+
reset_default_rollup_storage
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def lock_ttl=(value)
|
|
71
|
+
@lock_ttl = positive_integer(value, "lock_ttl")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def auto_cleanup_history=(value)
|
|
75
|
+
@auto_cleanup_history = boolean(value, "auto_cleanup_history")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def cleanup_history_retention=(value)
|
|
79
|
+
@cleanup_history_retention = positive_integer(value, "cleanup_history_retention")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def cleanup_history_interval=(value)
|
|
83
|
+
@cleanup_history_interval = positive_integer(value, "cleanup_history_interval")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def summary_key_limit=(value)
|
|
87
|
+
@summary_key_limit = positive_integer_or_nil(value, "summary_key_limit")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def auto_cleanup_error_handler=(handler)
|
|
91
|
+
unless handler.nil? || handler.respond_to?(:call)
|
|
92
|
+
raise ArgumentError, "auto_cleanup_error_handler must respond to call"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
@auto_cleanup_error_handler = handler
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def stream_storage
|
|
99
|
+
synchronize do
|
|
100
|
+
reset_redis_clients_if_stale
|
|
101
|
+
@stream_storage ||= default_stream_storage
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def stream_storage=(storage)
|
|
106
|
+
raise ArgumentError, "stream_storage cannot be nil" if storage.nil?
|
|
107
|
+
|
|
108
|
+
synchronize do
|
|
109
|
+
@stream_storage = storage
|
|
110
|
+
@stream_storage_default = false
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def rollup_storage
|
|
115
|
+
synchronize do
|
|
116
|
+
reset_redis_clients_if_stale
|
|
117
|
+
@rollup_storage ||= default_rollup_storage
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def rollup_storage=(storage)
|
|
122
|
+
raise ArgumentError, "rollup_storage cannot be nil" if storage.nil?
|
|
123
|
+
|
|
124
|
+
synchronize do
|
|
125
|
+
@rollup_storage = storage
|
|
126
|
+
@rollup_storage_default = false
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private
|
|
131
|
+
|
|
132
|
+
def synchronize(&block)
|
|
133
|
+
@configuration_lock.synchronize(&block)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def default_stream_storage
|
|
137
|
+
return missing_storage!("stream_storage") unless redis
|
|
138
|
+
|
|
139
|
+
@stream_storage_default = true
|
|
140
|
+
Stores::Stream::Redis.new(
|
|
141
|
+
redis: redis_client(:stream),
|
|
142
|
+
lock_redis: redis_client(:stream_lock),
|
|
143
|
+
namespace: namespace,
|
|
144
|
+
redis_read_limit: redis_read_limit
|
|
145
|
+
)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def default_rollup_storage
|
|
149
|
+
return missing_storage!("rollup_storage") unless redis
|
|
150
|
+
|
|
151
|
+
@rollup_storage_default = true
|
|
152
|
+
Stores::Rollup::Redis.new(
|
|
153
|
+
redis: redis_client(:rollup),
|
|
154
|
+
lock_redis: redis_client(:rollup_lock),
|
|
155
|
+
namespace: namespace,
|
|
156
|
+
rollup_ttl: rollup_ttl
|
|
157
|
+
)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def reset_default_storages
|
|
161
|
+
@stream_storage = nil if @stream_storage_default
|
|
162
|
+
reset_default_rollup_storage
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def reset_default_rollup_storage
|
|
166
|
+
@rollup_storage = nil if @rollup_storage_default
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def redis_client(purpose = :default)
|
|
170
|
+
synchronize do
|
|
171
|
+
reset_redis_clients_if_stale
|
|
172
|
+
initialize_redis_client_cache
|
|
173
|
+
@redis_clients[purpose] ||= begin
|
|
174
|
+
client = redis.is_a?(Proc) ? redis.call : redis
|
|
175
|
+
raise ConfigurationError, "redis client cannot be nil" if client.nil?
|
|
176
|
+
raise ConfigurationError, "redis client cannot be closed" if redis_client_closed?(client)
|
|
177
|
+
|
|
178
|
+
client
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def reset_redis_clients_if_stale
|
|
184
|
+
return unless @redis_clients
|
|
185
|
+
return unless redis_client_cache_stale?
|
|
186
|
+
|
|
187
|
+
reset_redis_client_cache
|
|
188
|
+
reset_default_storages
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def reset_redis_client_cache
|
|
192
|
+
@redis_clients = nil
|
|
193
|
+
@redis_client_pid = nil
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def initialize_redis_client_cache
|
|
197
|
+
return if @redis_clients
|
|
198
|
+
|
|
199
|
+
@redis_clients = {}
|
|
200
|
+
@redis_client_pid = process_id
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def redis_client_cache_stale?
|
|
204
|
+
@redis_client_pid != process_id || @redis_clients.any? do |_purpose, client|
|
|
205
|
+
redis_client_closed?(client)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def process_id
|
|
210
|
+
Process.pid
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def redis_client_closed?(client)
|
|
214
|
+
client.respond_to?(:closed?) && client.closed?
|
|
215
|
+
rescue StandardError
|
|
216
|
+
false
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def missing_storage!(name)
|
|
220
|
+
raise ConfigurationError, "configure #{name}, or set config.redis to use Redis storage"
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def positive_integer(value, name)
|
|
224
|
+
integer = Integer(value)
|
|
225
|
+
return integer if integer.positive?
|
|
226
|
+
|
|
227
|
+
raise ArgumentError, "#{name} must be positive"
|
|
228
|
+
rescue ArgumentError, TypeError, RangeError
|
|
229
|
+
raise ArgumentError, "#{name} must be positive"
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def positive_integer_or_nil(value, name)
|
|
233
|
+
return nil if value.nil?
|
|
234
|
+
|
|
235
|
+
positive_integer(value, name)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def boolean(value, name)
|
|
239
|
+
return value if value == true || value == false
|
|
240
|
+
|
|
241
|
+
raise ArgumentError, "#{name} must be true or false"
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
module EventMeter
|
|
2
|
+
class Error < StandardError; end
|
|
3
|
+
class UnsupportedQueryError < Error; end
|
|
4
|
+
class AlreadyRecordedError < Error; end
|
|
5
|
+
class ConfigurationError < Error; end
|
|
6
|
+
class DefinitionChangedError < Error; end
|
|
7
|
+
class DefinitionNotFoundError < Error; end
|
|
8
|
+
class LockLostError < Error; end
|
|
9
|
+
end
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
require "time"
|
|
2
|
+
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
require_relative "event_payload"
|
|
5
|
+
require_relative "hash_input"
|
|
6
|
+
require_relative "write_result"
|
|
7
|
+
|
|
8
|
+
module EventMeter
|
|
9
|
+
class Event
|
|
10
|
+
INVALID_EVENT_NAME = "event_meter.invalid"
|
|
11
|
+
|
|
12
|
+
attr_reader :name, :started_at, :finished_at, :status, :error
|
|
13
|
+
|
|
14
|
+
def self.start(name, attributes, keyword_attributes, started_at:)
|
|
15
|
+
new(name, attributes, keyword_attributes, started_at: started_at)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.failed(error)
|
|
19
|
+
new(INVALID_EVENT_NAME, error: error)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(name, attributes = nil, keyword_attributes = {}, started_at: nil, error: nil)
|
|
23
|
+
@started_at = nil
|
|
24
|
+
@finished_at = nil
|
|
25
|
+
@status = nil
|
|
26
|
+
@recorded = false
|
|
27
|
+
@error = nil
|
|
28
|
+
|
|
29
|
+
if error
|
|
30
|
+
@name = INVALID_EVENT_NAME
|
|
31
|
+
@base_attributes = {}
|
|
32
|
+
@error = error
|
|
33
|
+
return
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
@started_at = started_at&.utc
|
|
37
|
+
@name = normalize_name(name)
|
|
38
|
+
@base_attributes = normalize_start_attributes(attributes, keyword_attributes)
|
|
39
|
+
rescue StandardError => error
|
|
40
|
+
@name = fallback_name(name)
|
|
41
|
+
@base_attributes = {}
|
|
42
|
+
@error = error
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def success(attributes = {})
|
|
46
|
+
finish("success") { attributes }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def skip(reason_or_attributes = nil, attributes = {})
|
|
50
|
+
finish("skipped") do
|
|
51
|
+
normalize_reason_attributes(reason_or_attributes, attributes, :skip_reason)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def failure(error_or_attributes = nil, attributes = {})
|
|
56
|
+
finish("failure") do
|
|
57
|
+
normalize_error_attributes(error_or_attributes, attributes)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def error?
|
|
62
|
+
!error.nil?
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def finish(status)
|
|
68
|
+
if recorded?
|
|
69
|
+
return WriteResult.failed(
|
|
70
|
+
payload: nil,
|
|
71
|
+
error: AlreadyRecordedError.new("event has already been recorded")
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
@started_at ||= current_time
|
|
76
|
+
@status = status
|
|
77
|
+
@finished_at = current_time
|
|
78
|
+
|
|
79
|
+
return WriteResult.failed(payload: nil, error: error) if error?
|
|
80
|
+
|
|
81
|
+
payload = nil
|
|
82
|
+
payload_hash = nil
|
|
83
|
+
attributes = yield
|
|
84
|
+
|
|
85
|
+
payload = EventPayload.build(
|
|
86
|
+
name,
|
|
87
|
+
params: record_attributes(attributes),
|
|
88
|
+
status: status,
|
|
89
|
+
started_at: started_at,
|
|
90
|
+
duration_ms: duration_ms
|
|
91
|
+
)
|
|
92
|
+
payload_hash = payload.to_h
|
|
93
|
+
|
|
94
|
+
EventMeter.stream_storage.append(payload)
|
|
95
|
+
@recorded = true
|
|
96
|
+
WriteResult.recorded(payload_hash)
|
|
97
|
+
rescue StandardError => error
|
|
98
|
+
WriteResult.failed(payload: payload_hash, error: error)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def record_attributes(attributes)
|
|
102
|
+
@base_attributes.merge(normalize_attributes(attributes))
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def duration_ms
|
|
106
|
+
[((finished_at.to_f - started_at.to_f) * 1000).round, 0].max
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def normalize_reason_attributes(reason_or_attributes, attributes, key)
|
|
110
|
+
attributes = normalize_attributes(attributes)
|
|
111
|
+
|
|
112
|
+
case reason_or_attributes
|
|
113
|
+
when nil
|
|
114
|
+
attributes
|
|
115
|
+
when Hash
|
|
116
|
+
normalize_attributes(reason_or_attributes).merge(attributes)
|
|
117
|
+
else
|
|
118
|
+
{ key => reason_or_attributes.to_s }.merge(attributes)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def normalize_error_attributes(error_or_attributes, attributes)
|
|
123
|
+
attributes = normalize_attributes(attributes)
|
|
124
|
+
|
|
125
|
+
case error_or_attributes
|
|
126
|
+
when nil
|
|
127
|
+
attributes
|
|
128
|
+
when Hash
|
|
129
|
+
normalize_attributes(error_or_attributes).merge(attributes)
|
|
130
|
+
else
|
|
131
|
+
{
|
|
132
|
+
error_class: error_or_attributes.class.name,
|
|
133
|
+
error_message: error_message(error_or_attributes)
|
|
134
|
+
}.merge(attributes)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def normalize_attributes(attributes)
|
|
139
|
+
HashInput.coerce(attributes, "event attributes")
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def normalize_start_attributes(attributes, keyword_attributes)
|
|
143
|
+
normalize_attributes(attributes).merge(normalize_attributes(keyword_attributes))
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def normalize_name(value)
|
|
147
|
+
name = value.to_s
|
|
148
|
+
raise ArgumentError, "event name cannot be blank" if name.strip.empty?
|
|
149
|
+
|
|
150
|
+
name
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def fallback_name(value)
|
|
154
|
+
name = safe_string(value)
|
|
155
|
+
return INVALID_EVENT_NAME if name.strip.empty?
|
|
156
|
+
|
|
157
|
+
name
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def safe_string(value)
|
|
161
|
+
value.to_s
|
|
162
|
+
rescue StandardError
|
|
163
|
+
INVALID_EVENT_NAME
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def error_message(error)
|
|
167
|
+
return error.message if error.respond_to?(:message)
|
|
168
|
+
|
|
169
|
+
error.to_s
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def recorded?
|
|
173
|
+
@recorded
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def current_time
|
|
177
|
+
Time.now.utc
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|