multiplayer_otlp 0.0.1

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: 7eeb6ab03f33e4743bfebca755ccbceda2cd5d4ab6845b8253efd76959c56a1e
4
+ data.tar.gz: 0fb380313170e64a5e13973458fe4ffefb719e5f241f6ed91db5d5b5bae0c657
5
+ SHA512:
6
+ metadata.gz: 5cfefcca13d72d096f0b1b58030cfaa20a8a9c3733137662b49a56f3e4b56ba4c312e3807f9d5035b9cfcd87749790316e12f455e6e9fda896cc5d6c3fefce1d
7
+ data.tar.gz: 3d8361914b09d0279f60055fac13fbad994100dbe933f381362029695c4c2defbef599561c4157a6cb1c25004749fec405309e9d390daaa9ca45219056e97236
@@ -0,0 +1,57 @@
1
+ require "json"
2
+ require "opentelemetry/sdk"
3
+ require "opentelemetry/exporter/otlp"
4
+
5
+ module Multiplayer
6
+ class ExporterConfig
7
+ attr_accessor :endpoint, :headers, :api_key
8
+
9
+ def initialize(endpoint: nil, headers: {}, api_key: nil)
10
+ @endpoint = endpoint
11
+ @headers = headers
12
+ @api_key = api_key
13
+ end
14
+ end
15
+
16
+ class Exporter < OpenTelemetry::Exporter::OTLP::Exporter
17
+ def initialize(config = Multiplayer::ExporterConfig.new)
18
+
19
+ config.endpoint ||= MULTIPLAYER_OTEL_DEFAULT_TRACES_EXPORTER_URL
20
+ config.headers = (config.headers || {}).merge(
21
+ config.api_key ? { "Authorization" => config.api_key } : {}
22
+ )
23
+ super(endpoint: config.endpoint, headers: config.headers)
24
+ end
25
+
26
+ def get_default_url(config)
27
+ config.url || MULTIPLAYER_OTEL_DEFAULT_TRACES_EXPORTER_URL
28
+ end
29
+
30
+ def self.filter_mp_attributes(exporter)
31
+ original_export = exporter.method("export")
32
+ exporter.define_singleton_method("export") do |*args, &block|
33
+ args[0] = Exporter.prefilter(args[0])
34
+ original_export.call(*args, &block)
35
+ end
36
+
37
+ exporter
38
+ end
39
+
40
+ private
41
+ def self.prefilter(spans)
42
+ spans.map do |span|
43
+ new_attributes = span[:attributes].each_with_object({}) do |(key, value), acc|
44
+ unless key.start_with?(MULTIPLAYER_ATTRIBUTE_PREFIX)
45
+ acc[key] = value
46
+ end
47
+ end
48
+
49
+ span_with_new_attributes = span.dup
50
+ span_with_new_attributes[:attributes] = new_attributes
51
+
52
+ span_with_new_attributes
53
+ end
54
+ end
55
+
56
+ end
57
+ end
@@ -0,0 +1,122 @@
1
+ require "json"
2
+
3
+ module Multiplayer
4
+ class Middleware
5
+ attr_reader :headers_to_mask, :max_payload_size, :schemify_doc_span_payload, :mask_deb_span_payload
6
+ def initialize(app, options = {
7
+ headers_to_mask: [],
8
+ max_payload_size: nil,
9
+ schemify_doc_span_payload: true,
10
+ mask_deb_span_payload: true
11
+ }
12
+ )
13
+ @app = app
14
+ @headers_to_mask = options[:headers_to_mask] || []
15
+ @max_payload_size = options[:max_payload_size] || MULTIPLAYER_MAX_HTTP_REQUEST_RESPONSE_SIZE
16
+ @schemify_doc_span_payload = options.key?(:schemify_doc_span_payload) ?
17
+ options[:schemify_doc_span_payload] :
18
+ true
19
+ @mask_deb_span_payload = options.key?(:mask_deb_span_payload) ?
20
+ options[:mask_deb_span_payload] :
21
+ true
22
+
23
+ if @max_payload_size > MULTIPLAYER_MAX_HTTP_REQUEST_RESPONSE_SIZE
24
+ @max_payload_size = MULTIPLAYER_MAX_HTTP_REQUEST_RESPONSE_SIZE
25
+ end
26
+ end
27
+
28
+ protected
29
+
30
+ def mask_primitives(input, current_depth = 0)
31
+ return MASK_PLACEHOLDER if current_depth >= MAX_MASK_DEPTH
32
+
33
+ case input
34
+ when Hash
35
+ input.transform_values { |value| mask_primitives(value, current_depth + 1) }
36
+ when Array
37
+ input.map { |value| mask_primitives(value, current_depth + 1) }
38
+ when String, Numeric, TrueClass, FalseClass, NilClass, Symbol
39
+ MASK_PLACEHOLDER
40
+ else
41
+ input
42
+ end
43
+ end
44
+
45
+ def mask_body(value)
46
+ payload_json = begin
47
+ JSON.parse(value)
48
+ rescue JSON::ParserError
49
+ value
50
+ end
51
+
52
+ masked_data = mask_primitives(payload_json)
53
+
54
+ unless masked_data.is_a?(String)
55
+ masked_data = masked_data.to_json
56
+ end
57
+
58
+ masked_data
59
+ end
60
+
61
+ def schemify(payload)
62
+ return "" if payload.nil?
63
+ payload_json = nil
64
+
65
+ if payload.is_a?(String)
66
+ begin
67
+ payload_json = JSON.parse(payload)
68
+ rescue JSON::ParserError
69
+ return payload
70
+ end
71
+ elsif payload.is_a?(Hash) || payload.is_a?(Array)
72
+ payload_json = payload
73
+ else
74
+ return payload
75
+ end
76
+
77
+ begin
78
+ Multiplayer::Utils::JsonSchemaGenerator.generate(payload_json)
79
+ rescue StandardError => exception
80
+ ""
81
+ end
82
+ end
83
+
84
+ def mask_headers(headers, custom_header_names_to_mask = [])
85
+ default_header_names_to_mask = ["set-cookie", "cookie", "authorization", "proxy-authorization"]
86
+ masked_headers = headers.dup
87
+ headers_to_mask = default_header_names_to_mask + custom_header_names_to_mask
88
+
89
+ headers_to_mask.each do |header_name|
90
+ masked_headers[header_name] = MASK_PLACEHOLDER if masked_headers.key?(header_name)
91
+ end
92
+
93
+ masked_headers
94
+ end
95
+
96
+
97
+ # Extracts request headers from the Rack environment
98
+ def extract_request_headers(env)
99
+ env.select { |k, _| k.start_with?("HTTP_") }
100
+ .transform_keys { |k| k.sub(/^HTTP_/, "").split("_").map(&:downcase).join("-") }
101
+ end
102
+
103
+ # Reads the request body safely
104
+ def extract_request_body(request)
105
+ body = request.body.read
106
+ request.body.rewind # Rewind the stream for downstream middlewares
107
+ end
108
+
109
+ # Reads the response body safely
110
+ def extract_response_body(response)
111
+ body = []
112
+ response.each { |part| body << part }
113
+ body.join
114
+ end
115
+
116
+ # Truncates payload if maxPayloadSize is set
117
+ def truncate_if_needed(data)
118
+ return data unless data.to_s.size > @max_payload_size
119
+ "#{data[0...@max_payload_size]}...[TRUNCATED]"
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,31 @@
1
+ module Multiplayer
2
+ class RequestMiddleware < Multiplayer::Middleware
3
+ def call(env)
4
+ current_span = OpenTelemetry::Trace.current_span
5
+ trace_id = current_span.context.trace_id.unpack1("H*")
6
+ request = Rack::Request.new(env)
7
+
8
+ request_headers = extract_request_headers(env)
9
+ masked_headers = mask_headers(request_headers, @headers_to_mask)
10
+ current_span.set_attribute(ATTR_MULTIPLAYER_HTTP_REQUEST_HEADERS, masked_headers.to_json)
11
+
12
+ if request_headers.key?("content-type") == "application/json"
13
+ request_body = (extract_request_body(request)).dup
14
+
15
+ if trace_id.start_with?(MULTIPLAYER_TRACE_DEBUG_PREFIX) && @mask_deb_span_payload
16
+ request_body = mask_body(request_body)
17
+ elsif trace_id.start_with?(MULTIPLAYER_TRACE_DOC_PREFIX) || @schemify_doc_span_payload
18
+ request_body = schemify(request_body)
19
+ elsif !request_body.is_a?(String)
20
+ request_body = JSON.generate(request_body)
21
+ end
22
+
23
+ request_body = truncate_if_needed(request_body)
24
+
25
+ current_span.set_attribute(ATTR_MULTIPLAYER_HTTP_REQUEST_BODY, request_body.is_a?(String) ? request_body : request_body.to_json)
26
+ end
27
+
28
+ @app.call(env)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,32 @@
1
+ module Multiplayer
2
+ class ResponseMiddleware < Multiplayer::Middleware
3
+ def call(env)
4
+ status, headers, response = @app.call(env)
5
+
6
+ begin
7
+ current_span = OpenTelemetry::Trace.current_span
8
+ trace_id = current_span.context.trace_id.unpack1("H*")
9
+
10
+ headers["x-trace-id"] = trace_id
11
+
12
+ masked_headers = mask_headers(headers, @headers_to_mask)
13
+ response_body = (extract_response_body(response)).dup
14
+
15
+ if trace_id.start_with?(MULTIPLAYER_TRACE_DEBUG_PREFIX) && @mask_deb_span_payload
16
+ response_body = mask_body(response_body)
17
+ elsif trace_id.start_with?(MULTIPLAYER_TRACE_DOC_PREFIX) || @schemify_doc_span_payload
18
+ response_body = schemify(response_body)
19
+ elsif !response_body.is_a?(String)
20
+ response_body = JSON.generate(response_body)
21
+ end
22
+ response_body = truncate_if_needed(response_body)
23
+
24
+ current_span.set_attribute(ATTR_MULTIPLAYER_HTTP_RESPONSE_HEADERS, masked_headers.to_json)
25
+ current_span.set_attribute(ATTR_MULTIPLAYER_HTTP_RESPONSE_BODY, response_body.is_a?(String) ? response_body : response_body.to_json)
26
+ [ status, headers, response ]
27
+ rescue
28
+ [ status, headers, response ]
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,25 @@
1
+ require_relative "utils/id_generator"
2
+ require_relative "utils/json_schema_generator"
3
+ require_relative "utils/trace_id_ratio_based_sampler"
4
+ require_relative "version"
5
+ require_relative "exporters/http_exporter"
6
+ require_relative "middleware/middleware"
7
+ require_relative "middleware/request_middleware"
8
+ require_relative "middleware/response_middleware"
9
+
10
+ module Multiplayer
11
+ MULTIPLAYER_TRACE_DOC_PREFIX = "d0cd0c"
12
+ MULTIPLAYER_TRACE_DEBUG_PREFIX = "debdeb"
13
+ MULTIPLAYER_OTEL_DEFAULT_TRACES_EXPORTER_URL = "https://api.multiplayer.app/v1/traces"
14
+ MULTIPLAYER_OTEL_DEFAULT_LOGS_EXPORTER_URL = "https://api.multiplayer.app/v1/logs"
15
+ MULTIPLAYER_ATTRIBUTE_PREFIX = "multiplayer."
16
+ MULTIPLAYER_MAX_HTTP_REQUEST_RESPONSE_SIZE = (ENV["MULTIPLAYER_MAX_HTTP_REQUEST_RESPONSE_SIZE"] || "500000").to_i
17
+ ATTR_MULTIPLAYER_DEBUG_SESSION = "multiplayer.debug_session._id"
18
+ ATTR_MULTIPLAYER_HTTP_REQUEST_BODY = "multiplayer.http.request.body"
19
+ ATTR_MULTIPLAYER_HTTP_RESPONSE_BODY = "multiplayer.http.response.body"
20
+ ATTR_MULTIPLAYER_HTTP_REQUEST_HEADERS = "multiplayer.http.request.headers"
21
+ ATTR_MULTIPLAYER_HTTP_RESPONSE_HEADERS = "multiplayer.http.response.headers"
22
+ ATTR_MULTIPLAYER_HTTP_RESPONSE_BODY_ENCODING = "multiplayer.http.response.body.encoding"
23
+ MASK_PLACEHOLDER = "***MASKED***"
24
+ MAX_MASK_DEPTH = 8
25
+ end
@@ -0,0 +1,42 @@
1
+ require "opentelemetry-sdk"
2
+
3
+ module Multiplayer
4
+ module Utils
5
+ class IdGenerator
6
+ include OpenTelemetry::Trace
7
+ attr_accessor :debug_session_short_id, :id_generator, :sampler
8
+
9
+ SHARED_CHAR_CODES_ARRAY = Array.new(32)
10
+
11
+ def initialize(auto_doc_traces_ratio = 0)
12
+ @debug_session_short_id = nil
13
+ @id_generator = IdGenerator.get_id_generator(16)
14
+ @sampler = TraceIdRatioBasedSampler.new(auto_doc_traces_ratio)
15
+ end
16
+
17
+ def generate_trace_id
18
+ trace_id = @id_generator.call
19
+ if @debug_session_short_id
20
+ prefix = "#{MULTIPLAYER_TRACE_DEBUG_PREFIX}#{@debug_session_short_id}"
21
+ trace_id = "#{prefix}#{trace_id[prefix.length..-1]}"
22
+ elsif @sampler.is_mp_trace_id?(trace_id)
23
+ trace_id = "#{MULTIPLAYER_TRACE_DOC_PREFIX}#{trace_id[MULTIPLAYER_TRACE_DOC_PREFIX.length..-1]}"
24
+ end
25
+ [ trace_id ].pack("H*")
26
+ end
27
+
28
+ def self.get_id_generator(bytes)
29
+ lambda do
30
+ (0...(bytes * 2)).each do |i|
31
+ SHARED_CHAR_CODES_ARRAY[i] = rand(16) + 48
32
+ # valid hex characters in the range 48-57 and 97-102
33
+ if SHARED_CHAR_CODES_ARRAY[i] >= 58
34
+ SHARED_CHAR_CODES_ARRAY[i] += 39
35
+ end
36
+ end
37
+ SHARED_CHAR_CODES_ARRAY[0...(bytes * 2)].map(&:chr).join
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,36 @@
1
+ require "json-schema-generator"
2
+
3
+ module Multiplayer
4
+ module Utils
5
+ class JsonSchemaGenerator < JSON::SchemaGenerator
6
+ class << self
7
+ def generate(data)
8
+ generator = Multiplayer::JsonSchemaGenerator.new
9
+ generator.generate data
10
+ end
11
+ end
12
+
13
+ def initialize
14
+ @version = DRAFT4
15
+ @defaults = nil
16
+
17
+ @buffer = StringIO.new
18
+ end
19
+
20
+ def generate(data)
21
+ statement_group = StatementGroup.new
22
+ case data
23
+ when Array
24
+ $stop = true
25
+ create_array(statement_group, data, [])
26
+ when Hash
27
+ create_hash(statement_group, data, [])
28
+ else
29
+ create_primitive(statement_group, "", data, [])
30
+ end
31
+ @buffer.puts statement_group
32
+ result
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,57 @@
1
+ require "opentelemetry-sdk"
2
+
3
+ module Multiplayer
4
+ module Utils
5
+ class TraceIdRatioBasedSampler < OpenTelemetry::SDK::Trace::Samplers::TraceIdRatioBased
6
+ def initialize(ratio = 0)
7
+ @ratio = normalize(ratio)
8
+ @upper_bound = (@ratio * 0xffffffff).floor
9
+ super(@ratio)
10
+ end
11
+
12
+ def should_sample?(trace_id:, parent_context:, links:, name:, kind:, attributes:)
13
+ tracestate = OpenTelemetry::Trace.current_span(parent_context).context.tracestate
14
+ if self.is_mp_trace_id?(trace_id.unpack1("H*"))
15
+ OpenTelemetry::SDK::Trace::Samplers::Result.new(decision: OpenTelemetry::SDK::Trace::Samplers::Decision::RECORD_AND_SAMPLE, tracestate: tracestate)
16
+ else
17
+ OpenTelemetry::SDK::Trace::Samplers::Result.new(decision: OpenTelemetry::SDK::Trace::Samplers::Decision::DROP, tracestate: tracestate)
18
+ end
19
+ end
20
+
21
+ def is_mp_trace_id?(trace_id)
22
+ trace_id.start_with?(MULTIPLAYER_TRACE_DEBUG_PREFIX) ||
23
+ trace_id.start_with?(MULTIPLAYER_TRACE_DOC_PREFIX) ||
24
+ (self.valid_trace_id?(trace_id) && self.accumulate(trace_id) < @upper_bound)
25
+ end
26
+
27
+ def description
28
+ "MultiplayerTraceIdRatioBasedSampler#{@ratio}"
29
+ end
30
+
31
+ private
32
+
33
+ def valid_trace_id?(trace_id)
34
+ trace_id.is_a?(String) && trace_id.length == 32 && trace_id =~ /^([0-9a-f]{32})$/i
35
+ end
36
+
37
+ def normalize(ratio)
38
+ return 0 unless defined?(ratio) && ratio.is_a?(Numeric)
39
+ return 1 if ratio >= 1
40
+ return 0 if ratio <= 0
41
+
42
+ ratio
43
+ end
44
+
45
+ def accumulate(trace_id)
46
+ accumulation = 0
47
+ (0...trace_id.length / 8).each do |i|
48
+ pos = i * 8
49
+ part = trace_id[pos, 8].to_i(16)
50
+ accumulation = (accumulation ^ part) & 0xffffffff
51
+ end
52
+ accumulation
53
+ end
54
+ end
55
+ end
56
+ end
57
+
@@ -0,0 +1,3 @@
1
+ module Multiplayer
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,4 @@
1
+ require_relative "multiplayer/multiplayer"
2
+ module MultiplayerOTLP
3
+
4
+ end
metadata ADDED
@@ -0,0 +1,94 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: multiplayer_otlp
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Multiplayer Inc.
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-12-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: opentelemetry-sdk
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.6'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: opentelemetry-exporter-otlp
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.29.1
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.29.1
41
+ - !ruby/object:Gem::Dependency
42
+ name: json-schema-generator
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 0.0.9
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 0.0.9
55
+ description:
56
+ email:
57
+ executables: []
58
+ extensions: []
59
+ extra_rdoc_files: []
60
+ files:
61
+ - lib/multiplayer/exporters/http_exporter.rb
62
+ - lib/multiplayer/middleware/middleware.rb
63
+ - lib/multiplayer/middleware/request_middleware.rb
64
+ - lib/multiplayer/middleware/response_middleware.rb
65
+ - lib/multiplayer/multiplayer.rb
66
+ - lib/multiplayer/utils/id_generator.rb
67
+ - lib/multiplayer/utils/json_schema_generator.rb
68
+ - lib/multiplayer/utils/trace_id_ratio_based_sampler.rb
69
+ - lib/multiplayer/version.rb
70
+ - lib/multiplayer_otlp.rb
71
+ homepage: https://rubygems.org/gems/multiplayer_otlp
72
+ licenses:
73
+ - MIT
74
+ metadata: {}
75
+ post_install_message:
76
+ rdoc_options: []
77
+ require_paths:
78
+ - lib
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '3.0'
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ requirements: []
90
+ rubygems_version: 3.5.22
91
+ signing_key:
92
+ specification_version: 4
93
+ summary: Multiplayer OTLP library
94
+ test_files: []