chronos-ruby 0.8.0.pre.1 → 0.9.0.pre.2
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 +4 -4
- data/CHANGELOG.md +58 -0
- data/README.md +40 -10
- data/SECURITY.md +2 -0
- data/contracts/deploy-v1.schema.json +26 -0
- data/contracts/event-v1.schema.json +15 -1
- data/docs/adr/ADR-017-deploy-events-and-correlation.md +25 -0
- data/docs/adr/ADR-018-pre-1.0-hardening.md +9 -0
- data/docs/architecture.md +7 -1
- data/docs/compatibility.md +7 -2
- data/docs/configuration.md +18 -3
- data/docs/data-collected.md +6 -3
- data/docs/deprecation-policy.md +7 -0
- data/docs/examples/plain-ruby.md +7 -1
- data/docs/migration-from-airbrake.md +18 -0
- data/docs/modules/active-job.md +7 -0
- data/docs/modules/configuration.md +2 -0
- data/docs/modules/dependencies.md +2 -2
- data/docs/modules/deploy-tracking.md +86 -0
- data/docs/modules/ignore-rules.md +11 -0
- data/docs/modules/rails-legacy.md +3 -3
- data/docs/modules/sidekiq-legacy.md +1 -1
- data/docs/modules/telemetry-events.md +3 -1
- data/docs/performance.md +26 -1
- data/docs/privacy-lgpd.md +11 -2
- data/docs/protocol-v1.md +5 -0
- data/docs/release-1.0-readiness.md +18 -0
- data/docs/security-review.md +16 -0
- data/docs/semver.md +11 -0
- data/docs/troubleshooting.md +12 -0
- data/lib/chronos/agent.rb +30 -2
- data/lib/chronos/application/capture_exception.rb +7 -0
- data/lib/chronos/application/capture_telemetry.rb +17 -0
- data/lib/chronos/application/dependency_reporter.rb +12 -1
- data/lib/chronos/application/ignore_policy.rb +54 -0
- data/lib/chronos/application/remote_configuration.rb +1 -1
- data/lib/chronos/capistrano.rb +4 -0
- data/lib/chronos/configuration/apm_validation.rb +11 -0
- data/lib/chronos/configuration/validation.rb +16 -1
- data/lib/chronos/configuration.rb +14 -3
- data/lib/chronos/core/correlation_context.rb +65 -0
- data/lib/chronos/core/deploy_normalizer.rb +102 -0
- data/lib/chronos/core/payload_serializer.rb +4 -1
- data/lib/chronos/core/telemetry_event.rb +31 -4
- data/lib/chronos/integrations/active_job.rb +119 -0
- data/lib/chronos/integrations/capistrano.rb +61 -0
- data/lib/chronos/observability_facade.rb +9 -2
- data/lib/chronos/rails/installer.rb +7 -0
- data/lib/chronos/rails/notifications_subscriber.rb +29 -1
- data/lib/chronos/rails.rb +1 -0
- data/lib/chronos/version.rb +1 -1
- data/lib/chronos.rb +11 -1
- metadata +19 -1
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
require "securerandom"
|
|
2
|
+
require "uri"
|
|
3
|
+
|
|
4
|
+
module Chronos
|
|
5
|
+
module Core
|
|
6
|
+
# Normalizes explicit deployment metadata into the versioned deploy payload.
|
|
7
|
+
#
|
|
8
|
+
# @responsibility Validate required deploy identity and bound every public API field.
|
|
9
|
+
# @motivation Deployment commands need predictable safe input independent of frameworks.
|
|
10
|
+
# @limits It does not inspect Git, environment variables, credentials, or deployment systems.
|
|
11
|
+
# @collaborators Chronos.notify_deploy, CorrelationContext, and Configuration::Snapshot.
|
|
12
|
+
# @thread_safety Stateless apart from immutable configuration and an injected ID generator.
|
|
13
|
+
# @compatibility Ruby 2.2.10 through Ruby 2.6.
|
|
14
|
+
# @example
|
|
15
|
+
# DeployNormalizer.new(config).call(:revision => "abc", :version => "1.2.3")
|
|
16
|
+
# @errors Missing environment or both revision/version raise ArgumentError for the agent to contain.
|
|
17
|
+
# @performance Constant work over nine bounded scalar fields.
|
|
18
|
+
class DeployNormalizer
|
|
19
|
+
def initialize(config, options = {})
|
|
20
|
+
@config = config
|
|
21
|
+
@id_generator = options[:id_generator] || proc { SecureRandom.uuid }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def call(attributes = {})
|
|
25
|
+
values = string_hash(attributes)
|
|
26
|
+
payload = {
|
|
27
|
+
"deploy_id" => deploy_id(values["deploy_id"] || @config.deploy_id),
|
|
28
|
+
"environment" => bounded(values["environment"] || @config.environment, 128),
|
|
29
|
+
"revision" => bounded(values["revision"] || @config.revision, 128),
|
|
30
|
+
"version" => bounded(values["version"] || @config.app_version, 128),
|
|
31
|
+
"repository" => repository(values["repository"]),
|
|
32
|
+
"actor" => bounded(values["actor"], 128),
|
|
33
|
+
"service" => bounded(values["service"] || @config.service_name, 128),
|
|
34
|
+
"region" => bounded(values["region"] || @config.region, 128),
|
|
35
|
+
"instance" => bounded(values["instance"] || @config.instance_id, 128)
|
|
36
|
+
}
|
|
37
|
+
validate!(payload)
|
|
38
|
+
payload
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def validate!(payload)
|
|
44
|
+
raise ArgumentError, "deploy environment is required" if payload["environment"].to_s.empty?
|
|
45
|
+
return unless payload["revision"].to_s.empty? && payload["version"].to_s.empty?
|
|
46
|
+
|
|
47
|
+
raise ArgumentError, "deploy revision or version is required"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def generated_id
|
|
51
|
+
value = bounded(@id_generator.call, 128)
|
|
52
|
+
raise ArgumentError, "deploy ID is required" if value.to_s.empty?
|
|
53
|
+
|
|
54
|
+
value
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def deploy_id(value)
|
|
58
|
+
normalized = bounded(value, 128)
|
|
59
|
+
normalized.to_s.empty? ? generated_id : normalized
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def repository(value)
|
|
63
|
+
text = bounded(value, 512)
|
|
64
|
+
return nil if text.nil?
|
|
65
|
+
|
|
66
|
+
scp = text.match(/\A[^@]+@([^:]+):(.+)\z/)
|
|
67
|
+
return bounded("#{scp[1]}/#{scp[2]}", 512) if scp
|
|
68
|
+
|
|
69
|
+
uri = URI.parse(text)
|
|
70
|
+
return bounded("#{uri.host}#{uri.path}", 512) if uri.host
|
|
71
|
+
return bounded(uri.path, 512) unless uri.path.to_s.empty?
|
|
72
|
+
|
|
73
|
+
nil
|
|
74
|
+
rescue StandardError
|
|
75
|
+
nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def string_hash(value)
|
|
79
|
+
raise ArgumentError, "deploy attributes must be a Hash" unless value.is_a?(Hash)
|
|
80
|
+
|
|
81
|
+
value.each_with_object({}) do |(key, child), result|
|
|
82
|
+
result[key.to_s] = child if key.is_a?(String) || key.is_a?(Symbol)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def bounded(value, limit)
|
|
87
|
+
return nil if value.nil?
|
|
88
|
+
return nil unless scalar?(value)
|
|
89
|
+
|
|
90
|
+
text = value.to_s
|
|
91
|
+
text = text.scrub("?") if text.respond_to?(:scrub)
|
|
92
|
+
text.bytesize > limit ? text.byteslice(0, limit) : text
|
|
93
|
+
rescue StandardError
|
|
94
|
+
nil
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def scalar?(value)
|
|
98
|
+
value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(Numeric)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -44,6 +44,7 @@ module Chronos
|
|
|
44
44
|
@sanitizer = options[:sanitizer] || Sanitizer.new(config)
|
|
45
45
|
@safe_serializer = options[:safe_serializer] || SafeSerializer.new
|
|
46
46
|
@max_payload_size = options[:max_payload_size] || proc { @config.max_payload_size }
|
|
47
|
+
@correlation = options[:correlation] || CorrelationContext.new(config)
|
|
47
48
|
end
|
|
48
49
|
|
|
49
50
|
def call(notice)
|
|
@@ -70,8 +71,10 @@ module Chronos
|
|
|
70
71
|
"service" => {
|
|
71
72
|
"name" => @config.service_name,
|
|
72
73
|
"version" => @config.app_version,
|
|
73
|
-
"instance_id" => notice.host
|
|
74
|
+
"instance_id" => @config.instance_id || notice.host
|
|
74
75
|
},
|
|
76
|
+
"correlation" => @correlation.call("environment" => notice.environment,
|
|
77
|
+
"instance" => @config.instance_id || notice.host),
|
|
75
78
|
"runtime" => notice.runtime,
|
|
76
79
|
"context" => notice.context,
|
|
77
80
|
"payload" => payload(notice)
|
|
@@ -16,7 +16,7 @@ module Chronos
|
|
|
16
16
|
# @errors Unsupported types raise ArgumentError during construction.
|
|
17
17
|
# @performance Construction is linear in supplied context and payload size.
|
|
18
18
|
class TelemetryEvent
|
|
19
|
-
TYPES = %w(request query job cache external_http dependencies metric_batch).freeze
|
|
19
|
+
TYPES = %w(request query job cache external_http dependencies deploy metric_batch).freeze
|
|
20
20
|
|
|
21
21
|
attr_reader :event_id, :event_type, :timestamp, :context, :payload
|
|
22
22
|
|
|
@@ -68,6 +68,7 @@ module Chronos
|
|
|
68
68
|
@safe_serializer = options[:safe_serializer] || SafeSerializer.new
|
|
69
69
|
@max_payload_size = options[:max_payload_size] || proc { @config.max_payload_size }
|
|
70
70
|
@runtime_info = RuntimeInfo.new
|
|
71
|
+
@correlation = options[:correlation] || CorrelationContext.new(config)
|
|
71
72
|
end
|
|
72
73
|
|
|
73
74
|
def call(event)
|
|
@@ -89,13 +90,39 @@ module Chronos
|
|
|
89
90
|
"schema_version" => "1.0", "event_id" => event.event_id,
|
|
90
91
|
"event_type" => event.event_type, "occurred_at" => event.timestamp,
|
|
91
92
|
"sent_at" => @clock.call.utc.iso8601(6), "project_key" => @config.project_id,
|
|
92
|
-
"environment" =>
|
|
93
|
-
"service" =>
|
|
94
|
-
|
|
93
|
+
"environment" => event_environment(event),
|
|
94
|
+
"service" => event_service(event, runtime),
|
|
95
|
+
"correlation" => event_correlation(event, runtime),
|
|
95
96
|
"runtime" => runtime[:runtime], "context" => event.context, "payload" => event.payload
|
|
96
97
|
}
|
|
97
98
|
end
|
|
98
99
|
|
|
100
|
+
def event_environment(event)
|
|
101
|
+
event.event_type == "deploy" ? event.payload["environment"] : @config.environment
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def event_service(event, runtime)
|
|
105
|
+
deploy = event.event_type == "deploy" ? event.payload : {}
|
|
106
|
+
{
|
|
107
|
+
"name" => deploy["service"] || @config.service_name,
|
|
108
|
+
"version" => deploy["version"] || @config.app_version,
|
|
109
|
+
"instance_id" => deploy["instance"] || @config.instance_id || runtime[:host]
|
|
110
|
+
}
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def event_correlation(event, runtime)
|
|
114
|
+
overrides = {"instance" => @config.instance_id || runtime[:host]}
|
|
115
|
+
if event.event_type == "deploy"
|
|
116
|
+
overrides.merge!(
|
|
117
|
+
"release" => event.payload["version"], "revision" => event.payload["revision"],
|
|
118
|
+
"deploy_id" => event.payload["deploy_id"], "environment" => event.payload["environment"],
|
|
119
|
+
"service" => event.payload["service"], "region" => event.payload["region"],
|
|
120
|
+
"instance" => event.payload["instance"]
|
|
121
|
+
)
|
|
122
|
+
end
|
|
123
|
+
@correlation.call(overrides)
|
|
124
|
+
end
|
|
125
|
+
|
|
99
126
|
def compact_envelope(envelope)
|
|
100
127
|
envelope["context"] = {"_truncated" => true}
|
|
101
128
|
envelope["payload"] = {"_truncated" => true}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
require "securerandom"
|
|
2
|
+
|
|
3
|
+
module Chronos
|
|
4
|
+
module Integrations
|
|
5
|
+
# Propagates bounded Chronos context through Active Job serialization.
|
|
6
|
+
#
|
|
7
|
+
# @responsibility Add trace/request identifiers to the job envelope and restore them while performing.
|
|
8
|
+
# @motivation Active Job adapters commonly cross process boundaries where thread-local context is lost.
|
|
9
|
+
# @limits It changes only a namespaced serialization field, never public job arguments or adapter behavior.
|
|
10
|
+
# @collaborators ActiveJob::Base serialization hooks and the Chronos facade.
|
|
11
|
+
# @thread_safety Installation is mutex-protected and each job owns its restored context.
|
|
12
|
+
# @compatibility Active Job shipped with Rails 4.2 through Rails 5.2.
|
|
13
|
+
# @example
|
|
14
|
+
# Chronos::Integrations::ActiveJob.install(ActiveJob::Base)
|
|
15
|
+
# @errors Context failures are contained so enqueue and perform semantics remain unchanged.
|
|
16
|
+
# @performance Adds at most two strings of 128 bytes to a serialized job.
|
|
17
|
+
module ActiveJob
|
|
18
|
+
CONTEXT_KEY = "chronos_context".freeze
|
|
19
|
+
SCHEMA_VERSION = "1.0".freeze
|
|
20
|
+
MAX_IDENTIFIER_BYTES = 128
|
|
21
|
+
|
|
22
|
+
@mutex = Mutex.new
|
|
23
|
+
|
|
24
|
+
class << self
|
|
25
|
+
def install(base = nil, notifier = Chronos)
|
|
26
|
+
target = base || (::ActiveJob::Base if defined?(::ActiveJob::Base))
|
|
27
|
+
return false unless target && target.respond_to?(:prepend)
|
|
28
|
+
|
|
29
|
+
@mutex.synchronize do
|
|
30
|
+
return false if target.ancestors.include?(JobExtensions)
|
|
31
|
+
|
|
32
|
+
JobExtensions.notifier = notifier
|
|
33
|
+
target.send(:prepend, JobExtensions)
|
|
34
|
+
end
|
|
35
|
+
true
|
|
36
|
+
rescue StandardError
|
|
37
|
+
false
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def envelope(notifier)
|
|
41
|
+
source = notifier.respond_to?(:propagation_context) ? notifier.propagation_context : {}
|
|
42
|
+
source = {} unless source.is_a?(Hash)
|
|
43
|
+
context = %w(trace_id request_id).each_with_object({}) do |key, result|
|
|
44
|
+
value = source[key] || source[key.to_sym]
|
|
45
|
+
result[key] = bounded(value) unless value.to_s.empty?
|
|
46
|
+
end
|
|
47
|
+
context["trace_id"] = SecureRandom.uuid if context["trace_id"].to_s.empty?
|
|
48
|
+
{"schema_version" => SCHEMA_VERSION, "context" => context}
|
|
49
|
+
rescue StandardError
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def context(value)
|
|
54
|
+
return {} unless value.is_a?(Hash)
|
|
55
|
+
return {} unless (value["schema_version"] || value[:schema_version]).to_s == SCHEMA_VERSION
|
|
56
|
+
|
|
57
|
+
source = value["context"] || value[:context]
|
|
58
|
+
return {} unless source.is_a?(Hash)
|
|
59
|
+
|
|
60
|
+
%w(trace_id request_id).each_with_object({}) do |key, result|
|
|
61
|
+
candidate = source[key] || source[key.to_sym]
|
|
62
|
+
result[key] = bounded(candidate) unless candidate.to_s.empty?
|
|
63
|
+
end
|
|
64
|
+
rescue StandardError
|
|
65
|
+
{}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def bounded(value)
|
|
71
|
+
string = value.to_s
|
|
72
|
+
return string if string.bytesize <= MAX_IDENTIFIER_BYTES
|
|
73
|
+
|
|
74
|
+
string.byteslice(0, MAX_IDENTIFIER_BYTES)
|
|
75
|
+
rescue StandardError
|
|
76
|
+
""
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Active Job instance hooks installed through Module#prepend.
|
|
81
|
+
module JobExtensions
|
|
82
|
+
class << self
|
|
83
|
+
attr_accessor :notifier
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def serialize(*arguments)
|
|
87
|
+
data = super
|
|
88
|
+
begin
|
|
89
|
+
envelope = Chronos::Integrations::ActiveJob.envelope(JobExtensions.notifier || Chronos)
|
|
90
|
+
data[CONTEXT_KEY] = envelope if data.is_a?(Hash) && envelope
|
|
91
|
+
rescue StandardError
|
|
92
|
+
nil
|
|
93
|
+
end
|
|
94
|
+
data
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def deserialize(job_data)
|
|
98
|
+
result = super
|
|
99
|
+
begin
|
|
100
|
+
@chronos_context = Chronos::Integrations::ActiveJob.context(
|
|
101
|
+
job_data.is_a?(Hash) ? (job_data[CONTEXT_KEY] || job_data[CONTEXT_KEY.to_sym]) : nil
|
|
102
|
+
)
|
|
103
|
+
rescue StandardError
|
|
104
|
+
@chronos_context = {}
|
|
105
|
+
end
|
|
106
|
+
result
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def perform_now(*arguments, &block)
|
|
110
|
+
context = @chronos_context || {}
|
|
111
|
+
notifier = JobExtensions.notifier || Chronos
|
|
112
|
+
return super if context.empty? || !notifier.respond_to?(:with_context)
|
|
113
|
+
|
|
114
|
+
notifier.with_context(:context => context) { super }
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module Chronos
|
|
2
|
+
module Integrations
|
|
3
|
+
# Optional Capistrano task installer using only the public task DSL.
|
|
4
|
+
#
|
|
5
|
+
# @responsibility Register one post-publish task that delegates explicit values to notify_deploy.
|
|
6
|
+
# @motivation Legacy deployments need release reporting without a Capistrano runtime dependency.
|
|
7
|
+
# @limits Values are read only from configured Capistrano variables; Git and ENV are not inspected.
|
|
8
|
+
# @collaborators Capistrano/Rake DSL and the Chronos public facade.
|
|
9
|
+
# @thread_safety Installation is idempotent per DSL object before task execution.
|
|
10
|
+
# @compatibility Capistrano task DSLs available to supported Ruby 2.2.10 through Ruby 2.6 apps.
|
|
11
|
+
# @example
|
|
12
|
+
# require "chronos/capistrano"
|
|
13
|
+
# @errors Missing DSL methods return false; notification failures are contained by Chronos.
|
|
14
|
+
# @performance Adds one task and one after hook; no background worker is created.
|
|
15
|
+
module Capistrano
|
|
16
|
+
INSTALLED_KEY = :@__chronos_capistrano_installed
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
def install(dsl)
|
|
20
|
+
return false if dsl.instance_variable_get(INSTALLED_KEY)
|
|
21
|
+
return false unless compatible?(dsl)
|
|
22
|
+
|
|
23
|
+
dsl.send(:namespace, :chronos) do
|
|
24
|
+
dsl.send(:desc, "Notify Chronos about the published deployment") if dsl.respond_to?(:desc, true)
|
|
25
|
+
dsl.send(:task, :notify_deploy) { notify(dsl) }
|
|
26
|
+
end
|
|
27
|
+
dsl.send(:after, "deploy:published", "chronos:notify_deploy")
|
|
28
|
+
dsl.instance_variable_set(INSTALLED_KEY, true)
|
|
29
|
+
true
|
|
30
|
+
rescue StandardError
|
|
31
|
+
false
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def notify(dsl)
|
|
35
|
+
Chronos.notify_deploy(
|
|
36
|
+
:environment => value(dsl, :stage).to_s,
|
|
37
|
+
:revision => value(dsl, :current_revision) || value(dsl, :branch),
|
|
38
|
+
:version => value(dsl, :chronos_version) || value(dsl, :release_name),
|
|
39
|
+
:repository => value(dsl, :repo_url), :actor => value(dsl, :chronos_actor),
|
|
40
|
+
:deploy_id => value(dsl, :chronos_deploy_id), :service => value(dsl, :chronos_service),
|
|
41
|
+
:region => value(dsl, :chronos_region), :instance => value(dsl, :chronos_instance)
|
|
42
|
+
)
|
|
43
|
+
rescue StandardError
|
|
44
|
+
false
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def compatible?(dsl)
|
|
50
|
+
[:namespace, :task, :after, :fetch].all? { |name| dsl.respond_to?(name, true) }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def value(dsl, name)
|
|
54
|
+
dsl.send(:fetch, name, nil)
|
|
55
|
+
rescue StandardError
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module Chronos
|
|
2
|
-
# Public entry points for optional
|
|
2
|
+
# Public entry points for optional observability and deployment integrations.
|
|
3
3
|
#
|
|
4
|
-
# @responsibility Delegate dependency, cache,
|
|
4
|
+
# @responsibility Delegate dependency, cache, outbound HTTP, and deploy calls to the agent.
|
|
5
5
|
# @motivation Keep the main public facade small while preserving one stable Chronos namespace.
|
|
6
6
|
# @limits It installs only explicitly supplied Net::HTTP connections and never enables features.
|
|
7
7
|
# @collaborators Chronos::Agent and Chronos::Integrations::NetHttp.
|
|
@@ -19,6 +19,13 @@ module Chronos
|
|
|
19
19
|
false
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
+
def notify_deploy(attributes = {}, timeout = Agent::DEFAULT_FLUSH_TIMEOUT)
|
|
23
|
+
agent = current_agent
|
|
24
|
+
agent ? agent.notify_deploy(attributes, timeout) : false
|
|
25
|
+
rescue StandardError
|
|
26
|
+
false
|
|
27
|
+
end
|
|
28
|
+
|
|
22
29
|
def external_http_integration_options
|
|
23
30
|
agent = current_agent
|
|
24
31
|
agent ? agent.external_http_integration_options : {:enabled => false}
|
|
@@ -33,6 +33,7 @@ module Chronos
|
|
|
33
33
|
return false if self.class.applications[application.object_id]
|
|
34
34
|
|
|
35
35
|
install_middleware(application, options)
|
|
36
|
+
install_active_job
|
|
36
37
|
@subscriber.install
|
|
37
38
|
self.class.applications[application.object_id] = true
|
|
38
39
|
end
|
|
@@ -58,6 +59,12 @@ module Chronos
|
|
|
58
59
|
true
|
|
59
60
|
end
|
|
60
61
|
|
|
62
|
+
def install_active_job
|
|
63
|
+
return false unless defined?(::ActiveJob::Base)
|
|
64
|
+
|
|
65
|
+
Chronos::Integrations::ActiveJob.install(::ActiveJob::Base, @notifier)
|
|
66
|
+
end
|
|
67
|
+
|
|
61
68
|
def environment
|
|
62
69
|
defined?(::Rails) && ::Rails.respond_to?(:env) ? ::Rails.env.to_s : nil
|
|
63
70
|
end
|
|
@@ -144,11 +144,39 @@ module Chronos
|
|
|
144
144
|
|
|
145
145
|
def active_job(payload, duration)
|
|
146
146
|
job = value(payload, :job)
|
|
147
|
+
exception = job_exception(payload)
|
|
147
148
|
data = {
|
|
148
149
|
"kind" => "active_job", "class" => safe_class_name(job),
|
|
149
|
-
"
|
|
150
|
+
"adapter" => active_job_adapter(job), "job_id" => safe_job_value(job, :job_id),
|
|
151
|
+
"provider_job_id" => safe_job_value(job, :provider_job_id),
|
|
152
|
+
"queue" => safe_job_value(job, :queue_name), "attempts" => job_attempts(job),
|
|
153
|
+
"duration_ms" => duration, "status" => exception ? "failed" : "completed"
|
|
150
154
|
}
|
|
155
|
+
data["error_class"] = exception.class.name.to_s if exception
|
|
151
156
|
@notifier.record_event("job", data)
|
|
157
|
+
@notifier.notify_once(exception, :context => {"job" => data}) if exception
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def job_exception(payload)
|
|
161
|
+
exception = value(payload, :exception_object)
|
|
162
|
+
details = value(payload, :exception)
|
|
163
|
+
exception ||= RuntimeError.new(Array(details).last.to_s) if details
|
|
164
|
+
exception
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def active_job_adapter(job)
|
|
168
|
+
adapter = job.respond_to?(:queue_adapter) ? job.queue_adapter : nil
|
|
169
|
+
adapter = adapter.class if adapter && !adapter.is_a?(Class)
|
|
170
|
+
adapter ? adapter.name.to_s.split("::").last.to_s.sub(/Adapter$/, "") : ""
|
|
171
|
+
rescue StandardError
|
|
172
|
+
""
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def job_attempts(job)
|
|
176
|
+
value = job.respond_to?(:executions) ? job.executions : 0
|
|
177
|
+
value.is_a?(Numeric) ? value.to_i : 0
|
|
178
|
+
rescue StandardError
|
|
179
|
+
0
|
|
152
180
|
end
|
|
153
181
|
|
|
154
182
|
def cache(name, payload, duration)
|
data/lib/chronos/rails.rb
CHANGED
data/lib/chronos/version.rb
CHANGED
data/lib/chronos.rb
CHANGED
|
@@ -14,6 +14,8 @@ require "chronos/core/notice_builder"
|
|
|
14
14
|
require "chronos/core/sensitive_value_filter"
|
|
15
15
|
require "chronos/core/sanitizer"
|
|
16
16
|
require "chronos/core/safe_serializer"
|
|
17
|
+
require "chronos/core/correlation_context"
|
|
18
|
+
require "chronos/core/deploy_normalizer"
|
|
17
19
|
require "chronos/core/payload_serializer"
|
|
18
20
|
require "chronos/core/telemetry_event"
|
|
19
21
|
require "chronos/core/sql_normalizer"
|
|
@@ -31,6 +33,7 @@ require "chronos/core/breadcrumb"
|
|
|
31
33
|
require "chronos/application/retry_policy"
|
|
32
34
|
require "chronos/application/circuit_breaker"
|
|
33
35
|
require "chronos/application/remote_configuration"
|
|
36
|
+
require "chronos/application/ignore_policy"
|
|
34
37
|
require "chronos/application/delivery_pipeline"
|
|
35
38
|
require "chronos/application/capture_exception"
|
|
36
39
|
require "chronos/application/apm_error_classifier"
|
|
@@ -58,7 +61,7 @@ require "chronos/integrations/rack/middleware"
|
|
|
58
61
|
# config.host = "https://chronos.example.com"
|
|
59
62
|
# end
|
|
60
63
|
# Chronos.notify(RuntimeError.new("failed"))
|
|
61
|
-
module Chronos
|
|
64
|
+
module Chronos # rubocop:disable Metrics/ModuleLength
|
|
62
65
|
extend ObservabilityFacade
|
|
63
66
|
|
|
64
67
|
@mutex = Mutex.new
|
|
@@ -142,6 +145,13 @@ module Chronos
|
|
|
142
145
|
false
|
|
143
146
|
end
|
|
144
147
|
|
|
148
|
+
def ignore_if(&block)
|
|
149
|
+
agent = current_agent
|
|
150
|
+
agent ? agent.ignore_if(&block) : false
|
|
151
|
+
rescue StandardError
|
|
152
|
+
false
|
|
153
|
+
end
|
|
154
|
+
|
|
145
155
|
def rails_integration_options(environment = nil, console = false)
|
|
146
156
|
agent = current_agent
|
|
147
157
|
agent ? agent.rails_integration_options(environment, console) : {:enabled => false}
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: chronos-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.0.pre.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Antonio Jefferson
|
|
@@ -88,6 +88,7 @@ files:
|
|
|
88
88
|
- SECURITY.md
|
|
89
89
|
- contracts/apm-batch-v1.schema.json
|
|
90
90
|
- contracts/dependencies-v1.schema.json
|
|
91
|
+
- contracts/deploy-v1.schema.json
|
|
91
92
|
- contracts/event-v1.schema.json
|
|
92
93
|
- contracts/rack-context-v1.schema.json
|
|
93
94
|
- contracts/sidekiq-job-v1.schema.json
|
|
@@ -103,17 +104,24 @@ files:
|
|
|
103
104
|
- docs/adr/ADR-014-sidekiq-envelope-context.md
|
|
104
105
|
- docs/adr/ADR-015-bounded-apm-aggregation.md
|
|
105
106
|
- docs/adr/ADR-016-explicit-observability-integrations.md
|
|
107
|
+
- docs/adr/ADR-017-deploy-events-and-correlation.md
|
|
108
|
+
- docs/adr/ADR-018-pre-1.0-hardening.md
|
|
106
109
|
- docs/architecture.md
|
|
107
110
|
- docs/compatibility.md
|
|
108
111
|
- docs/configuration.md
|
|
109
112
|
- docs/data-collected.md
|
|
113
|
+
- docs/deprecation-policy.md
|
|
110
114
|
- docs/examples/plain-ruby.md
|
|
115
|
+
- docs/migration-from-airbrake.md
|
|
116
|
+
- docs/modules/active-job.md
|
|
111
117
|
- docs/modules/apm-aggregation.md
|
|
112
118
|
- docs/modules/async-queue.md
|
|
113
119
|
- docs/modules/cache-observability.md
|
|
114
120
|
- docs/modules/configuration.md
|
|
115
121
|
- docs/modules/dependencies.md
|
|
122
|
+
- docs/modules/deploy-tracking.md
|
|
116
123
|
- docs/modules/external-http.md
|
|
124
|
+
- docs/modules/ignore-rules.md
|
|
117
125
|
- docs/modules/notice-pipeline.md
|
|
118
126
|
- docs/modules/rack-context.md
|
|
119
127
|
- docs/modules/rails-legacy.md
|
|
@@ -126,6 +134,10 @@ files:
|
|
|
126
134
|
- docs/modules/transport.md
|
|
127
135
|
- docs/performance.md
|
|
128
136
|
- docs/privacy-lgpd.md
|
|
137
|
+
- docs/protocol-v1.md
|
|
138
|
+
- docs/release-1.0-readiness.md
|
|
139
|
+
- docs/security-review.md
|
|
140
|
+
- docs/semver.md
|
|
129
141
|
- docs/troubleshooting.md
|
|
130
142
|
- lib/chronos.rb
|
|
131
143
|
- lib/chronos/adapters.rb
|
|
@@ -140,8 +152,10 @@ files:
|
|
|
140
152
|
- lib/chronos/application/circuit_breaker.rb
|
|
141
153
|
- lib/chronos/application/delivery_pipeline.rb
|
|
142
154
|
- lib/chronos/application/dependency_reporter.rb
|
|
155
|
+
- lib/chronos/application/ignore_policy.rb
|
|
143
156
|
- lib/chronos/application/remote_configuration.rb
|
|
144
157
|
- lib/chronos/application/retry_policy.rb
|
|
158
|
+
- lib/chronos/capistrano.rb
|
|
145
159
|
- lib/chronos/configuration.rb
|
|
146
160
|
- lib/chronos/configuration/apm_validation.rb
|
|
147
161
|
- lib/chronos/configuration/snapshot.rb
|
|
@@ -150,6 +164,8 @@ files:
|
|
|
150
164
|
- lib/chronos/core/backtrace_parser.rb
|
|
151
165
|
- lib/chronos/core/breadcrumb.rb
|
|
152
166
|
- lib/chronos/core/cache_normalizer.rb
|
|
167
|
+
- lib/chronos/core/correlation_context.rb
|
|
168
|
+
- lib/chronos/core/deploy_normalizer.rb
|
|
153
169
|
- lib/chronos/core/exception_cause_collector.rb
|
|
154
170
|
- lib/chronos/core/metric_aggregate.rb
|
|
155
171
|
- lib/chronos/core/notice.rb
|
|
@@ -163,6 +179,8 @@ files:
|
|
|
163
179
|
- lib/chronos/core/telemetry_event.rb
|
|
164
180
|
- lib/chronos/errors.rb
|
|
165
181
|
- lib/chronos/integrations.rb
|
|
182
|
+
- lib/chronos/integrations/active_job.rb
|
|
183
|
+
- lib/chronos/integrations/capistrano.rb
|
|
166
184
|
- lib/chronos/integrations/job_payload.rb
|
|
167
185
|
- lib/chronos/integrations/net_http.rb
|
|
168
186
|
- lib/chronos/integrations/rack.rb
|