chronos-ruby 0.7.0.pre.1 → 0.9.0.pre.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 +4 -4
- data/CHANGELOG.md +49 -0
- data/README.md +55 -9
- data/contracts/apm-batch-v1.schema.json +1 -1
- data/contracts/dependencies-v1.schema.json +38 -0
- data/contracts/deploy-v1.schema.json +26 -0
- data/contracts/event-v1.schema.json +15 -1
- data/docs/adr/ADR-016-explicit-observability-integrations.md +25 -0
- data/docs/adr/ADR-017-deploy-events-and-correlation.md +25 -0
- data/docs/architecture.md +12 -1
- data/docs/compatibility.md +4 -0
- data/docs/configuration.md +24 -3
- data/docs/data-collected.md +11 -4
- data/docs/examples/plain-ruby.md +13 -1
- data/docs/modules/apm-aggregation.md +2 -2
- data/docs/modules/cache-observability.md +16 -0
- data/docs/modules/configuration.md +4 -0
- data/docs/modules/dependencies.md +18 -0
- data/docs/modules/deploy-tracking.md +86 -0
- data/docs/modules/external-http.md +27 -0
- data/docs/modules/rails-legacy.md +1 -1
- data/docs/modules/sidekiq-legacy.md +1 -1
- data/docs/modules/telemetry-events.md +4 -2
- data/docs/performance.md +30 -1
- data/docs/privacy-lgpd.md +20 -2
- data/docs/troubleshooting.md +25 -1
- data/lib/chronos/agent.rb +43 -0
- data/lib/chronos/application/apm_aggregator.rb +7 -10
- data/lib/chronos/application/apm_error_classifier.rb +27 -0
- data/lib/chronos/application/capture_telemetry.rb +18 -1
- data/lib/chronos/application/dependency_reporter.rb +140 -0
- data/lib/chronos/application/remote_configuration.rb +3 -1
- data/lib/chronos/capistrano.rb +4 -0
- data/lib/chronos/configuration/apm_validation.rb +28 -1
- data/lib/chronos/configuration.rb +25 -3
- data/lib/chronos/core/cache_normalizer.rb +99 -0
- 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/capistrano.rb +61 -0
- data/lib/chronos/integrations/net_http.rb +165 -0
- data/lib/chronos/net_http.rb +4 -0
- data/lib/chronos/observability_facade.rb +50 -0
- data/lib/chronos/rails/notifications_subscriber.rb +6 -5
- data/lib/chronos/version.rb +1 -1
- data/lib/chronos.rb +8 -0
- metadata +19 -1
|
@@ -2,7 +2,7 @@ module Chronos
|
|
|
2
2
|
module Internal
|
|
3
3
|
# Validates bounded APM aggregation, histogram, and detector settings.
|
|
4
4
|
#
|
|
5
|
-
# @responsibility Return configuration errors for
|
|
5
|
+
# @responsibility Return configuration errors for bounded APM and observability options.
|
|
6
6
|
# @motivation Keep the general configuration validator focused and maintainable.
|
|
7
7
|
# @limits It validates shape and bounds but does not allocate aggregator state.
|
|
8
8
|
# @collaborators Chronos::Configuration predicates and attributes.
|
|
@@ -55,6 +55,33 @@ module Chronos
|
|
|
55
55
|
|
|
56
56
|
values.each_cons(2).all? { |left, right| right > left }
|
|
57
57
|
end
|
|
58
|
+
|
|
59
|
+
def observability_errors
|
|
60
|
+
errors = []
|
|
61
|
+
errors << "external_http_enabled must be true or false" unless boolean?(external_http_enabled)
|
|
62
|
+
errors << "external_http_trace_headers must be true or false" unless boolean?(external_http_trace_headers)
|
|
63
|
+
errors << "cache_key_mode must be :none or :sha256" unless [:none, :sha256].include?(cache_key_mode)
|
|
64
|
+
errors << "dependency_reporting must be true or false" unless boolean?(dependency_reporting)
|
|
65
|
+
unless dependency_max_items.is_a?(Integer) && dependency_max_items >= 1 && dependency_max_items <= 200
|
|
66
|
+
errors << "dependency_max_items must be between 1 and 200"
|
|
67
|
+
end
|
|
68
|
+
correlation_attributes.each do |name, value|
|
|
69
|
+
errors << "#{name} must be a String with at most 128 bytes" unless bounded_optional_string?(value, 128)
|
|
70
|
+
end
|
|
71
|
+
errors
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def correlation_attributes
|
|
75
|
+
{:revision => revision, :deploy_id => deploy_id, :region => region, :instance_id => instance_id}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def bounded_optional_string?(value, limit)
|
|
79
|
+
value.nil? || (value.is_a?(String) && value.bytesize <= limit)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def boolean?(value)
|
|
83
|
+
[true, false].include?(value)
|
|
84
|
+
end
|
|
58
85
|
end
|
|
59
86
|
end
|
|
60
87
|
end
|
|
@@ -32,7 +32,8 @@ module Chronos
|
|
|
32
32
|
|
|
33
33
|
ATTRIBUTES = [
|
|
34
34
|
:project_id, :project_key, :host, :environment, :app_version,
|
|
35
|
-
:service_name, :
|
|
35
|
+
:service_name, :revision, :deploy_id, :region, :instance_id,
|
|
36
|
+
:root_directory, :logger, :timeout, :open_timeout,
|
|
36
37
|
:queue_size, :workers, :enabled, :error_notifications,
|
|
37
38
|
:ignored_environments, :proxy, :ssl_verify, :user_agent,
|
|
38
39
|
:max_payload_size, :gzip, :blocklist_keys, :allowlist_keys,
|
|
@@ -46,7 +47,8 @@ module Chronos
|
|
|
46
47
|
:apm_enabled, :apm_max_groups, :apm_flush_count, :apm_batch_size,
|
|
47
48
|
:apm_max_queries_per_request, :apm_slow_query_threshold_ms,
|
|
48
49
|
:apm_long_transaction_threshold_ms, :apm_n_plus_one_threshold,
|
|
49
|
-
:apm_histogram_buckets
|
|
50
|
+
:apm_histogram_buckets, :external_http_enabled, :external_http_trace_headers,
|
|
51
|
+
:cache_key_mode, :dependency_reporting, :dependency_max_items
|
|
50
52
|
].freeze
|
|
51
53
|
|
|
52
54
|
attr_accessor(*ATTRIBUTES)
|
|
@@ -57,6 +59,8 @@ module Chronos
|
|
|
57
59
|
initialize_resilience_defaults
|
|
58
60
|
initialize_rails_defaults
|
|
59
61
|
initialize_apm_defaults
|
|
62
|
+
initialize_observability_defaults
|
|
63
|
+
initialize_correlation_defaults
|
|
60
64
|
end
|
|
61
65
|
|
|
62
66
|
def snapshot
|
|
@@ -86,6 +90,7 @@ module Chronos
|
|
|
86
90
|
errors.concat(privacy_errors)
|
|
87
91
|
errors.concat(context_errors)
|
|
88
92
|
errors.concat(apm_errors)
|
|
93
|
+
errors.concat(observability_errors)
|
|
89
94
|
errors
|
|
90
95
|
end
|
|
91
96
|
|
|
@@ -117,6 +122,13 @@ module Chronos
|
|
|
117
122
|
@breadcrumb_max_bytes = 2048
|
|
118
123
|
end
|
|
119
124
|
|
|
125
|
+
def initialize_correlation_defaults
|
|
126
|
+
@revision = nil
|
|
127
|
+
@deploy_id = nil
|
|
128
|
+
@region = nil
|
|
129
|
+
@instance_id = nil
|
|
130
|
+
end
|
|
131
|
+
|
|
120
132
|
def initialize_rails_defaults
|
|
121
133
|
@rails_enabled = true
|
|
122
134
|
@rails_capture_in_console = false
|
|
@@ -136,6 +148,14 @@ module Chronos
|
|
|
136
148
|
@apm_histogram_buckets = [5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0, 1000.0, 2500.0, 5000.0]
|
|
137
149
|
end
|
|
138
150
|
|
|
151
|
+
def initialize_observability_defaults
|
|
152
|
+
@external_http_enabled = false
|
|
153
|
+
@external_http_trace_headers = true
|
|
154
|
+
@cache_key_mode = :none
|
|
155
|
+
@dependency_reporting = true
|
|
156
|
+
@dependency_max_items = 100
|
|
157
|
+
end
|
|
158
|
+
|
|
139
159
|
def initialize_privacy_defaults
|
|
140
160
|
@blocklist_keys = DEFAULT_BLOCKLIST_KEYS.dup
|
|
141
161
|
@allowlist_keys = []
|
|
@@ -155,7 +175,9 @@ module Chronos
|
|
|
155
175
|
@remote_configuration = true
|
|
156
176
|
@remote_config_max_bytes = 4096
|
|
157
177
|
@sampling_rate = 1.0
|
|
158
|
-
@enabled_event_types = [
|
|
178
|
+
@enabled_event_types = [
|
|
179
|
+
"exception", "request", "query", "job", "cache", "external_http", "dependencies", "deploy", "metric_batch"
|
|
180
|
+
]
|
|
159
181
|
@max_remote_send_interval = 60.0
|
|
160
182
|
end
|
|
161
183
|
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
require "digest"
|
|
2
|
+
|
|
3
|
+
module Chronos
|
|
4
|
+
module Core
|
|
5
|
+
# Produces bounded cache metadata without exposing raw cache keys or values.
|
|
6
|
+
#
|
|
7
|
+
# @responsibility Normalize operation, backend, namespace, outcome, and optional key hash.
|
|
8
|
+
# @motivation Cache telemetry needs diagnostic identity without leaking application keys.
|
|
9
|
+
# @limits It never reads cache values and hashes keys only after explicit opt-in.
|
|
10
|
+
# @collaborators Rails notification subscriber and Chronos configuration.
|
|
11
|
+
# @thread_safety Instances hold only immutable scalar configuration.
|
|
12
|
+
# @compatibility Ruby 2.2.10 through Ruby 2.6.
|
|
13
|
+
# @example
|
|
14
|
+
# CacheNormalizer.new("project", :none).call(name, payload)
|
|
15
|
+
# @errors Unreadable fields become empty bounded strings.
|
|
16
|
+
# @performance Constant work plus SHA-256 only when key hashing is enabled.
|
|
17
|
+
class CacheNormalizer
|
|
18
|
+
def initialize(project_id, key_mode)
|
|
19
|
+
@project_id = project_id.to_s
|
|
20
|
+
@key_mode = key_mode
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def call(name, payload = {})
|
|
24
|
+
hit = hit_value(name, payload)
|
|
25
|
+
result = {
|
|
26
|
+
"operation" => bounded(name.to_s.split(".").first, 64),
|
|
27
|
+
"backend" => backend(payload),
|
|
28
|
+
"namespace" => bounded(namespace(payload).to_s, 128),
|
|
29
|
+
"hit" => hit,
|
|
30
|
+
"outcome" => outcome(hit)
|
|
31
|
+
}
|
|
32
|
+
result["key_hash"] = key_hash(value(payload, :key)) if @key_mode == :sha256
|
|
33
|
+
result.delete_if { |key, child| child.to_s.empty? && key != "hit" }
|
|
34
|
+
rescue StandardError
|
|
35
|
+
{"operation" => "cache", "outcome" => "unknown"}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def outcome(hit)
|
|
41
|
+
return "unknown" if hit.nil?
|
|
42
|
+
|
|
43
|
+
hit ? "hit" : "miss"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def backend(payload)
|
|
47
|
+
store = value(payload, :store)
|
|
48
|
+
return "" if store.nil?
|
|
49
|
+
|
|
50
|
+
name = store.is_a?(String) || store.is_a?(Symbol) ? store.to_s : store.class.name.to_s
|
|
51
|
+
bounded(name, 128)
|
|
52
|
+
rescue StandardError
|
|
53
|
+
""
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def namespace(payload)
|
|
57
|
+
direct = value(payload, :namespace)
|
|
58
|
+
return direct unless direct.nil?
|
|
59
|
+
|
|
60
|
+
value(value(payload, :options), :namespace)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def hit_value(name, payload)
|
|
64
|
+
return true if name.to_s.start_with?("cache_fetch_hit")
|
|
65
|
+
|
|
66
|
+
hit = value(payload, :hit)
|
|
67
|
+
[true, false].include?(hit) ? hit : nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def key_hash(key)
|
|
71
|
+
return nil if key.nil?
|
|
72
|
+
|
|
73
|
+
Digest::SHA256.hexdigest("#{@project_id}:cache:#{safe_key(key)}")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def safe_key(key)
|
|
77
|
+
text = key.to_s
|
|
78
|
+
text = text.scrub("?") if text.respond_to?(:scrub)
|
|
79
|
+
text.bytesize > 2048 ? text.byteslice(0, 2048) : text
|
|
80
|
+
rescue StandardError
|
|
81
|
+
"[UNREADABLE]"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def value(hash, key)
|
|
85
|
+
return nil unless hash.is_a?(Hash)
|
|
86
|
+
|
|
87
|
+
hash.key?(key) ? hash[key] : hash[key.to_s]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def bounded(value, limit)
|
|
91
|
+
text = value.to_s
|
|
92
|
+
text = text.scrub("?") if text.respond_to?(:scrub)
|
|
93
|
+
text.bytesize > limit ? text.byteslice(0, limit) : text
|
|
94
|
+
rescue StandardError
|
|
95
|
+
""
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
module Chronos
|
|
2
|
+
module Core
|
|
3
|
+
# Builds the bounded release/deploy correlation shared by every event envelope.
|
|
4
|
+
#
|
|
5
|
+
# @responsibility Normalize release, revision, deploy, environment, service, region, and instance.
|
|
6
|
+
# @motivation Give the SaaS stable before/after-deploy dimensions without integration-specific fields.
|
|
7
|
+
# @limits Values come only from explicit configuration or caller overrides; no environment is scanned.
|
|
8
|
+
# @collaborators PayloadSerializer, TelemetrySerializer, and Configuration::Snapshot.
|
|
9
|
+
# @thread_safety Instances hold immutable configuration and calls allocate independent frozen hashes.
|
|
10
|
+
# @compatibility Ruby 2.2.10 through Ruby 2.6.
|
|
11
|
+
# @example
|
|
12
|
+
# CorrelationContext.new(config).call("revision" => "abc123")
|
|
13
|
+
# @errors Unreadable optional values become nil and do not escape.
|
|
14
|
+
# @performance Seven strings are copied with a fixed 128-byte maximum each.
|
|
15
|
+
class CorrelationContext
|
|
16
|
+
FIELDS = %w(release revision deploy_id environment service region instance).freeze
|
|
17
|
+
|
|
18
|
+
def initialize(config)
|
|
19
|
+
@config = config
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def call(overrides = {})
|
|
23
|
+
values = defaults.merge(string_hash(overrides))
|
|
24
|
+
result = FIELDS.each_with_object({}) do |name, correlation|
|
|
25
|
+
correlation[name.freeze] = bounded(values[name], 128)
|
|
26
|
+
end
|
|
27
|
+
result.each_value { |value| value.freeze if value }
|
|
28
|
+
result.freeze
|
|
29
|
+
rescue StandardError
|
|
30
|
+
FIELDS.each_with_object({}) { |name, fallback| fallback[name] = nil }.freeze
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def defaults
|
|
36
|
+
{
|
|
37
|
+
"release" => @config.app_version, "revision" => @config.revision,
|
|
38
|
+
"deploy_id" => @config.deploy_id, "environment" => @config.environment,
|
|
39
|
+
"service" => @config.service_name, "region" => @config.region,
|
|
40
|
+
"instance" => @config.instance_id
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def string_hash(value)
|
|
45
|
+
return {} unless value.is_a?(Hash)
|
|
46
|
+
|
|
47
|
+
value.each_with_object({}) do |(key, child), result|
|
|
48
|
+
result[key.to_s] = child if key.is_a?(String) || key.is_a?(Symbol)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def bounded(value, limit)
|
|
53
|
+
return nil if value.nil?
|
|
54
|
+
return nil unless value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(Numeric)
|
|
55
|
+
|
|
56
|
+
text = value.to_s
|
|
57
|
+
text = text.scrub("?") if text.respond_to?(:scrub)
|
|
58
|
+
text = text.byteslice(0, limit) if text.bytesize > limit
|
|
59
|
+
text
|
|
60
|
+
rescue StandardError
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -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 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,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
|