julewire-gcp 1.0.0 → 1.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7770e24c27f9957004b7e38a1fc5b49161a07f65173fe687bea6a8102f3796d5
4
- data.tar.gz: 37376ea4aa3f56d0488a15a36d5d2a9e82a4244c5accd18fca224c50a9e070f3
3
+ metadata.gz: 0bb2a1472182d07e7099ea1d254a8f56d6fcad94527eb5c9959974191d3b4bc5
4
+ data.tar.gz: 6a2ce085d54af374465210210496dc5bcf14692710f44c09717d500b9ad8dc5e
5
5
  SHA512:
6
- metadata.gz: 853abdb1eee58dfb2d61178f50ec572df0504131d2af476abd7fd9a4b8b141249dbe82b585b0880a8c17d882b110fdd0be0e3fecce74cfe2b2441afa02f6c79c
7
- data.tar.gz: 97b6621b589e538254db6455522270860d1b643a6f7f730f10f9b448d0d534b0abf4d04129bb028707aef90c739978180a052e01b996d84d203eccdd4dd86d33
6
+ metadata.gz: 383c1c579384c7ffeeb2b95c3b6ad7ec12f75ab3625955e7e11c62c6fbbdea907e8e53a8405b206a1f3e1be92f4d9fe5723e03241b4ba5f39151947645fa8735
7
+ data.tar.gz: 832fdcf7596afe7de1113ca3bc7599414a8ff87c76f969b0ca09452bcd06f088a11952dd3632ec0c5ad0af9d8ee91d8b680927eb018316e99ddf5e2995d92f6f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  ## Unreleased
2
2
 
3
+ ## 1.1.0 - 2026-07-20
4
+
5
+ - Keep Cloud Logging formatting and decoding aligned with the core record field
6
+ taxonomy and deterministic trace/source parsing.
7
+ - Require julewire-core 1.1.0.
8
+
9
+ ## 1.0.1 - 2026-06-25
10
+
11
+ - Replace the fallback backtrace regex with deterministic parsing so GCP source
12
+ location inference stays boring under CodeQL.
13
+ - Require julewire-core 1.0.1.
14
+
3
15
  ## 1.0.0 - 2026-06-21
4
16
 
5
17
  - Initial release: Google Cloud Logging formatter, trace fields, source
data/julewire-gcp.gemspec CHANGED
@@ -32,6 +32,6 @@ Gem::Specification.new do |spec|
32
32
  spec.executables = []
33
33
  spec.require_paths = ["lib"]
34
34
 
35
- spec.add_dependency "julewire-core", ">= 1.0"
35
+ spec.add_dependency "julewire-core", ">= 1.1.0"
36
36
  spec.add_dependency "zeitwerk", ">= 2.8.1"
37
37
  end
@@ -3,7 +3,7 @@
3
3
  module Julewire
4
4
  module GCP
5
5
  class Destination < Julewire::Core::Destinations::Destination
