julewire-core 1.0.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/CHANGELOG.md +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +73 -0
- data/docs/advanced-configuration.md +66 -0
- data/docs/attribute-keys.md +74 -0
- data/docs/configuration.md +327 -0
- data/docs/context-and-propagation.md +353 -0
- data/docs/contracts.md +211 -0
- data/docs/development.md +49 -0
- data/docs/extensions-and-api.md +567 -0
- data/docs/health-schema.md +104 -0
- data/docs/instrumentation-cheatsheet.md +29 -0
- data/docs/internals.md +135 -0
- data/docs/outputs-and-lifecycle.md +206 -0
- data/docs/quickstart.md +133 -0
- data/docs/record-sources.md +17 -0
- data/docs/records-and-data-policy.md +230 -0
- data/docs/security-and-wire.md +45 -0
- data/docs/tail.md +91 -0
- data/exe/julewire +6 -0
- data/julewire-core.gemspec +41 -0
- data/lib/julewire/core/cli/doctor.rb +143 -0
- data/lib/julewire/core/cli/line_helpers.rb +77 -0
- data/lib/julewire/core/cli/log_formats/console_text.rb +25 -0
- data/lib/julewire/core/cli/log_formats/core_json_decoder.rb +46 -0
- data/lib/julewire/core/cli/log_formats/core_json_encoder.rb +21 -0
- data/lib/julewire/core/cli/log_formats/record_decoder.rb +39 -0
- data/lib/julewire/core/cli/log_formats.rb +123 -0
- data/lib/julewire/core/cli/tail.rb +153 -0
- data/lib/julewire/core/cli/transcode.rb +105 -0
- data/lib/julewire/core/cli.rb +73 -0
- data/lib/julewire/core/configuration.rb +99 -0
- data/lib/julewire/core/context_store.rb +384 -0
- data/lib/julewire/core/destinations/chaos_output.rb +91 -0
- data/lib/julewire/core/destinations/collection.rb +177 -0
- data/lib/julewire/core/destinations/definition.rb +125 -0
- data/lib/julewire/core/destinations/destination.rb +268 -0
- data/lib/julewire/core/destinations/registry.rb +81 -0
- data/lib/julewire/core/destinations/sink.rb +35 -0
- data/lib/julewire/core/destinations/synchronized_output.rb +57 -0
- data/lib/julewire/core/destinations/tail_sampling.rb +321 -0
- data/lib/julewire/core/destinations/write_step.rb +119 -0
- data/lib/julewire/core/destinations.rb +33 -0
- data/lib/julewire/core/diagnostics/callback_notifier.rb +63 -0
- data/lib/julewire/core/diagnostics/doctor.rb +114 -0
- data/lib/julewire/core/diagnostics/failure_snapshot.rb +39 -0
- data/lib/julewire/core/diagnostics/health.rb +144 -0
- data/lib/julewire/core/diagnostics/integration_health_store.rb +64 -0
- data/lib/julewire/core/diagnostics/internal_records.rb +61 -0
- data/lib/julewire/core/diagnostics/invalid_severity_reporter.rb +112 -0
- data/lib/julewire/core/diagnostics/meta_observer.rb +161 -0
- data/lib/julewire/core/diagnostics/process_integration_health.rb +26 -0
- data/lib/julewire/core/diagnostics/tail/renderer.rb +36 -0
- data/lib/julewire/core/diagnostics/tail.rb +168 -0
- data/lib/julewire/core/diagnostics.rb +8 -0
- data/lib/julewire/core/error.rb +7 -0
- data/lib/julewire/core/execution/boundary.rb +106 -0
- data/lib/julewire/core/execution/handle.rb +77 -0
- data/lib/julewire/core/execution/lineage.rb +192 -0
- data/lib/julewire/core/execution/measurement_handle.rb +28 -0
- data/lib/julewire/core/execution/no_current_error.rb +9 -0
- data/lib/julewire/core/execution/scope.rb +246 -0
- data/lib/julewire/core/execution/scope_fields.rb +76 -0
- data/lib/julewire/core/execution/scope_identity.rb +71 -0
- data/lib/julewire/core/execution/scope_snapshot.rb +92 -0
- data/lib/julewire/core/execution/summary_state.rb +206 -0
- data/lib/julewire/core/execution/view.rb +56 -0
- data/lib/julewire/core/facade_methods.rb +181 -0
- data/lib/julewire/core/fields/attribute_keys.rb +54 -0
- data/lib/julewire/core/fields/attributes_proxy.rb +11 -0
- data/lib/julewire/core/fields/bags.rb +123 -0
- data/lib/julewire/core/fields/carry_proxy.rb +22 -0
- data/lib/julewire/core/fields/context_proxy.rb +11 -0
- data/lib/julewire/core/fields/field_set.rb +78 -0
- data/lib/julewire/core/fields/field_stack.rb +269 -0
- data/lib/julewire/core/fields/internal/deletion.rb +68 -0
- data/lib/julewire/core/fields/internal.rb +87 -0
- data/lib/julewire/core/fields/lookup.rb +35 -0
- data/lib/julewire/core/fields/section_proxy.rb +88 -0
- data/lib/julewire/core/fields/stack_set.rb +69 -0
- data/lib/julewire/core/fields/static_labels.rb +43 -0
- data/lib/julewire/core/fields/summary_proxy.rb +62 -0
- data/lib/julewire/core/integration/configurable.rb +52 -0
- data/lib/julewire/core/integration/destination_health.rb +43 -0
- data/lib/julewire/core/integration/event_subscriber.rb +62 -0
- data/lib/julewire/core/integration/facade.rb +131 -0
- data/lib/julewire/core/integration/fork_hooks.rb +79 -0
- data/lib/julewire/core/integration/health.rb +41 -0
- data/lib/julewire/core/integration/ivar_state.rb +38 -0
- data/lib/julewire/core/integration/lifecycle.rb +22 -0
- data/lib/julewire/core/integration/scoped.rb +34 -0
- data/lib/julewire/core/integration/settings.rb +92 -0
- data/lib/julewire/core/integration/subscriber_install.rb +39 -0
- data/lib/julewire/core/integration/subscription.rb +29 -0
- data/lib/julewire/core/integration/values.rb +192 -0
- data/lib/julewire/core/lifecycle_error.rb +7 -0
- data/lib/julewire/core/local_storage.rb +91 -0
- data/lib/julewire/core/processing/level_threshold.rb +53 -0
- data/lib/julewire/core/processing/match.rb +74 -0
- data/lib/julewire/core/processing/pipeline.rb +360 -0
- data/lib/julewire/core/processing/processor_chain.rb +69 -0
- data/lib/julewire/core/processing/processor_registry.rb +115 -0
- data/lib/julewire/core/processing/processor_wrapper.rb +44 -0
- data/lib/julewire/core/processing/record_field_transform.rb +124 -0
- data/lib/julewire/core/processing/sampling.rb +109 -0
- data/lib/julewire/core/processing.rb +41 -0
- data/lib/julewire/core/propagation/carrier.rb +93 -0
- data/lib/julewire/core/propagation.rb +50 -0
- data/lib/julewire/core/records/console_formatter.rb +24 -0
- data/lib/julewire/core/records/deconstruct.rb +19 -0
- data/lib/julewire/core/records/display_message.rb +166 -0
- data/lib/julewire/core/records/draft.rb +576 -0
- data/lib/julewire/core/records/formatter.rb +14 -0
- data/lib/julewire/core/records/lazy_emit_input.rb +99 -0
- data/lib/julewire/core/records/metadata.rb +23 -0
- data/lib/julewire/core/records/public_projection.rb +51 -0
- data/lib/julewire/core/records/raw_input.rb +41 -0
- data/lib/julewire/core/records/record.rb +175 -0
- data/lib/julewire/core/records/severity.rb +44 -0
- data/lib/julewire/core/runtime.rb +515 -0
- data/lib/julewire/core/runtime_locator.rb +20 -0
- data/lib/julewire/core/runtime_registry.rb +48 -0
- data/lib/julewire/core/runtime_state.rb +39 -0
- data/lib/julewire/core/scheduling/deadline.rb +24 -0
- data/lib/julewire/core/scheduling/deadline_scheduler.rb +207 -0
- data/lib/julewire/core/scheduling/shared_scheduler.rb +48 -0
- data/lib/julewire/core/sentinel.rb +18 -0
- data/lib/julewire/core/serialization/backtrace_limiter.rb +50 -0
- data/lib/julewire/core/serialization/bounded_transform.rb +55 -0
- data/lib/julewire/core/serialization/bounded_traversal.rb +274 -0
- data/lib/julewire/core/serialization/deep_compact_empty.rb +67 -0
- data/lib/julewire/core/serialization/deep_freeze.rb +63 -0
- data/lib/julewire/core/serialization/encoding_sanitizer.rb +40 -0
- data/lib/julewire/core/serialization/exception_shape.rb +88 -0
- data/lib/julewire/core/serialization/json_encoder.rb +69 -0
- data/lib/julewire/core/serialization/serializer.rb +233 -0
- data/lib/julewire/core/serialization/serializer_pool.rb +21 -0
- data/lib/julewire/core/serialization/text_encoder.rb +147 -0
- data/lib/julewire/core/serialization/value_copy.rb +209 -0
- data/lib/julewire/core/serialization/value_traversal.rb +150 -0
- data/lib/julewire/core/testing/chaos/catalog.rb +72 -0
- data/lib/julewire/core/testing/chaos/core_runtime.rb +120 -0
- data/lib/julewire/core/testing/chaos/destination.rb +55 -0
- data/lib/julewire/core/testing/chaos/emitter.rb +20 -0
- data/lib/julewire/core/testing/chaos/raising_output.rb +42 -0
- data/lib/julewire/core/testing/chaos.rb +80 -0
- data/lib/julewire/core/testing/contracts/component.rb +162 -0
- data/lib/julewire/core/testing/contracts/deadline_scheduler.rb +59 -0
- data/lib/julewire/core/testing/contracts/integration.rb +166 -0
- data/lib/julewire/core/testing/contracts/integration_fields.rb +36 -0
- data/lib/julewire/core/testing/contracts/record_draft.rb +37 -0
- data/lib/julewire/core/testing/contracts/runtime.rb +178 -0
- data/lib/julewire/core/testing/contracts/wire.rb +60 -0
- data/lib/julewire/core/testing/contracts.rb +24 -0
- data/lib/julewire/core/testing/coverage.rb +58 -0
- data/lib/julewire/core/testing/test_reports.rb +78 -0
- data/lib/julewire/core/testing.rb +122 -0
- data/lib/julewire/core/validation.rb +69 -0
- data/lib/julewire/core/version.rb +7 -0
- data/lib/julewire/core.rb +80 -0
- data/lib/julewire/error.rb +5 -0
- data/lib/julewire-core.rb +3 -0
- metadata +237 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
module Diagnostics
|
|
6
|
+
class Health
|
|
7
|
+
def initialize(
|
|
8
|
+
counter_keys:,
|
|
9
|
+
callback_failure_counter: nil,
|
|
10
|
+
callback_metadata: {},
|
|
11
|
+
failure_counter: nil,
|
|
12
|
+
track_failures: true
|
|
13
|
+
)
|
|
14
|
+
@callback_failure_counter = callback_failure_counter
|
|
15
|
+
@callback_metadata = callback_metadata
|
|
16
|
+
@failure_counter = failure_counter
|
|
17
|
+
@track_failures = track_failures
|
|
18
|
+
@mutex = Mutex.new
|
|
19
|
+
counter_keys = counter_keys.to_a
|
|
20
|
+
counter_keys = counter_keys.union([:failures]) if @track_failures
|
|
21
|
+
@counts = counter_keys.to_h { [it, 0] }
|
|
22
|
+
@current_degradation = nil
|
|
23
|
+
@last_callback_failure = nil
|
|
24
|
+
@last_failure = nil
|
|
25
|
+
@last_loss = nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def increment(key, by: 1)
|
|
29
|
+
@mutex.synchronize { increment_unlocked(key, by: by) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def counts
|
|
33
|
+
@mutex.synchronize { @counts.dup.freeze }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def degradation_marker
|
|
37
|
+
@mutex.synchronize { @current_degradation }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def degraded?(status_from: :current)
|
|
41
|
+
@mutex.synchronize { degraded_unlocked?(status_from) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def last_callback_failure
|
|
45
|
+
@mutex.synchronize { @last_callback_failure }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def last_failure
|
|
49
|
+
@mutex.synchronize { @last_failure }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def last_loss
|
|
53
|
+
@mutex.synchronize { @last_loss }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def clear_degradation
|
|
57
|
+
@mutex.synchronize { @current_degradation = nil }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def clear_degradation_if_unchanged(marker)
|
|
61
|
+
@mutex.synchronize { @current_degradation = nil if @current_degradation.equal?(marker) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def clear_failures!
|
|
65
|
+
@mutex.synchronize do
|
|
66
|
+
@current_degradation = nil
|
|
67
|
+
@last_callback_failure = nil
|
|
68
|
+
@last_failure = nil
|
|
69
|
+
@last_loss = nil
|
|
70
|
+
end
|
|
71
|
+
self
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def record_failure(error, callback: nil, counter: @failure_counter, degrade: true, **metadata)
|
|
75
|
+
failure = FailureSnapshot.build(error, **metadata)
|
|
76
|
+
@mutex.synchronize do
|
|
77
|
+
increment_unlocked(:failures) if @track_failures
|
|
78
|
+
increment_unlocked(counter) if counter && counter != :failures && @counts.key?(counter)
|
|
79
|
+
@last_failure = failure
|
|
80
|
+
@current_degradation = failure if degrade
|
|
81
|
+
end
|
|
82
|
+
notify_failure_callback(callback, error, metadata)
|
|
83
|
+
failure
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def record_callback_failure(callback_failure)
|
|
87
|
+
@mutex.synchronize do
|
|
88
|
+
@last_callback_failure = callback_failure.to_h
|
|
89
|
+
increment_unlocked(@callback_failure_counter) if @callback_failure_counter
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def record_loss(reason:, counter: reason, degrade: true, **metadata)
|
|
94
|
+
loss = { reason: reason }.merge(metadata).compact.freeze
|
|
95
|
+
@mutex.synchronize do
|
|
96
|
+
increment_unlocked(counter) if counter && @counts.key?(counter)
|
|
97
|
+
@last_loss = loss
|
|
98
|
+
@current_degradation = loss if degrade
|
|
99
|
+
end
|
|
100
|
+
loss
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def record_success
|
|
104
|
+
@mutex.synchronize { @current_degradation = nil }
|
|
105
|
+
self
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def snapshot(status: nil, status_from: :current, include_loss: false, **fields)
|
|
109
|
+
@mutex.synchronize do
|
|
110
|
+
result = {
|
|
111
|
+
counts: @counts.dup.freeze,
|
|
112
|
+
last_failure: @last_failure,
|
|
113
|
+
status: status || (degraded_unlocked?(status_from) ? :degraded : :ok)
|
|
114
|
+
}
|
|
115
|
+
result[:last_loss] = @last_loss if include_loss
|
|
116
|
+
result.merge(fields).compact.freeze
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
def notify_failure_callback(callback, error, metadata)
|
|
123
|
+
callback_result = CallbackNotifier.call(callback, error, @callback_metadata.merge(metadata))
|
|
124
|
+
record_callback_failure(callback_result) if CallbackNotifier.failure?(callback_result)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def increment_unlocked(key, by: 1)
|
|
128
|
+
@counts[key] = @counts.fetch(key) + by
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def degraded_unlocked?(status_from)
|
|
132
|
+
case status_from
|
|
133
|
+
when :current
|
|
134
|
+
!!@current_degradation
|
|
135
|
+
when :failure_or_loss
|
|
136
|
+
!!(@last_failure || @last_loss)
|
|
137
|
+
else
|
|
138
|
+
raise ArgumentError, "unknown health status source: #{status_from.inspect}"
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
module Diagnostics
|
|
6
|
+
class IntegrationHealthStore
|
|
7
|
+
def initialize
|
|
8
|
+
@mutex = Mutex.new
|
|
9
|
+
@entries = {}
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def record_failure(integration, error, **metadata)
|
|
13
|
+
name = normalize_name(integration)
|
|
14
|
+
metadata = { phase: :integration, integration: name }.merge(metadata)
|
|
15
|
+
@mutex.synchronize do
|
|
16
|
+
entry_for(name).record_failure(error, **metadata)
|
|
17
|
+
end
|
|
18
|
+
nil
|
|
19
|
+
rescue StandardError
|
|
20
|
+
nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def record_success(integration)
|
|
24
|
+
name = normalize_name(integration)
|
|
25
|
+
@mutex.synchronize do
|
|
26
|
+
entry_for(name).record_success
|
|
27
|
+
end
|
|
28
|
+
nil
|
|
29
|
+
rescue StandardError
|
|
30
|
+
nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def health
|
|
34
|
+
@mutex.synchronize do
|
|
35
|
+
@entries.to_h { |name, entry| [name, entry.snapshot] }.freeze
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def reset!
|
|
40
|
+
@mutex.synchronize { @entries.clear }
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def after_fork!
|
|
45
|
+
@mutex = Mutex.new
|
|
46
|
+
@entries = {}
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def entry_for(name)
|
|
53
|
+
@entries[name] ||= Health.new(counter_keys: [:failures])
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def normalize_name(value)
|
|
57
|
+
Core.normalize_name(value, name: :integration)
|
|
58
|
+
rescue StandardError
|
|
59
|
+
:unknown
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
module Diagnostics
|
|
6
|
+
module InternalRecords
|
|
7
|
+
class << self
|
|
8
|
+
def emit_error(error, error_backtrace_lines:)
|
|
9
|
+
Core::Records::Draft.build(
|
|
10
|
+
{
|
|
11
|
+
severity: :error,
|
|
12
|
+
kind: :point,
|
|
13
|
+
event: "julewire.emit_error",
|
|
14
|
+
source: "julewire",
|
|
15
|
+
message: "Julewire emit failed",
|
|
16
|
+
payload: {
|
|
17
|
+
error: failure_details(error)
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
context: {},
|
|
21
|
+
scope: nil,
|
|
22
|
+
error_backtrace_lines: error_backtrace_lines
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def processor_error(processor_name:, error:, record_metadata:, error_backtrace_lines:)
|
|
27
|
+
Core::Records::Draft.build(
|
|
28
|
+
{
|
|
29
|
+
severity: :error,
|
|
30
|
+
kind: :point,
|
|
31
|
+
event: "julewire.processor_error",
|
|
32
|
+
source: "julewire",
|
|
33
|
+
message: "Julewire processor failed",
|
|
34
|
+
labels: labels(record_metadata),
|
|
35
|
+
payload: {
|
|
36
|
+
processor: processor_name,
|
|
37
|
+
error: failure_details(error),
|
|
38
|
+
record: record_metadata
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
context: {},
|
|
42
|
+
scope: nil,
|
|
43
|
+
error_backtrace_lines: error_backtrace_lines
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def labels(record_metadata)
|
|
50
|
+
labels = record_metadata[:labels]
|
|
51
|
+
labels.is_a?(Hash) ? labels : {}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def failure_details(error)
|
|
55
|
+
{ class: error.class.name }.compact
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
module Diagnostics
|
|
6
|
+
module InvalidSeverityReporter
|
|
7
|
+
@warned = false
|
|
8
|
+
@mutex = Mutex.new
|
|
9
|
+
|
|
10
|
+
class RuntimeCounter
|
|
11
|
+
def initialize
|
|
12
|
+
@mutex = Mutex.new
|
|
13
|
+
@count = 0
|
|
14
|
+
@last = nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call(value, source: nil, event: nil)
|
|
18
|
+
metadata = InvalidSeverityReporter.metadata(value, source: source, event: event)
|
|
19
|
+
@mutex.synchronize do
|
|
20
|
+
@count += 1
|
|
21
|
+
@last = metadata
|
|
22
|
+
end
|
|
23
|
+
InvalidSeverityReporter.warn_once(metadata)
|
|
24
|
+
rescue StandardError
|
|
25
|
+
nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def health
|
|
29
|
+
@mutex.synchronize do
|
|
30
|
+
{
|
|
31
|
+
count: @count,
|
|
32
|
+
last_event: @last&.fetch(:event, nil),
|
|
33
|
+
last_source: @last&.fetch(:source, nil),
|
|
34
|
+
last_value_class: @last&.fetch(:value_class, nil)
|
|
35
|
+
}.compact
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def reset!
|
|
40
|
+
@mutex.synchronize do
|
|
41
|
+
@count = 0
|
|
42
|
+
@last = nil
|
|
43
|
+
end
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def reset_after_fork!
|
|
48
|
+
@mutex = Mutex.new
|
|
49
|
+
@count = 0
|
|
50
|
+
@last = nil
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private_constant :RuntimeCounter
|
|
56
|
+
|
|
57
|
+
class << self
|
|
58
|
+
def call(value, source: nil, event: nil)
|
|
59
|
+
warning_only.call(value, source: source, event: event)
|
|
60
|
+
rescue StandardError
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def counter = RuntimeCounter.new
|
|
65
|
+
|
|
66
|
+
def warn_once(metadata)
|
|
67
|
+
return unless first_warning?
|
|
68
|
+
|
|
69
|
+
# Bypass Ruby's verbosity gates; this warning is emitted once.
|
|
70
|
+
Warning.warn("julewire: unsupported record severity #{metadata.fetch(:value_class)}; using :info\n")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def reset!
|
|
74
|
+
@mutex.synchronize { @warned = false }
|
|
75
|
+
nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def reset_after_fork!
|
|
79
|
+
@mutex = Mutex.new
|
|
80
|
+
@warned = false
|
|
81
|
+
nil
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def metadata(value, source:, event:)
|
|
85
|
+
{
|
|
86
|
+
event: event,
|
|
87
|
+
source: source,
|
|
88
|
+
value_class: value.class.name || value.class.to_s
|
|
89
|
+
}.compact.freeze
|
|
90
|
+
rescue StandardError
|
|
91
|
+
{ value_class: "unknown" }.freeze
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def warning_only
|
|
97
|
+
@warning_only ||= RuntimeCounter.new
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def first_warning?
|
|
101
|
+
@mutex.synchronize do
|
|
102
|
+
return false if @warned
|
|
103
|
+
|
|
104
|
+
@warned = true
|
|
105
|
+
true
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
module Diagnostics
|
|
6
|
+
class MetaObserver
|
|
7
|
+
DEFAULT_EVENT = "julewire.runtime_health"
|
|
8
|
+
DEFAULT_INTERVAL = 30
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def attach!(runtime_name = :default, target: :meta, start: true, **)
|
|
12
|
+
observer = new(
|
|
13
|
+
runtime: Julewire.runtime(runtime_name),
|
|
14
|
+
target_runtime: Julewire.runtime(target),
|
|
15
|
+
runtime_name: runtime_name,
|
|
16
|
+
target_name: target,
|
|
17
|
+
**
|
|
18
|
+
)
|
|
19
|
+
observer.start! if start
|
|
20
|
+
observer
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def initialize(
|
|
25
|
+
runtime:,
|
|
26
|
+
target_runtime:,
|
|
27
|
+
runtime_name: :default,
|
|
28
|
+
target_name: :meta,
|
|
29
|
+
event: DEFAULT_EVENT,
|
|
30
|
+
interval: DEFAULT_INTERVAL,
|
|
31
|
+
include_ok: false,
|
|
32
|
+
scheduler: Scheduling::SharedScheduler
|
|
33
|
+
)
|
|
34
|
+
@runtime = runtime
|
|
35
|
+
@target_runtime = target_runtime
|
|
36
|
+
@runtime_name = Core.normalize_name(runtime_name, name: :runtime_name)
|
|
37
|
+
@target_name = Core.normalize_name(target_name, name: :target_name)
|
|
38
|
+
@event = event.to_s
|
|
39
|
+
@interval = Validation.validate_integer_limit!(interval, name: :interval, positive: true)
|
|
40
|
+
@include_ok = include_ok ? true : false
|
|
41
|
+
@scheduler = scheduler
|
|
42
|
+
@mutex = Mutex.new
|
|
43
|
+
@last_signature = nil
|
|
44
|
+
@last_failure = nil
|
|
45
|
+
@started = false
|
|
46
|
+
@stopped = false
|
|
47
|
+
@token = nil
|
|
48
|
+
@serializer_pool_key = :"julewire_core_meta_observer_serializers_#{object_id}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def start!
|
|
52
|
+
@mutex.synchronize do
|
|
53
|
+
return self if @started && !@stopped
|
|
54
|
+
|
|
55
|
+
@started = true
|
|
56
|
+
@stopped = false
|
|
57
|
+
schedule_next
|
|
58
|
+
end
|
|
59
|
+
self
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def stop!
|
|
63
|
+
token = @mutex.synchronize do
|
|
64
|
+
@stopped = true
|
|
65
|
+
@started = false
|
|
66
|
+
@token
|
|
67
|
+
end
|
|
68
|
+
@scheduler.cancel(token) if token
|
|
69
|
+
self
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def sample!
|
|
73
|
+
health = @runtime.health
|
|
74
|
+
signature = signature_for(health)
|
|
75
|
+
changed = @mutex.synchronize do
|
|
76
|
+
changed = signature != @last_signature
|
|
77
|
+
@last_signature = signature
|
|
78
|
+
changed
|
|
79
|
+
end
|
|
80
|
+
return false unless changed
|
|
81
|
+
return false unless emit_health?(health)
|
|
82
|
+
|
|
83
|
+
emit_health(health)
|
|
84
|
+
true
|
|
85
|
+
rescue StandardError => e
|
|
86
|
+
record_failure(e)
|
|
87
|
+
false
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def health
|
|
91
|
+
@mutex.synchronize do
|
|
92
|
+
{
|
|
93
|
+
event: @event,
|
|
94
|
+
include_ok: @include_ok,
|
|
95
|
+
interval: @interval,
|
|
96
|
+
last_failure: @last_failure,
|
|
97
|
+
observed_runtime: @runtime_name,
|
|
98
|
+
running: @started && !@stopped,
|
|
99
|
+
status: @last_failure ? :degraded : :ok,
|
|
100
|
+
target_runtime: @target_name
|
|
101
|
+
}.compact.freeze
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def schedule_next
|
|
108
|
+
@token = @scheduler.schedule(@interval) { scheduled_sample }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def scheduled_sample
|
|
112
|
+
sample!
|
|
113
|
+
@mutex.synchronize { schedule_next unless @stopped }
|
|
114
|
+
rescue StandardError => e
|
|
115
|
+
record_failure(e)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def emit_health?(health)
|
|
119
|
+
@include_ok || health[:status] != :ok
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def emit_health(health)
|
|
123
|
+
status = health.fetch(:status, :unknown)
|
|
124
|
+
@target_runtime.emit_without_level(
|
|
125
|
+
severity: severity_for(status),
|
|
126
|
+
source: :julewire,
|
|
127
|
+
event: @event,
|
|
128
|
+
message: "Julewire runtime #{@runtime_name} is #{status}",
|
|
129
|
+
runtime: @runtime_name,
|
|
130
|
+
status: status,
|
|
131
|
+
health: health
|
|
132
|
+
)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def severity_for(status)
|
|
136
|
+
status == :ok ? :info : :warn
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def signature_for(health)
|
|
140
|
+
serializer = cached_serializer
|
|
141
|
+
return build_serializer.serialize(health).hash if serializer.in_use?
|
|
142
|
+
|
|
143
|
+
serializer.serialize(health).hash
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def cached_serializer
|
|
147
|
+
Serialization::SerializerPool.serializer(@serializer_pool_key, :signature) { build_serializer }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def build_serializer
|
|
151
|
+
Serialization::Serializer.new(compact_empty: true)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def record_failure(error)
|
|
155
|
+
failure = FailureSnapshot.build(error, phase: :meta_observer)
|
|
156
|
+
@mutex.synchronize { @last_failure = failure }
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
module Diagnostics
|
|
6
|
+
module ProcessIntegrationHealth
|
|
7
|
+
@store = IntegrationHealthStore.new
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
def record_failure(...) = @store.record_failure(...)
|
|
11
|
+
|
|
12
|
+
def record_success(...) = @store.record_success(...)
|
|
13
|
+
|
|
14
|
+
def health = @store.health
|
|
15
|
+
|
|
16
|
+
def reset! = @store.reset!
|
|
17
|
+
|
|
18
|
+
def after_fork!
|
|
19
|
+
@store = IntegrationHealthStore.new
|
|
20
|
+
nil
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
module Diagnostics
|
|
6
|
+
class Tail
|
|
7
|
+
class Renderer
|
|
8
|
+
DEFAULT_MAX_VALUE_BYTES = Serialization::TextEncoder::DEFAULT_MAX_VALUE_BYTES
|
|
9
|
+
|
|
10
|
+
def initialize(max_value_bytes: DEFAULT_MAX_VALUE_BYTES)
|
|
11
|
+
@max_value_bytes = Validation.validate_integer_limit!(
|
|
12
|
+
max_value_bytes,
|
|
13
|
+
name: :max_value_bytes,
|
|
14
|
+
positive: true
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def call(entries, color: false)
|
|
19
|
+
encoder = Serialization::TextEncoder.new(
|
|
20
|
+
color: color,
|
|
21
|
+
max_value_bytes: @max_value_bytes
|
|
22
|
+
)
|
|
23
|
+
entries.map { encoder.call(payload_for(it)) }.join
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def payload_for(entry)
|
|
29
|
+
record = entry.record
|
|
30
|
+
record.merge("timestamp" => record["timestamp"] || entry.at.iso8601(6))
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|