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 +7 -0
- data/CHANGELOG.md +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +40 -0
- data/docs/advanced-configuration.md +25 -0
- data/docs/configuration.md +33 -0
- data/docs/development.md +7 -0
- data/docs/error-reporting.md +31 -0
- data/docs/shape.md +104 -0
- data/docs/trace.md +49 -0
- data/julewire-gcp.gemspec +37 -0
- data/lib/julewire/gcp/destination.rb +22 -0
- data/lib/julewire/gcp/execution_payload.rb +41 -0
- data/lib/julewire/gcp/formatter.rb +238 -0
- data/lib/julewire/gcp/formatter_options.rb +59 -0
- data/lib/julewire/gcp/http_request_fields.rb +49 -0
- data/lib/julewire/gcp/label_formatter.rb +59 -0
- data/lib/julewire/gcp/log_decoder.rb +64 -0
- data/lib/julewire/gcp/log_encoder.rb +19 -0
- data/lib/julewire/gcp/source_location.rb +53 -0
- data/lib/julewire/gcp/source_location_options.rb +33 -0
- data/lib/julewire/gcp/stack_trace.rb +47 -0
- data/lib/julewire/gcp/trace_context/traceparent.rb +62 -0
- data/lib/julewire/gcp/trace_context.rb +104 -0
- data/lib/julewire/gcp/version.rb +7 -0
- data/lib/julewire/gcp.rb +55 -0
- data/lib/julewire-gcp.rb +3 -0
- metadata +99 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module GCP
|
|
5
|
+
module FormatterOptions
|
|
6
|
+
ALLOWED_KEYS = %i[
|
|
7
|
+
label_formatter
|
|
8
|
+
label_options
|
|
9
|
+
max_label_key_bytes
|
|
10
|
+
max_label_value_bytes
|
|
11
|
+
max_labels
|
|
12
|
+
span_id_path
|
|
13
|
+
trace_id_path
|
|
14
|
+
trace_sampled_path
|
|
15
|
+
].freeze
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
def validate!(options)
|
|
19
|
+
Core::Validation.validate_options!(options, ALLOWED_KEYS, name: :formatter)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def trace_headers_paths(paths)
|
|
23
|
+
Array(paths).filter_map { normalize_path(it, min_length: 1) }.freeze
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def trace_value_path(path)
|
|
27
|
+
normalize_path(path, min_length: 2)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def label_formatter(options)
|
|
31
|
+
options[:label_formatter] || LabelFormatter.new(**label_options(options))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def label_options(options)
|
|
35
|
+
label_options = options.fetch(:label_options, {}).dup
|
|
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)
|
|
38
|
+
end
|
|
39
|
+
label_options
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def normalize_path(path, min_length:)
|
|
45
|
+
return if path.nil?
|
|
46
|
+
|
|
47
|
+
normalized = Array(path).map { normalize_path_segment(it) }
|
|
48
|
+
return if normalized.length < min_length
|
|
49
|
+
|
|
50
|
+
normalized.freeze
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def normalize_path_segment(segment)
|
|
54
|
+
segment.is_a?(String) ? segment.to_sym : segment
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module GCP
|
|
5
|
+
module HttpRequestFields
|
|
6
|
+
class << self
|
|
7
|
+
def http_request(record, attributes)
|
|
8
|
+
values = Core::Integration::Values::Shape
|
|
9
|
+
request = {}
|
|
10
|
+
values.append_field(request, "requestMethod", attributes[Core::Fields::AttributeKeys::HTTP_REQUEST_METHOD])
|
|
11
|
+
values.append_field(
|
|
12
|
+
request,
|
|
13
|
+
"requestUrl",
|
|
14
|
+
attributes[Core::Fields::AttributeKeys::URL_FULL] || attributes[Core::Fields::AttributeKeys::URL_PATH]
|
|
15
|
+
)
|
|
16
|
+
values.append_field(request, "status", attributes[Core::Fields::AttributeKeys::HTTP_RESPONSE_STATUS_CODE])
|
|
17
|
+
values.append_field(request, "userAgent", attributes[Core::Fields::AttributeKeys::USER_AGENT_ORIGINAL])
|
|
18
|
+
values.append_field(request, "remoteIp", attributes[Core::Fields::AttributeKeys::CLIENT_ADDRESS])
|
|
19
|
+
values.append_field(
|
|
20
|
+
request,
|
|
21
|
+
"responseSize",
|
|
22
|
+
int64_string(attributes[Core::Fields::AttributeKeys::HTTP_RESPONSE_BODY_SIZE])
|
|
23
|
+
)
|
|
24
|
+
return if request.empty?
|
|
25
|
+
|
|
26
|
+
latency_value = latency(record)
|
|
27
|
+
request["latency"] = latency_value if latency_value
|
|
28
|
+
request
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def latency(record)
|
|
34
|
+
duration_ms = record.fetch(:metrics)[:duration_ms]
|
|
35
|
+
seconds = Float(duration_ms) / 1000
|
|
36
|
+
"#{format("%.9f", seconds).sub(/0+\z/, "").delete_suffix(".")}s"
|
|
37
|
+
rescue ArgumentError, TypeError
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def int64_string(value)
|
|
42
|
+
Integer(value).to_s
|
|
43
|
+
rescue ArgumentError, TypeError
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module GCP
|
|
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)
|
|
9
|
+
@max_labels = validate_count_limit(max_labels, name: :max_labels)
|
|
10
|
+
@max_label_key_bytes = validate_byte_limit(max_label_key_bytes, name: :max_label_key_bytes)
|
|
11
|
+
@max_label_value_bytes = validate_byte_limit(max_label_value_bytes, name: :max_label_value_bytes)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call(labels)
|
|
15
|
+
return if labels.empty?
|
|
16
|
+
|
|
17
|
+
labels.each_with_object({}) do |(key, value), result|
|
|
18
|
+
break result if @max_labels && result.size >= @max_labels
|
|
19
|
+
|
|
20
|
+
label_key = label_key(key)
|
|
21
|
+
next unless label_key
|
|
22
|
+
|
|
23
|
+
result[label_key] = bounded_label_string(value, @max_label_value_bytes)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def label_key(value)
|
|
30
|
+
key = label_string(value)
|
|
31
|
+
return key unless @max_label_key_bytes && key.bytesize > @max_label_key_bytes
|
|
32
|
+
|
|
33
|
+
nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def bounded_label_string(value, max_bytes)
|
|
37
|
+
string = label_string(value)
|
|
38
|
+
return string unless max_bytes && string.bytesize > max_bytes
|
|
39
|
+
|
|
40
|
+
label_string(string.byteslice(0, max_bytes))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def label_string(value)
|
|
44
|
+
Core::Serialization::EncodingSanitizer.call(value.to_s)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def validate_count_limit(value, name:)
|
|
48
|
+
return if value.nil?
|
|
49
|
+
|
|
50
|
+
Core::Validation.validate_integer_limit!(value, name: name, positive: true)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def validate_byte_limit(value, name:)
|
|
54
|
+
Core::Validation.validate_byte_limit!(value, name: name)
|
|
55
|
+
value
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module GCP
|
|
5
|
+
module LogDecoder
|
|
6
|
+
RecordDecoder = Core::CLI::LogFormats::RecordDecoder
|
|
7
|
+
private_constant :RecordDecoder
|
|
8
|
+
|
|
9
|
+
JULEWIRE_SECTION_KEYS = {
|
|
10
|
+
execution: "execution",
|
|
11
|
+
context: "context",
|
|
12
|
+
metrics: "metrics"
|
|
13
|
+
}.freeze
|
|
14
|
+
PAYLOAD_SECTION_KEYS = {
|
|
15
|
+
attributes: "attributes",
|
|
16
|
+
labels: "logging.googleapis.com/labels",
|
|
17
|
+
payload: "payload"
|
|
18
|
+
}.freeze
|
|
19
|
+
private_constant :JULEWIRE_SECTION_KEYS, :PAYLOAD_SECTION_KEYS
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
def match?(payload)
|
|
23
|
+
payload[JULEWIRE_PAYLOAD_FIELD].is_a?(Hash)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def call(payload)
|
|
27
|
+
julewire = payload.fetch(JULEWIRE_PAYLOAD_FIELD)
|
|
28
|
+
record_base(payload, julewire).merge(
|
|
29
|
+
record_sections(payload, julewire),
|
|
30
|
+
error: RecordDecoder.error(julewire["error"])
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def record_base(payload, julewire)
|
|
37
|
+
{
|
|
38
|
+
timestamp: payload["time"] || payload["timestamp"],
|
|
39
|
+
severity: Julewire::Core::Records::Severity.normalize(payload["severity"] || :info),
|
|
40
|
+
kind: RecordDecoder.kind(julewire["kind"] || :point),
|
|
41
|
+
event: julewire["event"],
|
|
42
|
+
message: payload["message"],
|
|
43
|
+
logger: julewire["logger"],
|
|
44
|
+
source: julewire["source"]
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def record_sections(payload, julewire)
|
|
49
|
+
RecordDecoder.sections(payload) do |section, _source|
|
|
50
|
+
gcp_section_value(section, payload, julewire)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def gcp_section_value(section, payload, julewire)
|
|
55
|
+
if (key = JULEWIRE_SECTION_KEYS[section])
|
|
56
|
+
julewire[key]
|
|
57
|
+
elsif (key = PAYLOAD_SECTION_KEYS[section])
|
|
58
|
+
payload[key]
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module GCP
|
|
5
|
+
module LogEncoder
|
|
6
|
+
class << self
|
|
7
|
+
def call(record)
|
|
8
|
+
json_encoder.call(formatter.call(record))
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def formatter = @formatter ||= Formatter.new
|
|
14
|
+
|
|
15
|
+
def json_encoder = @json_encoder ||= JsonEncoder.new
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module GCP
|
|
5
|
+
module SourceLocation
|
|
6
|
+
BACKTRACE_PATTERN = /\A(?<file>.+?):(?<line>\d+)(?::in (?:[`'](?<quoted>.*)[`']|(?<plain>.*)))?\z/
|
|
7
|
+
private_constant :BACKTRACE_PATTERN
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
def call(options)
|
|
11
|
+
values = Core::Integration::Values::Shape
|
|
12
|
+
location = {}
|
|
13
|
+
values.append_field(location, "file", string_value(options[:file]))
|
|
14
|
+
values.append_field(location, "line", line_value(options[:line]))
|
|
15
|
+
values.append_field(location, "function", string_value(options[:function]))
|
|
16
|
+
location unless location.empty?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def from_error(error)
|
|
20
|
+
return unless error.is_a?(Hash)
|
|
21
|
+
|
|
22
|
+
Array(error[:backtrace]).each do |line|
|
|
23
|
+
location = from_backtrace_line(line)
|
|
24
|
+
return location if location
|
|
25
|
+
end
|
|
26
|
+
nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def from_backtrace_line(line)
|
|
30
|
+
match = BACKTRACE_PATTERN.match(line.to_s)
|
|
31
|
+
return unless match
|
|
32
|
+
|
|
33
|
+
call(
|
|
34
|
+
file: match[:file],
|
|
35
|
+
line: match[:line],
|
|
36
|
+
function: match[:quoted] || match[:plain]
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def string_value(value)
|
|
41
|
+
return if Core::Integration::Values::Read.blank?(value)
|
|
42
|
+
|
|
43
|
+
value.to_s
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def line_value(value)
|
|
47
|
+
string = value.to_s
|
|
48
|
+
string if string.match?(/\A\d+\z/)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module GCP
|
|
5
|
+
module SourceLocationOptions
|
|
6
|
+
EMPTY_HASH = {}.freeze
|
|
7
|
+
private_constant :EMPTY_HASH
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
def call(record, neutral_attributes)
|
|
11
|
+
options = record.dig(:payload, :gcp, :source_location)
|
|
12
|
+
return options if options.is_a?(Hash)
|
|
13
|
+
|
|
14
|
+
from_neutral_attributes(neutral_attributes)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def from_neutral_attributes(neutral_attributes)
|
|
18
|
+
file = neutral_attributes[Core::Fields::AttributeKeys::CODE_FILE_PATH]
|
|
19
|
+
line = neutral_attributes[Core::Fields::AttributeKeys::CODE_LINE_NUMBER]
|
|
20
|
+
function = neutral_attributes[Core::Fields::AttributeKeys::CODE_FUNCTION_NAME]
|
|
21
|
+
return EMPTY_HASH if file.nil? && line.nil? && function.nil?
|
|
22
|
+
|
|
23
|
+
values = Core::Integration::Values::Shape
|
|
24
|
+
options = {}
|
|
25
|
+
values.append_field(options, :file, file)
|
|
26
|
+
values.append_field(options, :line, line)
|
|
27
|
+
values.append_field(options, :function, function)
|
|
28
|
+
options
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module GCP
|
|
5
|
+
module StackTrace
|
|
6
|
+
class << self
|
|
7
|
+
def call(error)
|
|
8
|
+
return unless error.is_a?(Hash)
|
|
9
|
+
|
|
10
|
+
lines = lines(error)
|
|
11
|
+
lines.join("\n") unless lines.empty?
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def remove_backtraces(value)
|
|
15
|
+
case value
|
|
16
|
+
when Hash
|
|
17
|
+
value.each_with_object({}) do |(key, item), copy|
|
|
18
|
+
next if key == :backtrace
|
|
19
|
+
|
|
20
|
+
copy[key] = remove_backtraces(item)
|
|
21
|
+
end
|
|
22
|
+
when Array
|
|
23
|
+
value.map { remove_backtraces(it) }
|
|
24
|
+
else
|
|
25
|
+
value
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def lines(error)
|
|
32
|
+
backtrace = Array(error[:backtrace])
|
|
33
|
+
cause = error[:cause]
|
|
34
|
+
cause_lines = cause.is_a?(Hash) ? lines(cause) : []
|
|
35
|
+
return [] if backtrace.empty? && cause_lines.empty?
|
|
36
|
+
|
|
37
|
+
summary = Core::Records::DisplayMessage.error_summary(error)
|
|
38
|
+
[summary, *backtrace, *prefixed_cause_lines(cause_lines)].compact
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def prefixed_cause_lines(lines)
|
|
42
|
+
lines.map.with_index { |line, index| index.zero? ? "Caused by: #{line}" : line }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module GCP
|
|
5
|
+
module TraceContext
|
|
6
|
+
module Traceparent
|
|
7
|
+
TRACEPARENT_PATTERN =
|
|
8
|
+
/\A[[:xdigit:]]{2}-[[:xdigit:]]{32}-[[:xdigit:]]{16}-[[:xdigit:]]{2}(?:-.*)?\z/
|
|
9
|
+
MIN_BYTES = 55
|
|
10
|
+
TRACE_ID_OFFSET = 3
|
|
11
|
+
TRACE_ID_BYTES = 32
|
|
12
|
+
SPAN_ID_OFFSET = 36
|
|
13
|
+
SPAN_ID_BYTES = 16
|
|
14
|
+
TRACE_FLAGS_OFFSET = 53
|
|
15
|
+
private_constant :TRACEPARENT_PATTERN,
|
|
16
|
+
:MIN_BYTES,
|
|
17
|
+
:TRACE_ID_OFFSET,
|
|
18
|
+
:TRACE_ID_BYTES,
|
|
19
|
+
:SPAN_ID_OFFSET,
|
|
20
|
+
:SPAN_ID_BYTES,
|
|
21
|
+
:TRACE_FLAGS_OFFSET
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
def call(value)
|
|
25
|
+
value = value.to_s.scrub.strip
|
|
26
|
+
return unless TRACEPARENT_PATTERN.match?(value)
|
|
27
|
+
|
|
28
|
+
parse_value(value)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def parse_value(value)
|
|
34
|
+
version = value.byteslice(0, 2).downcase
|
|
35
|
+
return if version == "ff"
|
|
36
|
+
return if version == "00" && value.byteslice(MIN_BYTES)
|
|
37
|
+
|
|
38
|
+
trace_id = value.byteslice(TRACE_ID_OFFSET, TRACE_ID_BYTES)
|
|
39
|
+
span_id = value.byteslice(SPAN_ID_OFFSET, SPAN_ID_BYTES)
|
|
40
|
+
trace_id.downcase!
|
|
41
|
+
span_id.downcase!
|
|
42
|
+
return if Hex.zero?(trace_id) || Hex.zero?(span_id)
|
|
43
|
+
|
|
44
|
+
context(trace_id, span_id, trace_flags(value).allbits?(1))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def context(trace_id, span_id, sampled)
|
|
48
|
+
{
|
|
49
|
+
trace_id: trace_id,
|
|
50
|
+
span_id: span_id,
|
|
51
|
+
trace_sampled: sampled
|
|
52
|
+
}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def trace_flags(value)
|
|
56
|
+
Integer(value.byteslice(TRACE_FLAGS_OFFSET, 2), 16)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module GCP
|
|
5
|
+
module TraceContext
|
|
6
|
+
X_CLOUD_TRACE_PATTERN = %r{\A([[:xdigit:]]{32})(?:/(\d+))?(?:;o=(\d+))?\z}
|
|
7
|
+
TRACEPARENT_HEADER = "traceparent"
|
|
8
|
+
X_CLOUD_TRACE_CONTEXT_HEADER = "x-cloud-trace-context"
|
|
9
|
+
X_CLOUD_TRACE_CONTEXT_UNDERSCORE_HEADER = "x_cloud_trace_context"
|
|
10
|
+
MAX_SPAN_ID = (2**64) - 1
|
|
11
|
+
DIRECT_HEADER_KEYS = {
|
|
12
|
+
traceparent: [:traceparent, TRACEPARENT_HEADER],
|
|
13
|
+
x_cloud_trace_context: [
|
|
14
|
+
:x_cloud_trace_context,
|
|
15
|
+
X_CLOUD_TRACE_CONTEXT_UNDERSCORE_HEADER,
|
|
16
|
+
X_CLOUD_TRACE_CONTEXT_HEADER
|
|
17
|
+
]
|
|
18
|
+
}.freeze
|
|
19
|
+
CANONICAL_HEADER_NAMES = {
|
|
20
|
+
traceparent: TRACEPARENT_HEADER,
|
|
21
|
+
x_cloud_trace_context: X_CLOUD_TRACE_CONTEXT_HEADER
|
|
22
|
+
}.freeze
|
|
23
|
+
private_constant :DIRECT_HEADER_KEYS, :CANONICAL_HEADER_NAMES
|
|
24
|
+
|
|
25
|
+
module Hex
|
|
26
|
+
class << self
|
|
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
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
private_constant :Hex
|
|
39
|
+
|
|
40
|
+
class << self
|
|
41
|
+
def extract(headers)
|
|
42
|
+
return {} unless headers.respond_to?(:[])
|
|
43
|
+
|
|
44
|
+
parse_traceparent(fetch_header(headers, :traceparent)) ||
|
|
45
|
+
parse_x_cloud_trace_context(fetch_header(headers, :x_cloud_trace_context)) ||
|
|
46
|
+
{}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def parse_traceparent(value)
|
|
50
|
+
Traceparent.call(value)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def parse_x_cloud_trace_context(value)
|
|
54
|
+
match = X_CLOUD_TRACE_PATTERN.match(value.to_s.scrub.strip)
|
|
55
|
+
return unless match
|
|
56
|
+
|
|
57
|
+
trace_id = match[1].downcase
|
|
58
|
+
return if Hex.zero?(trace_id)
|
|
59
|
+
|
|
60
|
+
context = { trace_id: trace_id }
|
|
61
|
+
if match[2]
|
|
62
|
+
span_id = decimal_span_to_hex(match[2])
|
|
63
|
+
context[:span_id] = span_id if span_id
|
|
64
|
+
end
|
|
65
|
+
context[:trace_sampled] = Integer(match[3], 10).allbits?(1) if match[3]
|
|
66
|
+
context
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def fetch_header(headers, key)
|
|
72
|
+
DIRECT_HEADER_KEYS.fetch(key).each do |header_key|
|
|
73
|
+
value = headers[header_key]
|
|
74
|
+
return value unless value.nil?
|
|
75
|
+
end
|
|
76
|
+
return unless headers.respond_to?(:each)
|
|
77
|
+
|
|
78
|
+
canonical_name = CANONICAL_HEADER_NAMES.fetch(key)
|
|
79
|
+
headers.each do |name, value|
|
|
80
|
+
return value if normalize_header_name(name) == canonical_name
|
|
81
|
+
end
|
|
82
|
+
nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def normalize_header_name(name)
|
|
86
|
+
normalized = name.to_s.dup
|
|
87
|
+
normalized.tr!("_", "-")
|
|
88
|
+
normalized.downcase!
|
|
89
|
+
normalized
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def decimal_span_to_hex(value)
|
|
93
|
+
integer = Integer(value, 10)
|
|
94
|
+
return if integer > MAX_SPAN_ID
|
|
95
|
+
|
|
96
|
+
span_id = integer.to_s(16).rjust(16, "0")
|
|
97
|
+
return if Hex.zero?(span_id)
|
|
98
|
+
|
|
99
|
+
span_id
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
data/lib/julewire/gcp.rb
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zeitwerk"
|
|
4
|
+
require "julewire/core"
|
|
5
|
+
|
|
6
|
+
module Julewire
|
|
7
|
+
module GCP
|
|
8
|
+
CARRY_REQUEST_HEADERS = %w[
|
|
9
|
+
traceparent
|
|
10
|
+
tracestate
|
|
11
|
+
x-cloud-trace-context
|
|
12
|
+
].freeze
|
|
13
|
+
RECOMMENDED_MAX_RECORD_BYTES = 256 * 1024
|
|
14
|
+
DEFAULT_MAX_RECORD_BYTES = RECOMMENDED_MAX_RECORD_BYTES
|
|
15
|
+
DEFAULT_MAX_LABELS = 64
|
|
16
|
+
DEFAULT_MAX_LABEL_KEY_BYTES = 512
|
|
17
|
+
DEFAULT_MAX_LABEL_VALUE_BYTES = 64 * 1024
|
|
18
|
+
JULEWIRE_PAYLOAD_FIELD = "julewire"
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
def operation(id: nil, producer: nil, first: nil, last: nil)
|
|
22
|
+
values = Core::Integration::Values::Shape
|
|
23
|
+
operation = {}
|
|
24
|
+
values.append_field(operation, :id, id)
|
|
25
|
+
values.append_field(operation, :producer, producer)
|
|
26
|
+
values.append_field(operation, :first, first)
|
|
27
|
+
values.append_field(operation, :last, last)
|
|
28
|
+
{
|
|
29
|
+
gcp: {
|
|
30
|
+
operation: operation
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def source_location(file: nil, line: nil, function: nil)
|
|
36
|
+
values = Core::Integration::Values::Shape
|
|
37
|
+
source_location = {}
|
|
38
|
+
values.append_field(source_location, :file, file)
|
|
39
|
+
values.append_field(source_location, :line, line)
|
|
40
|
+
values.append_field(source_location, :function, function)
|
|
41
|
+
{
|
|
42
|
+
gcp: {
|
|
43
|
+
source_location: source_location
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
loader = Zeitwerk::Loader.for_gem_extension(self)
|
|
51
|
+
loader.inflector.inflect("gcp" => "GCP")
|
|
52
|
+
loader.setup
|
|
53
|
+
Core::Destinations.register(:gcp) { |name:, **options| GCP::Destination.new(name: name, **options) }
|
|
54
|
+
Core::CLI::LogFormats.register(:gcp, decoder: GCP::LogDecoder, encoder: GCP::LogEncoder, priority: 100)
|
|
55
|
+
end
|
data/lib/julewire-gcp.rb
ADDED