6
- def initialize(output:, name: :gcp, formatter: nil, encoder: Julewire::JsonEncoder.new,
6
+ def initialize(output:, name: :gcp, formatter: nil, encoder: JsonEncoder.new,
7
7
  max_record_bytes: DEFAULT_MAX_RECORD_BYTES, close_output: false, on_drop: nil,
8
8
  on_failure: nil)
9
9
  super(
@@ -41,11 +41,11 @@ module Julewire
41
41
  end
42
42
 
43
43
  def call(record)
44
- Julewire::Record.validate_normalized!(record)
44
+ Record.validate_normalized!(record)
45
45
 
46
46
  error = record.fetch(:error)
47
47
  operation_options = operation_options(record)
48
- neutral_attributes = Core::Fields::AttributeKeys.from(record.fetch(:neutral))
48
+ neutral_attributes = record.fetch(:neutral)
49
49
  source_location_options = SourceLocationOptions.call(record, neutral_attributes)
50
50
  entry = {
51
51
  "severity" => severity(record.fetch(:severity)),
@@ -67,7 +67,7 @@ module Julewire
67
67
 
68
68
  private
69
69
 
70
- def frozen_service_context(value) = value ? Core::Fields::FieldSet.frozen_copy(value) : nil
70
+ def frozen_service_context(value) = Core::Fields::FieldSet.frozen_copy(value)
71
71
 
72
72
  def append_special_fields(entry, record, error:, operation_options:, source_location_options:,
73
73
  neutral_attributes:)
@@ -110,7 +110,6 @@ module Julewire
110
110
  return if (value.is_a?(Hash) || value.is_a?(Array)) && value.empty?
111
111
 
112
112
  entry[key] = value
113
- nil
114
113
  end
115
114
 
116
115
  def severity(value) = SEVERITIES.fetch(value)
@@ -134,17 +133,19 @@ module Julewire
134
133
  end
135
134
 
136
135
  def operation_options(record)
137
- options = record.dig(:payload, :gcp, :operation)
136
+ control = record.fetch(:payload)[:gcp]
137
+ return EMPTY_HASH unless control.is_a?(Hash)
138
+
139
+ options = control[:operation]
138
140
  options.is_a?(Hash) ? options : EMPTY_HASH
139
141
  end
140
142
 
141
143
  def operation_producer(record, options)
142
- options[:producer] || @operation_producer || record[:source] || record[:logger]
144
+ options[:producer] || @operation_producer || record.fetch(:source) || record.fetch(:logger)
143
145
  end
144
146
 
145
147
  def source_location(error, options)
146
148
  return SourceLocation.call(options) unless options.empty?
147
- return unless error.is_a?(Hash) && !error.empty?
148
149
 
149
150
  SourceLocation.from_error(error)
150
151
  end
@@ -183,7 +184,7 @@ module Julewire
183
184
  end
184
185
 
185
186
  def remove_gcp_control_payload(payload, control)
186
- return payload unless control.is_a?(Hash) && gcp_control_payload?(control)
187
+ return payload unless control.is_a?(Hash)
187
188
 
188
189
  cleaned_payload = payload.dup
189
190
  cleaned_control = control.dup
@@ -197,11 +198,7 @@ module Julewire
197
198
  cleaned_payload
198
199
  end
199
200
 
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)
201
+ def julewire_payload(record, error:, operation_options:, stack_trace:)
205
202
  {}.tap do |payload|
206
203
  append_log_field(payload, :kind, record.fetch(:kind))
207
204
  append_log_field(payload, :event, record.fetch(:event))
@@ -214,19 +211,17 @@ module Julewire
214
211
  )
215
212
  append_log_field(payload, :context, record.fetch(:context))
216
213
  append_log_field(payload, :error, julewire_error(error, stack_trace: stack_trace))
217
- append_log_field(payload, :metrics, record[:metrics])
214
+ append_log_field(payload, :metrics, record.fetch(:metrics))
218
215
  end
219
216
  end
220
217
 
221
218
  def julewire_error(error, stack_trace:)
222
- return error unless error.is_a?(Hash) && stack_trace
219
+ return error unless stack_trace
223
220
 
224
221
  StackTrace.remove_backtraces(error)
225
222
  end
226
223
 
227
224
  def stack_trace(error)
228
- return unless error.is_a?(Hash) && !error.empty?
229
-
230
225
  StackTrace.call(error)
231
226
  end
232
227
 
@@ -20,7 +20,7 @@ module Julewire
20
20
  end
21
21
 
22
22
  def trace_headers_paths(paths)
23
- Array(paths).filter_map { normalize_path(it, min_length: 1) }.freeze
23
+ Array(paths).filter_map { normalize_path(it, min_length: 1) }
24
24
  end
25
25
 
26
26
  def trace_value_path(path)
@@ -34,7 +34,7 @@ module Julewire
34
34
  def label_options(options)
35
35
  label_options = options.fetch(:label_options, {}).dup
36
36
  %i[max_labels max_label_key_bytes max_label_value_bytes].each do |key|
37
- label_options[key] = options[key] if options.key?(key)
37
+ label_options[key] = options.fetch(key) if options.key?(key)
38
38
  end
39
39
  label_options
40
40
  end
@@ -42,12 +42,10 @@ module Julewire
42
42
  private
43
43
 
44
44
  def normalize_path(path, min_length:)
45
- return if path.nil?
46
-
47
45
  normalized = Array(path).map { normalize_path_segment(it) }
48
46
  return if normalized.length < min_length
49
47
 
50
- normalized.freeze
48
+ normalized
51
49
  end
52
50
 
53
51
  def normalize_path_segment(segment)
@@ -3,19 +3,17 @@
3
3
  module Julewire
4
4
  module GCP
5
5
  class LabelFormatter
6
- def initialize(max_labels: GCP::DEFAULT_MAX_LABELS,
7
- max_label_key_bytes: GCP::DEFAULT_MAX_LABEL_KEY_BYTES,
8
- max_label_value_bytes: GCP::DEFAULT_MAX_LABEL_VALUE_BYTES)
6
+ def initialize(max_labels: DEFAULT_MAX_LABELS,
7
+ max_label_key_bytes: DEFAULT_MAX_LABEL_KEY_BYTES,
8
+ max_label_value_bytes: DEFAULT_MAX_LABEL_VALUE_BYTES)
9
9
  @max_labels = validate_count_limit(max_labels, name: :max_labels)
10
10
  @max_label_key_bytes = validate_byte_limit(max_label_key_bytes, name: :max_label_key_bytes)
11
11
  @max_label_value_bytes = validate_byte_limit(max_label_value_bytes, name: :max_label_value_bytes)
12
12
  end
13
13
 
14
14
  def call(labels)
15
- return if labels.empty?
16
-
17
15
  labels.each_with_object({}) do |(key, value), result|
18
- break result if @max_labels && result.size >= @max_labels
16
+ break result if result.size == @max_labels
19
17
 
20
18
  label_key = label_key(key)
21
19
  next unless label_key
@@ -28,18 +26,20 @@ module Julewire
28
26
 
29
27
  def label_key(value)
30
28
  key = label_string(value)
31
- return key unless @max_label_key_bytes && key.bytesize > @max_label_key_bytes
32
-
33
- nil
29
+ key unless @max_label_key_bytes && key.bytesize > @max_label_key_bytes
34
30
  end
35
31
 
36
32
  def bounded_label_string(value, max_bytes)
37
33
  string = label_string(value)
38
- return string unless max_bytes && string.bytesize > max_bytes
34
+ return string unless label_too_long?(string, max_bytes)
39
35
 
40
36
  label_string(string.byteslice(0, max_bytes))
41
37
  end
42
38
 
39
+ def label_too_long?(string, max_bytes)
40
+ !max_bytes.nil? && string.bytesize > max_bytes
41
+ end
42
+
43
43
  def label_string(value)
44
44
  Core::Serialization::EncodingSanitizer.call(value.to_s)
45
45
  end
@@ -52,7 +52,6 @@ module Julewire
52
52
 
53
53
  def validate_byte_limit(value, name:)
54
54
  Core::Validation.validate_byte_limit!(value, name: name)
55
- value
56
55
  end
57
56
  end
58
57
  end
@@ -36,7 +36,7 @@ module Julewire
36
36
  def record_base(payload, julewire)
37
37
  {
38
38
  timestamp: payload["time"] || payload["timestamp"],
39
- severity: Julewire::Core::Records::Severity.normalize(payload["severity"] || :info),
39
+ severity: Core::Records::Severity.normalize(payload["severity"] || :info),
40
40
  kind: RecordDecoder.kind(julewire["kind"] || :point),
41
41
  event: julewire["event"],
42
42
  message: payload["message"],
@@ -46,7 +46,7 @@ module Julewire
46
46
  end
47
47
 
48
48
  def record_sections(payload, julewire)
49
- RecordDecoder.sections(payload) do |section, _source|
49
+ RecordDecoder.sections(nil) do |section, _source|
50
50
  gcp_section_value(section, payload, julewire)
51
51
  end
52
52
  end
@@ -3,8 +3,9 @@
3
3
  module Julewire
4
4
  module GCP
5
5
  module SourceLocation
6
- BACKTRACE_PATTERN = /\A(?<file>.+?):(?<line>\d+)(?::in (?:[`'](?<quoted>.*)[`']|(?<plain>.*)))?\z/
7
- private_constant :BACKTRACE_PATTERN
6
+ FUNCTION_SEPARATOR = ":in "
7
+ QUOTE_BYTES = [39, 96].freeze
8
+ private_constant :FUNCTION_SEPARATOR, :QUOTE_BYTES
8
9
 
9
10
  class << self
10
11
  def call(options)
@@ -27,13 +28,22 @@ module Julewire
27
28
  end
28
29
 
29
30
  def from_backtrace_line(line)
30
- match = BACKTRACE_PATTERN.match(line.to_s)
31
- return unless match
31
+ line = line.to_s
32
+ index = line.rindex(FUNCTION_SEPARATOR)
33
+ if index
34
+ location = line[0, index]
35
+ function = line[(index + FUNCTION_SEPARATOR.length)..]
36
+ else
37
+ location = line
38
+ function = nil
39
+ end
40
+ file, line_number = split_file_and_line(location)
41
+ return unless line_number
32
42
 
33
43
  call(
34
- file: match[:file],
35
- line: match[:line],
36
- function: match[:quoted] || match[:plain]
44
+ file: file,
45
+ line: line_number,
46
+ function: normalize_function(function)
37
47
  )
38
48
  end
39
49
 
@@ -47,6 +57,31 @@ module Julewire
47
57
  string = value.to_s
48
58
  string if string.match?(/\A\d+\z/)
49
59
  end
60
+
61
+ def split_file_and_line(location)
62
+ index = location.rindex(":")
63
+ return unless index
64
+
65
+ file = location[0, index]
66
+ line = location[(index + 1)..]
67
+ return if file.empty? || !line.match?(/\A\d+\z/)
68
+
69
+ [file, line]
70
+ end
71
+
72
+ def normalize_function(function)
73
+ return unless function
74
+
75
+ if function.length >= 2 && quoted?(function.getbyte(0)) && quoted?(function.getbyte(-1))
76
+ function[1...-1]
77
+ else
78
+ function
79
+ end
80
+ end
81
+
82
+ def quoted?(byte)
83
+ QUOTE_BYTES.include?(byte)
84
+ end
50
85
  end
51
86
  end
52
87
  end
@@ -3,12 +3,12 @@
3
3
  module Julewire
4
4
  module GCP
5
5
  module SourceLocationOptions
6
- EMPTY_HASH = {}.freeze
7
- private_constant :EMPTY_HASH
8
-
9
6
  class << self
10
7
  def call(record, neutral_attributes)
11
- options = record.dig(:payload, :gcp, :source_location)
8
+ control = record.fetch(:payload)[:gcp]
9
+ return from_neutral_attributes(neutral_attributes) unless control.is_a?(Hash)
10
+
11
+ options = control[:source_location]
12
12
  return options if options.is_a?(Hash)
13
13
 
14
14
  from_neutral_attributes(neutral_attributes)
@@ -18,8 +18,6 @@ module Julewire
18
18
  file = neutral_attributes[Core::Fields::AttributeKeys::CODE_FILE_PATH]
19
19
  line = neutral_attributes[Core::Fields::AttributeKeys::CODE_LINE_NUMBER]
20
20
  function = neutral_attributes[Core::Fields::AttributeKeys::CODE_FUNCTION_NAME]
21
- return EMPTY_HASH if file.nil? && line.nil? && function.nil?
22
-
23
21
  values = Core::Integration::Values::Shape
24
22
  options = {}
25
23
  values.append_field(options, :file, file)
@@ -25,13 +25,7 @@ module Julewire
25
25
  module Hex
26
26
  class << self
27
27
  def zero?(value)
28
- offset = 0
29
- while offset < value.bytesize
30
- return false unless value.getbyte(offset) == 48
31
-
32
- offset += 1
33
- end
34
- true
28
+ value.each_byte.all? { it == 48 }
35
29
  end
36
30
  end
37
31
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Julewire
4
4
  module GCP
5
- VERSION = "1.0.0"
5
+ VERSION = "1.1.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: julewire-gcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexander Grebennik
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: '1.0'
18
+ version: 1.1.0
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: '1.0'
25
+ version: 1.1.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: zeitwerk
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -93,7 +93,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
93
93
  - !ruby/object:Gem::Version
94
94
  version: '0'
95
95
  requirements: []
96
- rubygems_version: 4.0.14
96
+ rubygems_version: 4.0.16
97
97
  specification_version: 4
98
98
  summary: Google Cloud Logging formatter for Julewire.
99
99
  test_files: []