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.
Files changed (164) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +6 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +73 -0
  5. data/docs/advanced-configuration.md +66 -0
  6. data/docs/attribute-keys.md +74 -0
  7. data/docs/configuration.md +327 -0
  8. data/docs/context-and-propagation.md +353 -0
  9. data/docs/contracts.md +211 -0
  10. data/docs/development.md +49 -0
  11. data/docs/extensions-and-api.md +567 -0
  12. data/docs/health-schema.md +104 -0
  13. data/docs/instrumentation-cheatsheet.md +29 -0
  14. data/docs/internals.md +135 -0
  15. data/docs/outputs-and-lifecycle.md +206 -0
  16. data/docs/quickstart.md +133 -0
  17. data/docs/record-sources.md +17 -0
  18. data/docs/records-and-data-policy.md +230 -0
  19. data/docs/security-and-wire.md +45 -0
  20. data/docs/tail.md +91 -0
  21. data/exe/julewire +6 -0
  22. data/julewire-core.gemspec +41 -0
  23. data/lib/julewire/core/cli/doctor.rb +143 -0
  24. data/lib/julewire/core/cli/line_helpers.rb +77 -0
  25. data/lib/julewire/core/cli/log_formats/console_text.rb +25 -0
  26. data/lib/julewire/core/cli/log_formats/core_json_decoder.rb +46 -0
  27. data/lib/julewire/core/cli/log_formats/core_json_encoder.rb +21 -0
  28. data/lib/julewire/core/cli/log_formats/record_decoder.rb +39 -0
  29. data/lib/julewire/core/cli/log_formats.rb +123 -0
  30. data/lib/julewire/core/cli/tail.rb +153 -0
  31. data/lib/julewire/core/cli/transcode.rb +105 -0
  32. data/lib/julewire/core/cli.rb +73 -0
  33. data/lib/julewire/core/configuration.rb +99 -0
  34. data/lib/julewire/core/context_store.rb +384 -0
  35. data/lib/julewire/core/destinations/chaos_output.rb +91 -0
  36. data/lib/julewire/core/destinations/collection.rb +177 -0
  37. data/lib/julewire/core/destinations/definition.rb +125 -0
  38. data/lib/julewire/core/destinations/destination.rb +268 -0
  39. data/lib/julewire/core/destinations/registry.rb +81 -0
  40. data/lib/julewire/core/destinations/sink.rb +35 -0
  41. data/lib/julewire/core/destinations/synchronized_output.rb +57 -0
  42. data/lib/julewire/core/destinations/tail_sampling.rb +321 -0
  43. data/lib/julewire/core/destinations/write_step.rb +119 -0
  44. data/lib/julewire/core/destinations.rb +33 -0
  45. data/lib/julewire/core/diagnostics/callback_notifier.rb +63 -0
  46. data/lib/julewire/core/diagnostics/doctor.rb +114 -0
  47. data/lib/julewire/core/diagnostics/failure_snapshot.rb +39 -0
  48. data/lib/julewire/core/diagnostics/health.rb +144 -0
  49. data/lib/julewire/core/diagnostics/integration_health_store.rb +64 -0
  50. data/lib/julewire/core/diagnostics/internal_records.rb +61 -0
  51. data/lib/julewire/core/diagnostics/invalid_severity_reporter.rb +112 -0
  52. data/lib/julewire/core/diagnostics/meta_observer.rb +161 -0
  53. data/lib/julewire/core/diagnostics/process_integration_health.rb +26 -0
  54. data/lib/julewire/core/diagnostics/tail/renderer.rb +36 -0
  55. data/lib/julewire/core/diagnostics/tail.rb +168 -0
  56. data/lib/julewire/core/diagnostics.rb +8 -0
  57. data/lib/julewire/core/error.rb +7 -0
  58. data/lib/julewire/core/execution/boundary.rb +106 -0
  59. data/lib/julewire/core/execution/handle.rb +77 -0
  60. data/lib/julewire/core/execution/lineage.rb +192 -0
  61. data/lib/julewire/core/execution/measurement_handle.rb +28 -0
  62. data/lib/julewire/core/execution/no_current_error.rb +9 -0
  63. data/lib/julewire/core/execution/scope.rb +246 -0
  64. data/lib/julewire/core/execution/scope_fields.rb +76 -0
  65. data/lib/julewire/core/execution/scope_identity.rb +71 -0
  66. data/lib/julewire/core/execution/scope_snapshot.rb +92 -0
  67. data/lib/julewire/core/execution/summary_state.rb +206 -0
  68. data/lib/julewire/core/execution/view.rb +56 -0
  69. data/lib/julewire/core/facade_methods.rb +181 -0
  70. data/lib/julewire/core/fields/attribute_keys.rb +54 -0
  71. data/lib/julewire/core/fields/attributes_proxy.rb +11 -0
  72. data/lib/julewire/core/fields/bags.rb +123 -0
  73. data/lib/julewire/core/fields/carry_proxy.rb +22 -0
  74. data/lib/julewire/core/fields/context_proxy.rb +11 -0
  75. data/lib/julewire/core/fields/field_set.rb +78 -0
  76. data/lib/julewire/core/fields/field_stack.rb +269 -0
  77. data/lib/julewire/core/fields/internal/deletion.rb +68 -0
  78. data/lib/julewire/core/fields/internal.rb +87 -0
  79. data/lib/julewire/core/fields/lookup.rb +35 -0
  80. data/lib/julewire/core/fields/section_proxy.rb +88 -0
  81. data/lib/julewire/core/fields/stack_set.rb +69 -0
  82. data/lib/julewire/core/fields/static_labels.rb +43 -0
  83. data/lib/julewire/core/fields/summary_proxy.rb +62 -0
  84. data/lib/julewire/core/integration/configurable.rb +52 -0
  85. data/lib/julewire/core/integration/destination_health.rb +43 -0
  86. data/lib/julewire/core/integration/event_subscriber.rb +62 -0
  87. data/lib/julewire/core/integration/facade.rb +131 -0
  88. data/lib/julewire/core/integration/fork_hooks.rb +79 -0
  89. data/lib/julewire/core/integration/health.rb +41 -0
  90. data/lib/julewire/core/integration/ivar_state.rb +38 -0
  91. data/lib/julewire/core/integration/lifecycle.rb +22 -0
  92. data/lib/julewire/core/integration/scoped.rb +34 -0
  93. data/lib/julewire/core/integration/settings.rb +92 -0
  94. data/lib/julewire/core/integration/subscriber_install.rb +39 -0
  95. data/lib/julewire/core/integration/subscription.rb +29 -0
  96. data/lib/julewire/core/integration/values.rb +192 -0
  97. data/lib/julewire/core/lifecycle_error.rb +7 -0
  98. data/lib/julewire/core/local_storage.rb +91 -0
  99. data/lib/julewire/core/processing/level_threshold.rb +53 -0
  100. data/lib/julewire/core/processing/match.rb +74 -0
  101. data/lib/julewire/core/processing/pipeline.rb +360 -0
  102. data/lib/julewire/core/processing/processor_chain.rb +69 -0
  103. data/lib/julewire/core/processing/processor_registry.rb +115 -0
  104. data/lib/julewire/core/processing/processor_wrapper.rb +44 -0
  105. data/lib/julewire/core/processing/record_field_transform.rb +124 -0
  106. data/lib/julewire/core/processing/sampling.rb +109 -0
  107. data/lib/julewire/core/processing.rb +41 -0
  108. data/lib/julewire/core/propagation/carrier.rb +93 -0
  109. data/lib/julewire/core/propagation.rb +50 -0
  110. data/lib/julewire/core/records/console_formatter.rb +24 -0
  111. data/lib/julewire/core/records/deconstruct.rb +19 -0
  112. data/lib/julewire/core/records/display_message.rb +166 -0
  113. data/lib/julewire/core/records/draft.rb +576 -0
  114. data/lib/julewire/core/records/formatter.rb +14 -0
  115. data/lib/julewire/core/records/lazy_emit_input.rb +99 -0
  116. data/lib/julewire/core/records/metadata.rb +23 -0
  117. data/lib/julewire/core/records/public_projection.rb +51 -0
  118. data/lib/julewire/core/records/raw_input.rb +41 -0
  119. data/lib/julewire/core/records/record.rb +175 -0
  120. data/lib/julewire/core/records/severity.rb +44 -0
  121. data/lib/julewire/core/runtime.rb +515 -0
  122. data/lib/julewire/core/runtime_locator.rb +20 -0
  123. data/lib/julewire/core/runtime_registry.rb +48 -0
  124. data/lib/julewire/core/runtime_state.rb +39 -0
  125. data/lib/julewire/core/scheduling/deadline.rb +24 -0
  126. data/lib/julewire/core/scheduling/deadline_scheduler.rb +207 -0
  127. data/lib/julewire/core/scheduling/shared_scheduler.rb +48 -0
  128. data/lib/julewire/core/sentinel.rb +18 -0
  129. data/lib/julewire/core/serialization/backtrace_limiter.rb +50 -0
  130. data/lib/julewire/core/serialization/bounded_transform.rb +55 -0
  131. data/lib/julewire/core/serialization/bounded_traversal.rb +274 -0
  132. data/lib/julewire/core/serialization/deep_compact_empty.rb +67 -0
  133. data/lib/julewire/core/serialization/deep_freeze.rb +63 -0
  134. data/lib/julewire/core/serialization/encoding_sanitizer.rb +40 -0
  135. data/lib/julewire/core/serialization/exception_shape.rb +88 -0
  136. data/lib/julewire/core/serialization/json_encoder.rb +69 -0
  137. data/lib/julewire/core/serialization/serializer.rb +233 -0
  138. data/lib/julewire/core/serialization/serializer_pool.rb +21 -0
  139. data/lib/julewire/core/serialization/text_encoder.rb +147 -0
  140. data/lib/julewire/core/serialization/value_copy.rb +209 -0
  141. data/lib/julewire/core/serialization/value_traversal.rb +150 -0
  142. data/lib/julewire/core/testing/chaos/catalog.rb +72 -0
  143. data/lib/julewire/core/testing/chaos/core_runtime.rb +120 -0
  144. data/lib/julewire/core/testing/chaos/destination.rb +55 -0
  145. data/lib/julewire/core/testing/chaos/emitter.rb +20 -0
  146. data/lib/julewire/core/testing/chaos/raising_output.rb +42 -0
  147. data/lib/julewire/core/testing/chaos.rb +80 -0
  148. data/lib/julewire/core/testing/contracts/component.rb +162 -0
  149. data/lib/julewire/core/testing/contracts/deadline_scheduler.rb +59 -0
  150. data/lib/julewire/core/testing/contracts/integration.rb +166 -0
  151. data/lib/julewire/core/testing/contracts/integration_fields.rb +36 -0
  152. data/lib/julewire/core/testing/contracts/record_draft.rb +37 -0
  153. data/lib/julewire/core/testing/contracts/runtime.rb +178 -0
  154. data/lib/julewire/core/testing/contracts/wire.rb +60 -0
  155. data/lib/julewire/core/testing/contracts.rb +24 -0
  156. data/lib/julewire/core/testing/coverage.rb +58 -0
  157. data/lib/julewire/core/testing/test_reports.rb +78 -0
  158. data/lib/julewire/core/testing.rb +122 -0
  159. data/lib/julewire/core/validation.rb +69 -0
  160. data/lib/julewire/core/version.rb +7 -0
  161. data/lib/julewire/core.rb +80 -0
  162. data/lib/julewire/error.rb +5 -0
  163. data/lib/julewire-core.rb +3 -0
  164. 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