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,246 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
module Execution
|
|
6
|
+
class Scope
|
|
7
|
+
EMPTY_HASH = {}.freeze
|
|
8
|
+
private_constant :EMPTY_HASH
|
|
9
|
+
|
|
10
|
+
attr_reader :finished_at
|
|
11
|
+
|
|
12
|
+
def initialize(type:, id: nil, execution: EMPTY_HASH, execution_owned: false, summary_event: nil, # rubocop:disable Metrics/ParameterLists
|
|
13
|
+
summary_severity: nil, summary_source: nil, attributes: EMPTY_HASH, carry: EMPTY_HASH,
|
|
14
|
+
context: EMPTY_HASH, labels: EMPTY_HASH, neutral: EMPTY_HASH, parent: nil, started_at: nil)
|
|
15
|
+
@identity = ScopeIdentity.new(
|
|
16
|
+
type: type,
|
|
17
|
+
id: id,
|
|
18
|
+
started_at: started_at,
|
|
19
|
+
parent: parent,
|
|
20
|
+
parent_reference: parent&.execution_reference_for_child
|
|
21
|
+
)
|
|
22
|
+
@summary_state = summary_state(summary_event, summary_severity, summary_source)
|
|
23
|
+
@execution = @identity.execution_fields(execution, owned: execution_owned)
|
|
24
|
+
initialize_state(context: context, attributes: attributes, labels: labels, carry: carry, neutral: neutral)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def id = @identity.id
|
|
28
|
+
|
|
29
|
+
def type = @identity.type
|
|
30
|
+
|
|
31
|
+
def started_at = @identity.started_at
|
|
32
|
+
|
|
33
|
+
def lineage = @identity.lineage
|
|
34
|
+
|
|
35
|
+
def parent = @identity.parent
|
|
36
|
+
|
|
37
|
+
def depth = @identity.depth
|
|
38
|
+
|
|
39
|
+
def execution_hash
|
|
40
|
+
Fields::FieldSet.deep_dup(frozen_execution_hash)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def frozen_execution_hash
|
|
44
|
+
@frozen_execution_hash ||= @identity.frozen_execution_hash(@execution)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def inheritable_execution_hash
|
|
48
|
+
Fields::FieldSet.deep_dup(@execution)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def context_hash = @fields.context_hash
|
|
52
|
+
|
|
53
|
+
def carry_hash = @fields.carry_hash
|
|
54
|
+
|
|
55
|
+
def attributes_hash = @fields.attributes_hash
|
|
56
|
+
|
|
57
|
+
def neutral_hash = @fields.neutral_hash
|
|
58
|
+
|
|
59
|
+
def field_stacks = @fields.stacks
|
|
60
|
+
|
|
61
|
+
def labels_hash = @fields.labels_hash
|
|
62
|
+
|
|
63
|
+
def frozen_labels_hash = @fields.frozen_labels_hash
|
|
64
|
+
|
|
65
|
+
def field_hash(section)
|
|
66
|
+
@fields.field_hash(section)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def field_stack(section)
|
|
70
|
+
@fields.field_stack(section)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def summary_hash
|
|
74
|
+
@summary_state.payload_hash
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def metrics_hash
|
|
78
|
+
@summary_state.metrics_hash
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def measure_summary(key)
|
|
82
|
+
measurement = @summary_state.measurement(key)
|
|
83
|
+
started = monotonic_time
|
|
84
|
+
begin
|
|
85
|
+
yield
|
|
86
|
+
ensure
|
|
87
|
+
@summary_state.record_measurement(measurement, ((monotonic_time - started) * 1000).round(3))
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def measure_summary_start(key)
|
|
92
|
+
measurement = @summary_state.measurement(key)
|
|
93
|
+
started = monotonic_time
|
|
94
|
+
MeasurementHandle.new do
|
|
95
|
+
@summary_state.record_measurement(measurement, ((monotonic_time - started) * 1000).round(3))
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def add_field(section, fields, owned: false)
|
|
100
|
+
@fields.add(section, fields, owned: owned)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def delete_carry(path)
|
|
104
|
+
path = Fields::Internal.normalize_path(path)
|
|
105
|
+
@fields.delete(:carry, path)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def with_field(section, fields, owned: false, &)
|
|
109
|
+
@fields.with(section, fields, owned: owned, &)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def with_context(fields, &)
|
|
113
|
+
with_field(:context, fields, &)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def with_carry(fields, &)
|
|
117
|
+
with_field(:carry, fields, &)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def without_carry(path, &)
|
|
121
|
+
normalized_path = Fields::Internal.normalize_path(path)
|
|
122
|
+
raise ArgumentError, "carry path is required" if normalized_path.empty?
|
|
123
|
+
|
|
124
|
+
@fields.without(:carry, normalized_path, &)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def add_summary(fields, owned: false)
|
|
128
|
+
@summary_state.add(fields, owned: owned)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def add_summary_attributes(fields, owned: false)
|
|
132
|
+
@summary_state.add_attributes(fields, owned: owned)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def add_summary_neutral(fields, owned: false)
|
|
136
|
+
@summary_state.add_neutral(fields, owned: owned)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def increment_summary_attribute(path, by: 1)
|
|
140
|
+
@summary_state.increment_attribute(path, by: by)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def increment_summary(key, by: 1)
|
|
144
|
+
@summary_state.increment(key, by: by)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def append_summary(key, value)
|
|
148
|
+
@summary_state.append(key, value)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def summary_record_input
|
|
152
|
+
@summary_state.record_input(**summary_record_fields(timestamp: finished_at || frozen_time(Time.now.utc)))
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def owned_summary_record_input
|
|
156
|
+
@summary_state.owned_record_input(
|
|
157
|
+
**summary_record_fields(timestamp: finished_at || frozen_time(Time.now.utc))
|
|
158
|
+
)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def finished?
|
|
162
|
+
!finished_at.nil?
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def finish_owned(error: nil, severity: nil, finished_at: Time.now.utc)
|
|
166
|
+
# The first completion snapshot wins; later finish calls are no-ops.
|
|
167
|
+
return owned_summary_record_input if finished?
|
|
168
|
+
|
|
169
|
+
record_error(error, severity: severity) if error
|
|
170
|
+
@finished_at = frozen_time(finished_at)
|
|
171
|
+
@summary_state.record_duration(((monotonic_time - @identity.started_monotonic) * 1000).round(3))
|
|
172
|
+
@summary_state.finalize_record_input(**summary_record_fields(timestamp: @finished_at))
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def record_error(error, severity: nil)
|
|
176
|
+
@summary_state.record_error(error, severity: severity)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def non_standard_exception?
|
|
180
|
+
@summary_state.non_standard_exception?
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
private
|
|
184
|
+
|
|
185
|
+
def monotonic_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
186
|
+
|
|
187
|
+
def frozen_time(value) = @identity.frozen_time(value)
|
|
188
|
+
|
|
189
|
+
def initialize_state(context:, attributes:, labels:, carry: {}, neutral: {})
|
|
190
|
+
@fields = ScopeFields.new(
|
|
191
|
+
context: context,
|
|
192
|
+
carry: carry,
|
|
193
|
+
attributes: attributes,
|
|
194
|
+
labels: labels,
|
|
195
|
+
neutral: neutral
|
|
196
|
+
)
|
|
197
|
+
@finished_at = nil
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def summary_state(event, severity, source)
|
|
201
|
+
SummaryState.new(
|
|
202
|
+
event: normalize_summary_event(event),
|
|
203
|
+
severity: normalize_summary_severity(severity),
|
|
204
|
+
source: normalize_summary_source(source)
|
|
205
|
+
)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def normalize_summary_event(event)
|
|
209
|
+
normalized = event.nil? ? "#{type}.completed" : event.to_s
|
|
210
|
+
raise ArgumentError, "summary event is required" if normalized.empty?
|
|
211
|
+
|
|
212
|
+
normalized
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def normalize_summary_severity(severity)
|
|
216
|
+
Records::Severity.normalize(severity) unless severity.nil?
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def normalize_summary_source(source)
|
|
220
|
+
normalized = source.nil? ? "julewire" : source.to_s
|
|
221
|
+
raise ArgumentError, "summary source is required" if normalized.empty?
|
|
222
|
+
|
|
223
|
+
normalized
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def summary_record_fields(timestamp:)
|
|
227
|
+
{
|
|
228
|
+
timestamp: timestamp,
|
|
229
|
+
execution: execution_hash,
|
|
230
|
+
context: context_hash,
|
|
231
|
+
carry: carry_hash,
|
|
232
|
+
neutral: neutral_hash,
|
|
233
|
+
attributes: attributes_hash,
|
|
234
|
+
labels: labels_hash
|
|
235
|
+
}
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
protected
|
|
239
|
+
|
|
240
|
+
def execution_reference_for_child
|
|
241
|
+
@identity.reference
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
module Execution
|
|
6
|
+
class ScopeFields
|
|
7
|
+
EMPTY_HASH = {}.freeze
|
|
8
|
+
private_constant :EMPTY_HASH
|
|
9
|
+
|
|
10
|
+
attr_reader :stacks
|
|
11
|
+
|
|
12
|
+
def initialize(context:, attributes:, labels:, carry:, neutral:)
|
|
13
|
+
@stacks = Fields::StackSet.new(
|
|
14
|
+
context: context,
|
|
15
|
+
carry: carry,
|
|
16
|
+
attributes: attributes,
|
|
17
|
+
neutral: neutral
|
|
18
|
+
)
|
|
19
|
+
@labels = normalized_static_hash(labels)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def context_hash = @stacks.snapshot(:context)
|
|
23
|
+
|
|
24
|
+
def carry_hash = @stacks.snapshot(:carry)
|
|
25
|
+
|
|
26
|
+
def attributes_hash = @stacks.snapshot(:attributes)
|
|
27
|
+
|
|
28
|
+
def neutral_hash = @stacks.snapshot(:neutral)
|
|
29
|
+
|
|
30
|
+
def field_hash(section)
|
|
31
|
+
@stacks.snapshot(section)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def field_stack(section)
|
|
35
|
+
@stacks.stack(section)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def labels_hash
|
|
39
|
+
return {} if @labels.empty?
|
|
40
|
+
|
|
41
|
+
Fields::FieldSet.deep_dup(@labels)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def frozen_labels_hash
|
|
45
|
+
return EMPTY_HASH if @labels.empty?
|
|
46
|
+
|
|
47
|
+
@frozen_labels_hash ||= Fields::Internal.frozen_copy(@labels)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def add(section, fields, owned: false)
|
|
51
|
+
@stacks.add(section, fields, owned: owned)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def delete(section, path)
|
|
55
|
+
@stacks.delete(section, path)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def with(section, fields, owned: false, &)
|
|
59
|
+
@stacks.with(section, fields, owned: owned, &)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def without(section, path, &)
|
|
63
|
+
@stacks.without(section, path, &)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def normalized_static_hash(value)
|
|
69
|
+
return EMPTY_HASH if value.is_a?(Hash) && value.empty?
|
|
70
|
+
|
|
71
|
+
Fields::FieldSet.deep_symbolize_keys(value)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module Julewire
|
|
6
|
+
module Core
|
|
7
|
+
module Execution
|
|
8
|
+
class ScopeIdentity
|
|
9
|
+
attr_reader :id, :lineage, :parent, :reference, :started_at, :started_monotonic, :type
|
|
10
|
+
|
|
11
|
+
def initialize(type:, id: nil, started_at: nil, parent: nil, parent_reference: nil)
|
|
12
|
+
@id = normalize_id(id || SecureRandom.uuid)
|
|
13
|
+
@type = normalize_type(type)
|
|
14
|
+
@started_at = frozen_time(started_at || Time.now.utc)
|
|
15
|
+
@started_monotonic = monotonic_time
|
|
16
|
+
@parent = parent
|
|
17
|
+
@reference = { type: @type, id: @id }.freeze
|
|
18
|
+
@lineage = Lineage.new(
|
|
19
|
+
reference: @reference,
|
|
20
|
+
parent_lineage: parent&.lineage,
|
|
21
|
+
parent_reference: parent_reference
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def depth = @lineage.depth
|
|
26
|
+
|
|
27
|
+
def execution_fields(execution, owned:)
|
|
28
|
+
fields = owned ? Lineage.clean_owned_execution_hash(execution) : Lineage.clean_execution_hash(execution)
|
|
29
|
+
fields[:type] = @type
|
|
30
|
+
fields[:id] = @id
|
|
31
|
+
fields
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def frozen_execution_hash(execution)
|
|
35
|
+
@lineage.merge_into_frozen(execution)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def frozen_time(value)
|
|
39
|
+
Serialization::ValueCopy.call(value, freeze_values: true)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def monotonic_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
45
|
+
|
|
46
|
+
def normalize_type(type)
|
|
47
|
+
normalized = type.to_s
|
|
48
|
+
raise ArgumentError, "execution type is required" if normalized.empty?
|
|
49
|
+
|
|
50
|
+
freeze_identity_value(normalized)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def normalize_id(id)
|
|
54
|
+
freeze_identity_value(id)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def freeze_identity_value(value)
|
|
58
|
+
case value
|
|
59
|
+
when String
|
|
60
|
+
copy = value.frozen? ? value : value.dup
|
|
61
|
+
copy.freeze
|
|
62
|
+
when Symbol, Numeric, true, false, nil
|
|
63
|
+
value
|
|
64
|
+
else
|
|
65
|
+
Serialization::ValueCopy.call(value, freeze_values: true)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
module Execution
|
|
6
|
+
class ScopeSnapshot
|
|
7
|
+
EMPTY_HASH = {}.freeze
|
|
8
|
+
private_constant :EMPTY_HASH
|
|
9
|
+
|
|
10
|
+
def initialize(execution: {}, carry: {}, attributes: {}, labels: {}, neutral: {}, lineage: nil)
|
|
11
|
+
@execution = normalized_hash(execution)
|
|
12
|
+
@carry = normalized_hash(carry)
|
|
13
|
+
@attributes = normalized_hash(attributes)
|
|
14
|
+
@neutral = normalized_hash(neutral)
|
|
15
|
+
@labels = normalized_hash(labels)
|
|
16
|
+
@lineage = lineage || Lineage.from_execution_hash(@execution)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def execution_hash
|
|
20
|
+
Fields::FieldSet.deep_dup(frozen_execution_hash)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def frozen_execution_hash
|
|
24
|
+
@frozen_execution_hash ||= Fields::Internal.frozen_copy(@execution)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
attr_reader :lineage
|
|
28
|
+
|
|
29
|
+
def id = @execution[:id]
|
|
30
|
+
|
|
31
|
+
def type = @execution[:type]
|
|
32
|
+
|
|
33
|
+
def started_at = nil
|
|
34
|
+
|
|
35
|
+
def finished_at = nil
|
|
36
|
+
|
|
37
|
+
def parent = nil
|
|
38
|
+
|
|
39
|
+
def context_hash = {}
|
|
40
|
+
|
|
41
|
+
def carry_hash
|
|
42
|
+
return {} if @carry.empty?
|
|
43
|
+
|
|
44
|
+
Fields::FieldSet.deep_dup(@carry)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def attributes_hash
|
|
48
|
+
return {} if @attributes.empty?
|
|
49
|
+
|
|
50
|
+
Fields::FieldSet.deep_dup(@attributes)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def neutral_hash
|
|
54
|
+
return {} if @neutral.empty?
|
|
55
|
+
|
|
56
|
+
Fields::FieldSet.deep_dup(@neutral)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def labels_hash
|
|
60
|
+
return {} if @labels.empty?
|
|
61
|
+
|
|
62
|
+
Fields::FieldSet.deep_dup(@labels)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def summary_hash = {}
|
|
66
|
+
|
|
67
|
+
def metrics_hash = {}
|
|
68
|
+
|
|
69
|
+
def frozen_labels_hash
|
|
70
|
+
return EMPTY_HASH if @labels.empty?
|
|
71
|
+
|
|
72
|
+
@frozen_labels_hash ||= Fields::Internal.frozen_copy(@labels)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def execution_reference_for_child
|
|
76
|
+
reference = {}
|
|
77
|
+
reference[:type] = @execution[:type] if @execution.key?(:type)
|
|
78
|
+
reference[:id] = @execution[:id] if @execution.key?(:id)
|
|
79
|
+
reference.empty? ? nil : Fields::Internal.frozen_copy(reference)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def normalized_hash(value)
|
|
85
|
+
return EMPTY_HASH if value.is_a?(Hash) && value.empty?
|
|
86
|
+
|
|
87
|
+
Fields::FieldSet.deep_symbolize_keys(value)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
module Execution
|
|
6
|
+
class SummaryState
|
|
7
|
+
Measurement = Data.define(:count_key, :duration_key)
|
|
8
|
+
private_constant :Measurement
|
|
9
|
+
|
|
10
|
+
def initialize(event:, severity:, source:)
|
|
11
|
+
@event = event
|
|
12
|
+
@severity = severity
|
|
13
|
+
@source = source
|
|
14
|
+
@payload = {}
|
|
15
|
+
@neutral = {}
|
|
16
|
+
@attributes = {}
|
|
17
|
+
@metrics = {}
|
|
18
|
+
@errors = []
|
|
19
|
+
@error_severity = nil
|
|
20
|
+
@record_input = nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def payload_hash
|
|
24
|
+
Fields::FieldSet.deep_dup(@payload)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def metrics_hash
|
|
28
|
+
Fields::FieldSet.deep_dup(@metrics)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def add(fields, owned: false)
|
|
32
|
+
merge_fields!(@payload, fields, owned: owned)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def add_attributes(fields, owned: false)
|
|
36
|
+
deep_merge_fields!(@attributes, fields, owned: owned)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def add_neutral(fields, owned: false)
|
|
40
|
+
deep_merge_fields!(@neutral, fields, owned: owned)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def increment_attribute(path, by: 1)
|
|
44
|
+
path = Fields::Internal.normalize_path(path)
|
|
45
|
+
raise ArgumentError, "attribute path is required" if path.empty?
|
|
46
|
+
|
|
47
|
+
container = attribute_container(path)
|
|
48
|
+
key = path.last
|
|
49
|
+
value = Fields::FieldSet.value_for(container, key, default: MISSING)
|
|
50
|
+
existing = !value.equal?(MISSING)
|
|
51
|
+
container[Fields::Internal.normalize_key(key)] = incremented_value(value, by, existing: existing)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def increment(key, by: 1)
|
|
55
|
+
key = Fields::Internal.normalize_key(key)
|
|
56
|
+
value = Fields::FieldSet.value_for(@payload, key, default: MISSING)
|
|
57
|
+
existing = !value.equal?(MISSING)
|
|
58
|
+
@payload[key] = incremented_value(value, by, existing: existing)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def append(key, value)
|
|
62
|
+
key = Fields::Internal.normalize_key(key)
|
|
63
|
+
current = Fields::FieldSet.value_for(@payload, key, default: MISSING)
|
|
64
|
+
values = array_value(current, existing: !current.equal?(MISSING))
|
|
65
|
+
@payload[key] = values
|
|
66
|
+
values << Fields::FieldSet.deep_dup(value)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def record_error(error, severity: nil)
|
|
70
|
+
@errors << error
|
|
71
|
+
@error_severity = Records::Severity.normalize(severity) unless severity.nil?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def non_standard_exception?
|
|
75
|
+
@errors.any? { !it.is_a?(StandardError) }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def record_duration(duration_ms)
|
|
79
|
+
@metrics[:duration_ms] = duration_ms
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def measurement(key)
|
|
83
|
+
base = measurement_base(key)
|
|
84
|
+
Measurement.new(:"#{base}_count", :"#{base}_duration_ms")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def record_measurement(measurement, duration_ms)
|
|
88
|
+
increment(measurement.count_key)
|
|
89
|
+
increment_metric(measurement.duration_key, by: duration_ms)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def record_input(**fields)
|
|
93
|
+
Fields::FieldSet.deep_dup(owned_record_input(**fields))
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def owned_record_input(**fields)
|
|
97
|
+
@record_input || build_record_input(**fields)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def finalize_record_input(**fields)
|
|
101
|
+
@record_input = build_record_input(**fields)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def merge_fields!(target, fields, owned:)
|
|
107
|
+
if owned
|
|
108
|
+
Fields::Internal.merge_owned!(target, fields)
|
|
109
|
+
else
|
|
110
|
+
Fields::FieldSet.merge!(target, fields)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def deep_merge_fields!(target, fields, owned:)
|
|
115
|
+
if owned
|
|
116
|
+
Fields::Internal.deep_merge_owned!(target, fields)
|
|
117
|
+
else
|
|
118
|
+
Fields::Internal.deep_merge!(target, fields)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def build_record_input(timestamp:, execution:, context:, carry:, neutral:, attributes:, labels:)
|
|
123
|
+
{
|
|
124
|
+
timestamp: timestamp,
|
|
125
|
+
kind: :summary,
|
|
126
|
+
event: @event,
|
|
127
|
+
source: @source,
|
|
128
|
+
execution: execution,
|
|
129
|
+
context: context,
|
|
130
|
+
carry: carry,
|
|
131
|
+
neutral: neutral_hash(neutral),
|
|
132
|
+
attributes: attributes_hash(attributes),
|
|
133
|
+
labels: labels,
|
|
134
|
+
metrics: metrics_hash,
|
|
135
|
+
payload: payload_hash,
|
|
136
|
+
error: @errors.last
|
|
137
|
+
}.tap do |record|
|
|
138
|
+
severity = summary_severity
|
|
139
|
+
record[:severity] = severity if severity
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def attributes_hash(base_attributes)
|
|
144
|
+
return base_attributes if @attributes.empty?
|
|
145
|
+
|
|
146
|
+
Fields::Internal.deep_merge(base_attributes, @attributes)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def neutral_hash(base_neutral)
|
|
150
|
+
return base_neutral if @neutral.empty?
|
|
151
|
+
|
|
152
|
+
Fields::Internal.deep_merge(base_neutral, @neutral)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def array_value(value, existing:)
|
|
156
|
+
return [] unless existing
|
|
157
|
+
return value if value.is_a?(Array)
|
|
158
|
+
|
|
159
|
+
[value]
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def incremented_value(value, by, existing:)
|
|
163
|
+
return Fields::FieldSet.deep_dup(by) unless existing
|
|
164
|
+
return value + by if value.is_a?(Numeric) && by.is_a?(Numeric)
|
|
165
|
+
|
|
166
|
+
array_value(value, existing: true).tap { it << Fields::FieldSet.deep_dup(by) }
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def increment_metric(key, by:)
|
|
170
|
+
value = Fields::FieldSet.value_for(@metrics, key, default: MISSING)
|
|
171
|
+
existing = !value.equal?(MISSING)
|
|
172
|
+
@metrics[key] = incremented_value(value, by, existing: existing)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def measurement_base(key)
|
|
176
|
+
unless key.is_a?(String) || key.is_a?(Symbol)
|
|
177
|
+
raise ArgumentError, "measurement key must be a String or Symbol"
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
base = key.to_s
|
|
181
|
+
raise ArgumentError, "measurement key is required" if base.empty?
|
|
182
|
+
|
|
183
|
+
base
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def attribute_container(path)
|
|
187
|
+
path[0...-1].reduce(@attributes) do |container, key|
|
|
188
|
+
normalized = Fields::Internal.normalize_key(key)
|
|
189
|
+
child = container[normalized]
|
|
190
|
+
unless child.is_a?(Hash)
|
|
191
|
+
child = {}
|
|
192
|
+
container[normalized] = child
|
|
193
|
+
end
|
|
194
|
+
child
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def summary_severity
|
|
199
|
+
return @severity if @errors.empty?
|
|
200
|
+
|
|
201
|
+
@error_severity || :error
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|