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,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Fields
6
+ class SummaryProxy
7
+ def initialize(store)
8
+ @store = store
9
+ end
10
+
11
+ def add(fields = nil, **keyword_fields)
12
+ current_scope.add_summary(summary_fields(fields, keyword_fields), owned: true)
13
+ self
14
+ end
15
+
16
+ def add_attributes(fields = nil, **keyword_fields)
17
+ current_scope.add_summary_attributes(summary_fields(fields, keyword_fields), owned: true)
18
+ self
19
+ end
20
+
21
+ def increment_attribute(*path, by: 1)
22
+ current_scope.increment_summary_attribute(path, by: by)
23
+ self
24
+ end
25
+
26
+ def increment(key, by: 1)
27
+ current_scope.increment_summary(key, by: by)
28
+ self
29
+ end
30
+
31
+ def measure(key, &)
32
+ raise ArgumentError, "block required" unless block_given?
33
+
34
+ current_scope.measure_summary(key, &)
35
+ end
36
+
37
+ def measure_start(key)
38
+ current_scope.measure_summary_start(key)
39
+ end
40
+
41
+ def append(key, value)
42
+ current_scope.append_summary(key, value)
43
+ self
44
+ end
45
+
46
+ def active?
47
+ @store.current_scope?
48
+ end
49
+
50
+ private
51
+
52
+ def current_scope
53
+ @store.current_scope || raise(Execution::NoCurrentError, "summary data requires a current execution scope")
54
+ end
55
+
56
+ def summary_fields(fields, keyword_fields)
57
+ FieldSet.coerce(fields, keyword_fields, invalid: :wrap)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Integration
6
+ # @api integration_spi
7
+ module Configurable
8
+ def configurable_with(&configuration_class)
9
+ raise ArgumentError, "configuration class block required" unless configuration_class
10
+
11
+ @julewire_configuration_class = configuration_class
12
+ end
13
+
14
+ def config
15
+ @config ||= build_config
16
+ end
17
+
18
+ def config=(configuration)
19
+ validate_config!(configuration)
20
+ @config = configuration
21
+ end
22
+
23
+ def configure
24
+ raise ArgumentError, "#{name}.configure requires a block" unless block_given?
25
+
26
+ yield config
27
+ config
28
+ end
29
+
30
+ def reset!
31
+ @config = build_config
32
+ end
33
+
34
+ private
35
+
36
+ def build_config
37
+ configuration_class.new
38
+ end
39
+
40
+ def validate_config!(configuration)
41
+ return if configuration.is_a?(configuration_class)
42
+
43
+ raise TypeError, "expected #{configuration_class.name}"
44
+ end
45
+
46
+ def configuration_class
47
+ @julewire_configuration_class.call
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Integration
6
+ # @api integration_spi
7
+ class DestinationHealth
8
+ def initialize(counter_keys:, failure_counter: :failures)
9
+ @failure_counter = failure_counter
10
+ @state = Diagnostics::Health.new(
11
+ counter_keys: counter_keys,
12
+ failure_counter: failure_counter,
13
+ track_failures: failure_counter == :failures
14
+ )
15
+ end
16
+
17
+ def increment(key, by: 1)
18
+ @state.increment(key, by: by)
19
+ end
20
+
21
+ def record_failure(error, counter: @failure_counter, **metadata)
22
+ @state.record_failure(error, counter: counter, degrade: false, **metadata)
23
+ end
24
+
25
+ def record_loss(reason:, counter: reason, **metadata)
26
+ @state.record_loss(reason: reason, counter: counter, degrade: false, **metadata)
27
+ end
28
+
29
+ def clear_degraded! = @state.clear_failures!
30
+
31
+ def degraded? = @state.degraded?(status_from: :failure_or_loss)
32
+
33
+ def last_loss = @state.last_loss
34
+
35
+ def last_failure = @state.last_failure
36
+
37
+ def snapshot(status: nil, **fields)
38
+ @state.snapshot(status: status, status_from: :failure_or_loss, include_loss: true, **fields)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Integration
6
+ # @api integration_spi
7
+ module EventSubscriber
8
+ class << self
9
+ def included(base)
10
+ base.extend(ClassMethods)
11
+ end
12
+ end
13
+
14
+ module ClassMethods
15
+ def event_subscriber(integration_health:, configuration_class:, component: :event_subscriber)
16
+ event_subscriber_options[:component] = component
17
+ event_subscriber_options[:configuration_class] = configuration_class
18
+ event_subscriber_options[:integration_health] = integration_health
19
+ end
20
+
21
+ def default_configuration
22
+ event_subscriber_options.fetch(:configuration_class).new
23
+ end
24
+
25
+ def event_subscriber_component
26
+ event_subscriber_options.fetch(:component)
27
+ end
28
+
29
+ def event_subscriber_health
30
+ event_subscriber_options.fetch(:integration_health)
31
+ end
32
+
33
+ private
34
+
35
+ def event_subscriber_options
36
+ @event_subscriber_options ||= {}
37
+ end
38
+ end
39
+
40
+ def initialize(configuration = self.class.default_configuration)
41
+ self.configuration = configuration
42
+ end
43
+
44
+ def configuration=(configuration)
45
+ @configuration = configuration
46
+ after_configuration_change
47
+ end
48
+
49
+ def emit(event)
50
+ self.class.event_subscriber_health.with_failure_health(
51
+ action: :emit,
52
+ component: self.class.event_subscriber_component
53
+ ) { emit_event(event) }
54
+ end
55
+
56
+ private
57
+
58
+ def after_configuration_change = nil
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Integration
6
+ # @api integration_spi
7
+ module Facade
8
+ class << self
9
+ def emit(record = Core::UNSET, enforce_level: true, **fields)
10
+ record = Core.emit_input(record, fields)
11
+ runtime = RuntimeLocator.current
12
+ if runtime.respond_to?(:emit_integration)
13
+ runtime.emit_integration(record, enforce_level: enforce_level)
14
+ elsif enforce_level
15
+ runtime.emit(record)
16
+ else
17
+ runtime.emit_without_level(record)
18
+ end
19
+ nil
20
+ end
21
+
22
+ def with_execution(type:, **, &)
23
+ raise ArgumentError, "block required" unless block_given?
24
+
25
+ integration_write_section!(:execution)
26
+ RuntimeLocator.current.with_execution(type: type, owned: true, **, &)
27
+ end
28
+
29
+ def with_attributes(fields, &)
30
+ with_fields(:attributes, fields, &)
31
+ end
32
+
33
+ def with_neutral(fields, &)
34
+ with_fields(:neutral, fields, &)
35
+ end
36
+
37
+ def with_carry(fields, &)
38
+ with_fields(:carry, fields, &)
39
+ end
40
+
41
+ def with_context(fields, &)
42
+ with_fields(:context, fields, &)
43
+ end
44
+
45
+ def add_context(fields)
46
+ add_fields(:context, fields)
47
+ end
48
+
49
+ def add_attributes(fields)
50
+ add_fields(:attributes, fields)
51
+ end
52
+
53
+ def add_neutral(fields)
54
+ add_fields(:neutral, fields)
55
+ end
56
+
57
+ def add_carry(fields)
58
+ add_fields(:carry, fields)
59
+ end
60
+
61
+ def add_summary_attributes(fields)
62
+ add_summary_fields(fields, :add_summary_attributes)
63
+ end
64
+
65
+ def add_summary_neutral(fields)
66
+ add_summary_fields(fields, :add_summary_neutral)
67
+ end
68
+
69
+ def summary_active?
70
+ current_scope?
71
+ end
72
+
73
+ def increment_summary_attribute(*path, by: 1)
74
+ scope = ContextStore.current.current_scope
75
+ return unless scope
76
+
77
+ scope.increment_summary_attribute(path, by: by)
78
+ nil
79
+ end
80
+
81
+ private
82
+
83
+ def current_scope?
84
+ ContextStore.current.current_scope?
85
+ end
86
+
87
+ def with_fields(section, fields, &)
88
+ raise ArgumentError, "block required" unless block_given?
89
+
90
+ integration_write_section!(section)
91
+ case section
92
+ when :attributes then ContextStore.current.with_attributes(fields, owned: true, &)
93
+ when :carry then ContextStore.current.with_carry(fields, owned: true, &)
94
+ when :context then ContextStore.current.with_context(fields, owned: true, &)
95
+ when :neutral then ContextStore.current.with_neutral(fields, owned: true, &)
96
+ end
97
+ end
98
+
99
+ def add_fields(section, fields)
100
+ integration_write_section!(section)
101
+ case section
102
+ when :attributes then ContextStore.current.add_attributes(fields, owned: true)
103
+ when :carry then ContextStore.current.add_carry(fields, owned: true)
104
+ when :context then ContextStore.current.add_context(fields, owned: true)
105
+ when :neutral then ContextStore.current.add_neutral(fields, owned: true)
106
+ end
107
+ nil
108
+ end
109
+
110
+ def integration_write_section!(section)
111
+ # Keep the failure path close to the table it protects; new field
112
+ # bags should not become integration-writable by accident.
113
+ return if Fields::Bags.integration_write_sections.include?(section)
114
+
115
+ raise ArgumentError, "integration cannot write #{section}"
116
+ end
117
+
118
+ def add_summary_fields(fields, writer)
119
+ integration_write_section!(:summary)
120
+ scope = ContextStore.current.current_scope
121
+ return unless scope && fields.is_a?(Hash)
122
+
123
+ fields = Serialization::DeepCompactEmpty.compact_owned!(fields)
124
+ scope.public_send(writer, fields, owned: true) unless fields.empty?
125
+ nil
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Integration
6
+ module ForkHooks
7
+ Entry = Data.define(:integration, :component, :callback)
8
+ private_constant :Entry
9
+
10
+ @mutex = Mutex.new
11
+ @entries = {}
12
+
13
+ class << self
14
+ def register(integration, component:, &callback)
15
+ raise ArgumentError, "block required" unless callback
16
+
17
+ name = integration_name(integration)
18
+ component = component.to_sym
19
+ register_entry(name, component, callback)
20
+ end
21
+
22
+ def run
23
+ snapshot = mutex.synchronize { entries.values }
24
+ snapshot.each { run_entry(it) }
25
+ nil
26
+ end
27
+
28
+ def after_fork!
29
+ @mutex = Mutex.new
30
+ nil
31
+ end
32
+
33
+ def reset!
34
+ mutex.synchronize { entries.clear }
35
+ nil
36
+ end
37
+
38
+ private
39
+
40
+ attr_reader :entries, :mutex
41
+
42
+ def register_entry(name, component, callback)
43
+ mutex.synchronize do
44
+ entries[[name, component]] = Entry.new(name, component, callback)
45
+ end
46
+ nil
47
+ rescue StandardError => e
48
+ Diagnostics::ProcessIntegrationHealth.record_failure(
49
+ name,
50
+ e,
51
+ action: :register_after_fork,
52
+ component: component
53
+ )
54
+ nil
55
+ end
56
+
57
+ def run_entry(entry)
58
+ entry.callback.call
59
+ rescue StandardError => e
60
+ Diagnostics::ProcessIntegrationHealth.record_failure(
61
+ entry.integration,
62
+ e,
63
+ action: :after_fork,
64
+ component: entry.component
65
+ )
66
+ nil
67
+ end
68
+
69
+ def integration_name(value)
70
+ name = value.to_s
71
+ raise ArgumentError, "integration name is required" if name.empty?
72
+
73
+ name.to_sym
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Integration
6
+ # @api integration_spi
7
+ module Health
8
+ class << self
9
+ def record_failure(integration, error, runtime: nil, **metadata)
10
+ if runtime
11
+ runtime.record_integration_failure(integration, error, **metadata)
12
+ else
13
+ Diagnostics::ProcessIntegrationHealth.record_failure(integration, error, **metadata)
14
+ end
15
+ nil
16
+ end
17
+
18
+ def record_success(integration, runtime: nil, **)
19
+ if runtime
20
+ runtime.record_integration_success(integration)
21
+ else
22
+ Diagnostics::ProcessIntegrationHealth.record_success(integration)
23
+ end
24
+ nil
25
+ end
26
+
27
+ def with_failure_health(integration, component:, action:, runtime: nil, **metadata)
28
+ yield.tap { record_success(integration, runtime: runtime) }
29
+ rescue StandardError => e
30
+ record_failure(integration, e, runtime: runtime, component: component, action: action, **metadata)
31
+ nil
32
+ end
33
+
34
+ def scoped(integration, runtime: nil)
35
+ Scoped.new(integration, runtime: runtime)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Integration
6
+ # @api integration_spi
7
+ class IvarState
8
+ def initialize(marker)
9
+ @marker = marker
10
+ end
11
+
12
+ def fetch(owner)
13
+ return unless owner.respond_to?(:instance_variable_get)
14
+
15
+ owner.instance_variable_get(@marker)
16
+ rescue StandardError
17
+ nil
18
+ end
19
+
20
+ def store(owner, value)
21
+ return value unless owner.respond_to?(:instance_variable_set)
22
+
23
+ owner.instance_variable_set(@marker, value)
24
+ value
25
+ rescue StandardError
26
+ value
27
+ end
28
+
29
+ def fetch_or_store(owner)
30
+ existing = fetch(owner)
31
+ return existing if existing
32
+
33
+ store(owner, yield)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Integration
6
+ # @api integration_spi
7
+ module Lifecycle
8
+ class << self
9
+ def require_optional(path)
10
+ require path
11
+ rescue LoadError
12
+ nil
13
+ end
14
+
15
+ def register_after_fork(integration, component:, &)
16
+ ForkHooks.register(integration, component: component, &)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Integration
6
+ # @api integration_spi
7
+ class Scoped
8
+ def initialize(integration, runtime: nil)
9
+ @integration = integration
10
+ @runtime = runtime
11
+ end
12
+
13
+ def record_failure(error, **metadata)
14
+ Health.record_failure(@integration, error, runtime: @runtime, **metadata)
15
+ end
16
+
17
+ def record_success(*, **)
18
+ Health.record_success(@integration, runtime: @runtime)
19
+ end
20
+
21
+ def with_failure_health(component:, action:, **metadata, &)
22
+ Health.with_failure_health(
23
+ @integration,
24
+ component: component,
25
+ action: action,
26
+ runtime: @runtime,
27
+ **metadata,
28
+ &
29
+ )
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Integration
6
+ # @api integration_spi
7
+ module Settings
8
+ class << self
9
+ def included(base)
10
+ base.extend(ClassMethods)
11
+ end
12
+ end
13
+
14
+ module ClassMethods
15
+ def setting(name, default: nil, predicate: false, validate: nil, &block)
16
+ settings_defaults[name] = block || proc { default }
17
+ settings_validators[name] = validate if validate
18
+ ivar = :"@#{name}"
19
+
20
+ define_method(name) { instance_variable_get(ivar) }
21
+ define_method(:"#{name}=") do |value|
22
+ instance_variable_set(ivar, validate_setting(name, value))
23
+ end
24
+
25
+ define_method(:"#{name}?") { !!public_send(name) } if predicate
26
+ end
27
+
28
+ def byte_limit
29
+ proc { |value, name| Core::Validation.validate_byte_limit!(value, name: name) }
30
+ end
31
+
32
+ def integer_limit(positive: false)
33
+ proc { |value, name| Core::Validation.validate_integer_limit!(value, name: name, positive: positive) }
34
+ end
35
+
36
+ def settings_defaults
37
+ @settings_defaults ||= {}
38
+ end
39
+
40
+ def settings_validators
41
+ @settings_validators ||= {}
42
+ end
43
+ end
44
+
45
+ def initialize
46
+ initialize_settings
47
+ end
48
+
49
+ def validate!
50
+ validate_settings!
51
+ self
52
+ end
53
+
54
+ private
55
+
56
+ def initialize_settings
57
+ self.class.settings_defaults.each do |name, default|
58
+ public_send(:"#{name}=", setting_default(default))
59
+ end
60
+ end
61
+
62
+ def setting_default(default)
63
+ Core::Fields::FieldSet.deep_dup(instance_exec(&default))
64
+ end
65
+
66
+ def validate_settings!
67
+ self.class.settings_defaults.each_key do |name|
68
+ public_send(:"#{name}=", public_send(name))
69
+ end
70
+ end
71
+
72
+ def validate_setting(name, value)
73
+ validator = self.class.settings_validators[name]
74
+ return value unless validator
75
+
76
+ result = call_setting_validator(validator, name, value)
77
+ result.nil? ? value : result
78
+ end
79
+
80
+ def call_setting_validator(validator, name, value)
81
+ case validator
82
+ when Symbol
83
+ validator_method = method(validator)
84
+ validator_method.arity == 1 ? validator_method.call(value) : validator_method.call(value, name)
85
+ else
86
+ validator.arity == 1 ? instance_exec(value, &validator) : instance_exec(value, name, &validator)
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Integration
6
+ # @api integration_spi
7
+ module SubscriberInstall
8
+ def subscriber = @subscription&.subscriber
9
+
10
+ def installed? = !subscriber.nil?
11
+
12
+ def reset!
13
+ @subscription&.reset
14
+ @subscription = nil
15
+ end
16
+
17
+ private
18
+
19
+ def update_subscription(configuration)
20
+ @subscription&.update(configuration)
21
+ end
22
+
23
+ def store_subscription(subscriber, unsubscribe: nil)
24
+ @subscription = Subscription.new(subscriber, unsubscribe: unsubscribe)
25
+ subscriber
26
+ end
27
+
28
+ def install_subscriber(configuration, enabled:)
29
+ return reset! unless enabled
30
+ return update_subscription(configuration) if installed?
31
+
32
+ subscriber = new(configuration)
33
+ unsubscribe = yield subscriber
34
+ store_subscription(subscriber, unsubscribe: unsubscribe)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end