julewire-gcp 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7770e24c27f9957004b7e38a1fc5b49161a07f65173fe687bea6a8102f3796d5
4
+ data.tar.gz: 37376ea4aa3f56d0488a15a36d5d2a9e82a4244c5accd18fca224c50a9e070f3
5
+ SHA512:
6
+ metadata.gz: 853abdb1eee58dfb2d61178f50ec572df0504131d2af476abd7fd9a4b8b141249dbe82b585b0880a8c17d882b110fdd0be0e3fecce74cfe2b2441afa02f6c79c
7
+ data.tar.gz: 97b6621b589e538254db6455522270860d1b643a6f7f730f10f9b448d0d534b0abf4d04129bb028707aef90c739978180a052e01b996d84d203eccdd4dd86d33
data/CHANGELOG.md ADDED
@@ -0,0 +1,6 @@
1
+ ## Unreleased
2
+
3
+ ## 1.0.0 - 2026-06-21
4
+
5
+ - Initial release: Google Cloud Logging formatter, trace fields, source
6
+ locations, Error Reporting shape, and CLI transcode support.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Alexander Grebennik
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # Julewire GCP
2
+
3
+ Google Cloud Logging structured JSON formatter and direct-output destination
4
+ for Julewire records.
5
+
6
+ It does not own queues, batching, retries, or network transport.
7
+
8
+ ## Quickstart
9
+
10
+ ```ruby
11
+ gem "julewire-gcp"
12
+ ```
13
+
14
+ ```ruby
15
+ Julewire.configure do |config|
16
+ config.destinations.use(
17
+ :gcp,
18
+ formatter: Julewire::GCP::Formatter.new(project_id: "my-project"),
19
+ output: $stdout
20
+ )
21
+ end
22
+ ```
23
+
24
+ Default shape:
25
+
26
+ - Cloud Logging special fields: `severity`, `message`, `time`, `httpRequest`,
27
+ labels, operation, source location, trace, span, and trace sampled
28
+ - remaining Julewire fields in JSON payload
29
+ - records with neutral HTTP attributes mapped to `httpRequest`
30
+ - stack traces promoted for Error Reporting when core-shaped errors include
31
+ backtrace lines
32
+
33
+ ## Docs
34
+
35
+ - [Configuration](docs/configuration.md)
36
+ - [Advanced Configuration](docs/advanced-configuration.md)
37
+ - [Shape](docs/shape.md)
38
+ - [Trace](docs/trace.md)
39
+ - [Error Reporting](docs/error-reporting.md)
40
+ - [Development](docs/development.md)
@@ -0,0 +1,25 @@
1
+ # Advanced Configuration
2
+
3
+ | Option | Default | Purpose |
4
+ | --- | --- | --- |
5
+ | `trace_headers_paths` | `carry.http.request_headers`, then secondary paths | Request trace-header paths. |
6
+ | `trace_id_path` | `nil` | Explicit path to trace id. |
7
+ | `span_id_path` | `nil` | Explicit path to span id. |
8
+ | `trace_sampled_path` | `nil` | Explicit path to sampling flag. |
9
+ | `label_formatter` | `nil` | Custom label formatter. |
10
+ | `label_options` | `{}` | Options passed to the default label formatter. |
11
+
12
+ Use explicit trace paths when trace facts are already normalized into record
13
+ fields and no header parsing is needed:
14
+
15
+ ```ruby
16
+ Julewire::GCP::Formatter.new(
17
+ project_id: "my-project",
18
+ trace_id_path: %i[attributes trace_id],
19
+ span_id_path: %i[attributes span_id],
20
+ trace_sampled_path: %i[attributes trace_sampled]
21
+ )
22
+ ```
23
+
24
+ Use `label_formatter` only when the default label limits or key formatting are
25
+ not enough.
@@ -0,0 +1,33 @@
1
+ # Configuration
2
+
3
+ ## Default Path
4
+
5
+ | Option | Default | Purpose |
6
+ | --- | --- | --- |
7
+ | `project_id` | `nil` | Expand bare trace IDs to Cloud Trace resource names. |
8
+ | `service_context` | `nil` | Add Error Reporting service/version context. |
9
+ | `max_record_bytes` | `256 KiB` | Destination record-size cap from `Julewire::GCP::Destination`. |
10
+
11
+ ## Common Knobs
12
+
13
+ | Option | Default | Purpose |
14
+ | --- | --- | --- |
15
+ | `operation_producer` | `nil` | Override `logging.googleapis.com/operation.producer`. |
16
+ | `max_labels` | `64` | Maximum Cloud Logging labels emitted. |
17
+ | `max_label_key_bytes` | `512` | Maximum label key byte size. |
18
+ | `max_label_value_bytes` | `65_536` | Maximum label value byte size. |
19
+
20
+ Oversized label values are truncated. Oversized label keys are dropped because
21
+ Cloud Logging rejects invalid label keys.
22
+
23
+ Example:
24
+
25
+ ```ruby
26
+ Julewire::GCP::Formatter.new(
27
+ project_id: "my-project",
28
+ service_context: { service: "web", version: "2026-05-31" }
29
+ )
30
+ ```
31
+
32
+ `Julewire::GCP::Destination` uses `Julewire::GCP::RECOMMENDED_MAX_RECORD_BYTES`
33
+ as a conservative `max_record_bytes` default.
@@ -0,0 +1,7 @@
1
+ # Development
2
+
3
+ Run the normal gem checks:
4
+
5
+ ```sh
6
+ bundle exec rake
7
+ ```
@@ -0,0 +1,31 @@
1
+ # Error Reporting
2
+
3
+ When a record contains an `error` hash with backtrace lines, the formatter
4
+ promotes those lines to top-level JSON payload field `stack_trace` for Cloud
5
+ Logging/Error Reporting ingestion.
6
+
7
+ The stack trace starts with the exception summary, includes nested causes, and
8
+ respects core's `error_backtrace_lines` setting. If
9
+ `error_backtrace_lines = 0`, core-shaped errors have no backtrace and the
10
+ formatter emits no `stack_trace`.
11
+
12
+ When `stack_trace` is promoted, nested `julewire.error.backtrace` fields are
13
+ removed to avoid carrying the same stack twice in one log entry. Exception
14
+ class, message, and cause metadata remain in `julewire.error`.
15
+
16
+ If the record has no message and no HTTP-derived message can be derived, the
17
+ top-level `message` uses the exception summary, such as
18
+ `RuntimeError: boom`.
19
+
20
+ When no explicit `payload.gcp.source_location` is present, the formatter first
21
+ uses neutral `code.*` fields from the record's `neutral` section, then infers
22
+ `logging.googleapis.com/sourceLocation` from the first error backtrace frame.
23
+
24
+ References:
25
+
26
+ - Google Cloud Logging structured logging:
27
+ https://docs.cloud.google.com/logging/docs/structured-logging
28
+ - Google Cloud Logging `LogEntry`:
29
+ https://docs.cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry
30
+ - Google Cloud Error Reporting log formatting:
31
+ https://docs.cloud.google.com/error-reporting/docs/formatting-error-messages
data/docs/shape.md ADDED
@@ -0,0 +1,104 @@
1
+ # Shape
2
+
3
+ The formatter maps Julewire records to the special JSON fields recognized by
4
+ Cloud Logging agents:
5
+
6
+ - `severity`
7
+ - `message`
8
+ - `time`
9
+ - `httpRequest`
10
+ - `logging.googleapis.com/labels`
11
+ - `logging.googleapis.com/operation`
12
+ - `logging.googleapis.com/sourceLocation`
13
+ - `logging.googleapis.com/trace`
14
+ - `logging.googleapis.com/spanId`
15
+ - `logging.googleapis.com/trace_sampled`
16
+
17
+ The remaining non-empty Julewire data stays queryable in the JSON payload:
18
+
19
+ - `payload`
20
+ - `attributes`
21
+ - `serviceContext` when configured
22
+ - `julewire.kind`
23
+ - `julewire.event`
24
+ - `julewire.logger`
25
+ - `julewire.source`
26
+ - `julewire.execution`
27
+ - `julewire.context`
28
+ - `julewire.error`
29
+ - `julewire.metrics`
30
+
31
+ Records are mapped into `httpRequest` when they include provider-neutral HTTP
32
+ fields in the record's `neutral` section:
33
+
34
+ - `http.request.method`
35
+ - `url.full`
36
+ - `url.path`
37
+ - `http.response.status_code`
38
+ - `user_agent.original`
39
+ - `client.address`
40
+ - `http.response.body.size`
41
+
42
+ `httpRequest.latency` comes from `metrics.duration_ms` when present.
43
+
44
+ The formatter reads these core neutral HTTP attributes to build `httpRequest`.
45
+ Integration-specific attribute namespaces stay queryable.
46
+
47
+ Records are mapped into `logging.googleapis.com/sourceLocation` when they carry
48
+ neutral `code.file.path`, `code.line.number`, or `code.function.name` attributes
49
+ in the record's `neutral` section. Explicit `payload.gcp.source_location` wins.
50
+
51
+ When a record has HTTP method, URL or path, and status but no explicit message,
52
+ the formatter emits a concise HTTP-derived message such as
53
+ `GET /orders -> 200 in 24.1ms`.
54
+
55
+ Request summaries are marked as `logging.googleapis.com/operation.last = true`.
56
+ The formatter does not infer `operation.first`; mark that specific record
57
+ explicitly when it matters:
58
+
59
+ ```ruby
60
+ Julewire.with_execution(type: :job, id: "job-1") do
61
+ Julewire.emit(
62
+ event: "job.started",
63
+ source: "worker",
64
+ payload: Julewire::GCP.operation(first: true)
65
+ )
66
+ end
67
+ ```
68
+
69
+ GCP output keeps public execution fields under `julewire.execution`.
70
+ Lineage internals are omitted. Execution fields promoted to GCP-native fields,
71
+ such as `logging.googleapis.com/operation.id` or configured trace paths, are
72
+ not duplicated there.
73
+
74
+ For records without an execution, provide an operation id:
75
+
76
+ ```ruby
77
+ Julewire.emit(
78
+ event: "script.started",
79
+ source: "script",
80
+ payload: Julewire::GCP.operation(
81
+ id: "nightly-import-20260531",
82
+ producer: "script",
83
+ first: true
84
+ )
85
+ )
86
+ ```
87
+
88
+ The formatter consumes `payload.gcp.operation` and
89
+ `payload.gcp.source_location` as control metadata and removes those control keys
90
+ from emitted application payload.
91
+
92
+ Add source-location metadata when the application has a meaningful code location
93
+ to attach to a record:
94
+
95
+ ```ruby
96
+ Julewire.emit(
97
+ event: "import.started",
98
+ payload: Julewire::GCP.source_location(
99
+ file: "app/jobs/import_job.rb",
100
+ line: 42,
101
+ function: "ImportJob#perform"
102
+ )
103
+ )
104
+ ```
data/docs/trace.md ADDED
@@ -0,0 +1,49 @@
1
+ # Trace
2
+
3
+ The formatter exposes the trace header names it needs as:
4
+
5
+ ```ruby
6
+ Julewire::GCP::CARRY_REQUEST_HEADERS
7
+ ```
8
+
9
+ Put selected headers into Julewire carry before emitting records:
10
+
11
+ ```ruby
12
+ headers = {
13
+ "traceparent" => "00-...",
14
+ "tracestate" => "...",
15
+ "x-cloud-trace-context" => "..."
16
+ }
17
+
18
+ Julewire.with_execution(type: :request, id: "request-1") do
19
+ Julewire.carry.add(http: { request_headers: headers })
20
+ Julewire.emit(message: "handled")
21
+ end
22
+ ```
23
+
24
+ By default the formatter looks for request headers at
25
+ `carry.http.request_headers`, then checks `payload.request_headers` and
26
+ `context.request_headers`. It understands W3C `traceparent` first, then Google
27
+ `x-cloud-trace-context`.
28
+
29
+ `tracestate` is carried with `traceparent`, but Cloud Logging special fields do
30
+ not use it directly. If `project_id` is configured, a bare trace ID is expanded
31
+ to:
32
+
33
+ ```text
34
+ projects/[PROJECT-ID]/traces/[TRACE-ID]
35
+ ```
36
+
37
+ Already-expanded trace resource names are left unchanged.
38
+
39
+ You can point trace extraction at different record paths when another
40
+ integration or processor stores trace facts elsewhere:
41
+
42
+ ```ruby
43
+ Julewire::GCP::Formatter.new(
44
+ project_id: "my-project",
45
+ trace_id_path: %i[context trace_id],
46
+ span_id_path: %i[context span_id],
47
+ trace_sampled_path: %i[context trace_sampled]
48
+ )
49
+ ```
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/julewire/gcp/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "julewire-gcp"
7
+ spec.version = Julewire::GCP::VERSION
8
+ spec.authors = ["Alexander Grebennik"]
9
+ spec.email = ["slbug@users.noreply.github.com", "sl.bug.sl@gmail.com"]
10
+
11
+ spec.summary = "Google Cloud Logging formatter for Julewire."
12
+ spec.description = "Google Cloud Logging structured JSON formatter for Julewire records."
13
+ spec.homepage = "https://github.com/slbug/julewire"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 3.4"
16
+ spec.metadata["homepage_uri"] = spec.homepage
17
+ spec.metadata["source_code_uri"] = "https://github.com/slbug/julewire/tree/main/gems/gcp"
18
+ spec.metadata["changelog_uri"] = "https://github.com/slbug/julewire/blob/main/gems/gcp/CHANGELOG.md"
19
+
20
+ spec.metadata["rubygems_mfa_required"] = "true"
21
+
22
+ spec.files = Dir.chdir(__dir__) do
23
+ Dir[
24
+ "CHANGELOG.md",
25
+ "LICENSE.txt",
26
+ "README.md",
27
+ "docs/**/*.md",
28
+ "julewire-gcp.gemspec",
29
+ "lib/**/*.rb"
30
+ ]
31
+ end
32
+ spec.executables = []
33
+ spec.require_paths = ["lib"]
34
+
35
+ spec.add_dependency "julewire-core", ">= 1.0"
36
+ spec.add_dependency "zeitwerk", ">= 2.8.1"
37
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module GCP
5
+ class Destination < Julewire::Core::Destinations::Destination
6
+ def initialize(output:, name: :gcp, formatter: nil, encoder: Julewire::JsonEncoder.new,
7
+ max_record_bytes: DEFAULT_MAX_RECORD_BYTES, close_output: false, on_drop: nil,
8
+ on_failure: nil)
9
+ super(
10
+ name: name,
11
+ close_output: close_output,
12
+ encoder: encoder,
13
+ formatter: formatter || Formatter.new,
14
+ max_record_bytes: max_record_bytes,
15
+ on_drop: on_drop,
16
+ on_failure: on_failure,
17
+ output: output
18
+ )
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module GCP
5
+ class ExecutionPayload
6
+ def initialize(trace_id_path:, span_id_path:, trace_sampled_path:)
7
+ @trace_keys = trace_execution_keys(trace_id_path, span_id_path, trace_sampled_path)
8
+ end
9
+
10
+ def call(record, operation_options:)
11
+ execution = record.fetch(:execution)
12
+ output = nil
13
+ execution.each do |key, value|
14
+ next if promoted_key?(key, value, operation_options)
15
+
16
+ (output ||= {})[key] = value
17
+ end
18
+ output
19
+ end
20
+
21
+ private
22
+
23
+ def promoted_key?(key, value, operation_options)
24
+ return true if key == :id && !operation_options[:id]
25
+
26
+ @trace_keys.key?(key) && !Core::Integration::Values::Read.blank?(value)
27
+ end
28
+
29
+ def trace_execution_keys(*paths)
30
+ paths.map { trace_execution_key(it) }.to_h { [it, true] }
31
+ end
32
+
33
+ def trace_execution_key(path)
34
+ return unless path&.length == 2
35
+ return unless path.fetch(0) == :execution
36
+
37
+ path.fetch(1)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,238 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module GCP
5
+ class Formatter
6
+ SEVERITIES = {
7
+ debug: "DEBUG",
8
+ info: "INFO",
9
+ warn: "WARNING",
10
+ error: "ERROR",
11
+ fatal: "CRITICAL",
12
+ unknown: "DEFAULT"
13
+ }.freeze
14
+ EMPTY_HASH = {}.freeze
15
+ TRUE_VALUES = %w[true 1 yes].freeze
16
+ private_constant :EMPTY_HASH, :TRUE_VALUES
17
+
18
+ def initialize(project_id: nil,
19
+ operation_producer: nil,
20
+ service_context: nil,
21
+ trace_headers_paths: [
22
+ %i[carry http request_headers],
23
+ %i[payload request_headers],
24
+ %i[context request_headers]
25
+ ],
26
+ **options)
27
+ FormatterOptions.validate!(options)
28
+ @project_id = project_id
29
+ @operation_producer = operation_producer
30
+ @service_context = frozen_service_context(service_context)
31
+ @trace_headers_paths = FormatterOptions.trace_headers_paths(trace_headers_paths)
32
+ @label_formatter = FormatterOptions.label_formatter(options)
33
+ @trace_id_path = FormatterOptions.trace_value_path(options[:trace_id_path])
34
+ @span_id_path = FormatterOptions.trace_value_path(options[:span_id_path])
35
+ @trace_sampled_path = FormatterOptions.trace_value_path(options[:trace_sampled_path])
36
+ @execution_payload = ExecutionPayload.new(
37
+ trace_id_path: @trace_id_path,
38
+ span_id_path: @span_id_path,
39
+ trace_sampled_path: @trace_sampled_path
40
+ )
41
+ end
42
+
43
+ def call(record)
44
+ Julewire::Record.validate_normalized!(record)
45
+
46
+ error = record.fetch(:error)
47
+ operation_options = operation_options(record)
48
+ neutral_attributes = Core::Fields::AttributeKeys.from(record.fetch(:neutral))
49
+ source_location_options = SourceLocationOptions.call(record, neutral_attributes)
50
+ entry = {
51
+ "severity" => severity(record.fetch(:severity)),
52
+ "time" => record.fetch(:timestamp)
53
+ }
54
+ append_log_field(entry, "message", Core::Records::DisplayMessage.call(record))
55
+ entry.tap do |log_entry|
56
+ append_special_fields(
57
+ log_entry,
58
+ record,
59
+ error: error,
60
+ operation_options: operation_options,
61
+ source_location_options: source_location_options,
62
+ neutral_attributes: neutral_attributes
63
+ )
64
+ append_payload_fields(log_entry, record, error: error, operation_options: operation_options)
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def frozen_service_context(value) = value ? Core::Fields::FieldSet.frozen_copy(value) : nil
71
+
72
+ def append_special_fields(entry, record, error:, operation_options:, source_location_options:,
73
+ neutral_attributes:)
74
+ trace_context = trace_context(record)
75
+ append_log_field(entry, "httpRequest", HttpRequestFields.http_request(record, neutral_attributes))
76
+ append_log_field(entry, "logging.googleapis.com/labels", labels(record))
77
+ append_log_field(entry, "logging.googleapis.com/operation", operation(record, operation_options))
78
+ append_log_field(
79
+ entry,
80
+ "logging.googleapis.com/sourceLocation",
81
+ source_location(error, source_location_options)
82
+ )
83
+ append_log_field(entry, "logging.googleapis.com/trace", trace(record, trace_context))
84
+ append_log_field(
85
+ entry,
86
+ "logging.googleapis.com/spanId",
87
+ trace_value(record, trace_context, @span_id_path, :span_id)
88
+ )
89
+ append_log_field(
90
+ entry,
91
+ "logging.googleapis.com/trace_sampled",
92
+ trace_value(record, trace_context, @trace_sampled_path, :trace_sampled)
93
+ )
94
+ end
95
+
96
+ def append_payload_fields(entry, record, error:, operation_options:)
97
+ stack_trace = stack_trace(error)
98
+ append_log_field(entry, JULEWIRE_PAYLOAD_FIELD, julewire_payload(record,
99
+ error: error,
100
+ operation_options: operation_options,
101
+ stack_trace: stack_trace))
102
+ append_log_field(entry, "attributes", attributes_payload(record))
103
+ append_log_field(entry, "payload", application_payload(record))
104
+ append_log_field(entry, "stack_trace", stack_trace)
105
+ append_log_field(entry, "serviceContext", @service_context)
106
+ end
107
+
108
+ def append_log_field(entry, key, value)
109
+ return if value.nil?
110
+ return if (value.is_a?(Hash) || value.is_a?(Array)) && value.empty?
111
+
112
+ entry[key] = value
113
+ nil
114
+ end
115
+
116
+ def severity(value) = SEVERITIES.fetch(value)
117
+
118
+ def labels(record)
119
+ @label_formatter.call(record.fetch(:labels))
120
+ end
121
+
122
+ def operation(record, options)
123
+ execution = record.fetch(:execution)
124
+ id = options[:id] || execution[:id] || record.lineage.root_reference&.fetch(:id, nil)
125
+ return unless id
126
+
127
+ operation = {
128
+ "id" => id.to_s
129
+ }
130
+ append_log_field(operation, "producer", operation_producer(record, options))
131
+ operation["first"] = true if true_value?(options[:first])
132
+ operation["last"] = true if record.fetch(:kind) == :summary || true_value?(options[:last])
133
+ operation
134
+ end
135
+
136
+ def operation_options(record)
137
+ options = record.dig(:payload, :gcp, :operation)
138
+ options.is_a?(Hash) ? options : EMPTY_HASH
139
+ end
140
+
141
+ def operation_producer(record, options)
142
+ options[:producer] || @operation_producer || record[:source] || record[:logger]
143
+ end
144
+
145
+ def source_location(error, options)
146
+ return SourceLocation.call(options) unless options.empty?
147
+ return unless error.is_a?(Hash) && !error.empty?
148
+
149
+ SourceLocation.from_error(error)
150
+ end
151
+
152
+ def trace(record, trace_context)
153
+ value = trace_value(record, trace_context, @trace_id_path, :trace_id)
154
+ return unless value
155
+
156
+ trace = value.to_s
157
+ return trace if trace.start_with?("projects/") || @project_id.nil?
158
+
159
+ "projects/#{@project_id}/traces/#{trace}"
160
+ end
161
+
162
+ def trace_context(record)
163
+ @trace_headers_paths.each do |path|
164
+ context = TraceContext.extract(Core::Integration::Values::Read.path_value(record, path))
165
+ return context unless context.empty?
166
+ end
167
+ EMPTY_HASH
168
+ end
169
+
170
+ def trace_value(record, trace_context, path, key)
171
+ value = Core::Integration::Values::Read.path_value(record, path) if path
172
+ Core::Integration::Values::Read.blank?(value) ? trace_context[key] : value
173
+ end
174
+
175
+ def application_payload(record)
176
+ payload = record.fetch(:payload)
177
+ control = payload[:gcp]
178
+ remove_gcp_control_payload(payload, control)
179
+ end
180
+
181
+ def attributes_payload(record)
182
+ record.fetch(:attributes)
183
+ end
184
+
185
+ def remove_gcp_control_payload(payload, control)
186
+ return payload unless control.is_a?(Hash) && gcp_control_payload?(control)
187
+
188
+ cleaned_payload = payload.dup
189
+ cleaned_control = control.dup
190
+ cleaned_control.delete(:operation)
191
+ cleaned_control.delete(:source_location)
192
+ if cleaned_control.empty?
193
+ cleaned_payload.delete(:gcp)
194
+ else
195
+ cleaned_payload[:gcp] = cleaned_control
196
+ end
197
+ cleaned_payload
198
+ end
199
+
200
+ def gcp_control_payload?(control)
201
+ control.key?(:operation) || control.key?(:source_location)
202
+ end
203
+
204
+ def julewire_payload(record, error:, operation_options:, stack_trace: nil)
205
+ {}.tap do |payload|
206
+ append_log_field(payload, :kind, record.fetch(:kind))
207
+ append_log_field(payload, :event, record.fetch(:event))
208
+ append_log_field(payload, :logger, record.fetch(:logger))
209
+ append_log_field(payload, :source, record.fetch(:source))
210
+ append_log_field(
211
+ payload,
212
+ :execution,
213
+ @execution_payload.call(record, operation_options: operation_options)
214
+ )
215
+ append_log_field(payload, :context, record.fetch(:context))
216
+ append_log_field(payload, :error, julewire_error(error, stack_trace: stack_trace))
217
+ append_log_field(payload, :metrics, record[:metrics])
218
+ end
219
+ end
220
+
221
+ def julewire_error(error, stack_trace:)
222
+ return error unless error.is_a?(Hash) && stack_trace
223
+
224
+ StackTrace.remove_backtraces(error)
225
+ end
226
+
227
+ def stack_trace(error)
228
+ return unless error.is_a?(Hash) && !error.empty?
229
+
230
+ StackTrace.call(error)
231
+ end
232
+
233
+ def true_value?(value)
234
+ TRUE_VALUES.include?(value.to_s.downcase)
235
+ end
236
+ end
237
+ end
238
+ end