multiplayer-session-recorder 0.0.6
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/lib/session-recorder.rb +12 -0
- data/lib/session_recorder/constants.rb +79 -0
- data/lib/session_recorder/exporters/grpc_logs_exporter.rb +36 -0
- data/lib/session_recorder/exporters/grpc_trace_exporter.rb +36 -0
- data/lib/session_recorder/exporters/http_logs_exporter.rb +36 -0
- data/lib/session_recorder/exporters/http_trace_exporter.rb +36 -0
- data/lib/session_recorder/exporters/logs_exporter_wrapper.rb +44 -0
- data/lib/session_recorder/exporters/trace_exporter_wrapper.rb +44 -0
- data/lib/session_recorder/exporters.rb +42 -0
- data/lib/session_recorder/middleware/middleware.rb +110 -0
- data/lib/session_recorder/middleware/request_middleware.rb +29 -0
- data/lib/session_recorder/middleware/response_middleware.rb +32 -0
- data/lib/session_recorder/trace/id_generator.rb +66 -0
- data/lib/session_recorder/trace/trace_id_ratio_based_sampler.rb +72 -0
- data/lib/session_recorder/type/session_type.rb +10 -0
- data/lib/session_recorder/version.rb +7 -0
- metadata +86 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 1ca02b61e222654ebd5a78168a2455dd3ed138fe1c61dd30bb7189028568c670
|
4
|
+
data.tar.gz: f97f4949e513ff1fb19477fdbf079de84d4a9619c32c1b9da07430136e514955
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 789daab68f6ff9a9dff8ce56fb9e212a8d4d54340f942040db2481f5f951ecfc12eebf9022750486c895080b895e655d17dc7aee3bccc7c0153a272816ab0d34
|
7
|
+
data.tar.gz: a45acfb3e3163c8ed9c2166a78344a561989c4caa5c0dbc6ec0f4821d588a1605d9d4e15ddb727862bd4248a7aa2b1a9bd888241dc16fee5abeabedde3f6c088
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require "session_recorder/exporters"
|
2
|
+
require "session_recorder/middleware"
|
3
|
+
require "session_recorder/trace"
|
4
|
+
require "session_recorder/type"
|
5
|
+
require "session_recorder/constants"
|
6
|
+
require "session_recorder/version"
|
7
|
+
|
8
|
+
module Multiplayer
|
9
|
+
module SessionRecorder
|
10
|
+
# Main module for Session Recorder functionality
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SessionRecorder
|
4
|
+
MULTIPLAYER_TRACE_DEBUG_PREFIX = 'debdeb'
|
5
|
+
|
6
|
+
MULTIPLAYER_TRACE_CONTINUOUS_DEBUG_PREFIX = 'cdbcdb'
|
7
|
+
|
8
|
+
MULTIPLAYER_TRACE_DEBUG_SESSION_SHORT_ID_LENGTH = 16
|
9
|
+
|
10
|
+
MULTIPLAYER_OTEL_DEFAULT_TRACES_EXPORTER_HTTP_URL = 'https://otlp.multiplayer.app/v1/traces'
|
11
|
+
|
12
|
+
MULTIPLAYER_OTEL_DEFAULT_LOGS_EXPORTER_HTTP_URL = 'https://otlp.multiplayer.app/v1/logs'
|
13
|
+
|
14
|
+
MULTIPLAYER_OTEL_DEFAULT_TRACES_EXPORTER_GRPC_URL = 'https://otlp.multiplayer.app:4317/v1/traces'
|
15
|
+
|
16
|
+
MULTIPLAYER_OTEL_DEFAULT_LOGS_EXPORTER_GRPC_URL = 'https://otlp.multiplayer.app:4317/v1/logs'
|
17
|
+
|
18
|
+
MULTIPLAYER_BASE_API_URL = 'https://api.multiplayer.app'
|
19
|
+
|
20
|
+
MULTIPLAYER_ATTRIBUTE_PREFIX = 'multiplayer.'
|
21
|
+
|
22
|
+
ATTR_MULTIPLAYER_WORKSPACE_ID = 'multiplayer.workspace.id'
|
23
|
+
|
24
|
+
ATTR_MULTIPLAYER_PROJECT_ID = 'multiplayer.project.id'
|
25
|
+
|
26
|
+
ATTR_MULTIPLAYER_PLATFORM_ID = 'multiplayer.platform.id'
|
27
|
+
|
28
|
+
ATTR_MULTIPLAYER_CONTINUOUS_SESSION_AUTO_SAVE = 'multiplayer.session.auto-save'
|
29
|
+
|
30
|
+
ATTR_MULTIPLAYER_CONTINUOUS_SESSION_AUTO_SAVE_REASON = 'multiplayer.session.auto-save.reason'
|
31
|
+
|
32
|
+
ATTR_MULTIPLAYER_PLATFORM_NAME = 'multiplayer.platform.name'
|
33
|
+
|
34
|
+
ATTR_MULTIPLAYER_CLIENT_ID = 'multiplayer.client.id'
|
35
|
+
|
36
|
+
ATTR_MULTIPLAYER_INTEGRATION_ID = 'multiplayer.integration.id'
|
37
|
+
|
38
|
+
ATTR_MULTIPLAYER_SESSION_ID = 'multiplayer.session.id'
|
39
|
+
|
40
|
+
ATTR_MULTIPLAYER_HTTP_PROXY = 'multiplayer.http.proxy'
|
41
|
+
|
42
|
+
ATTR_MULTIPLAYER_HTTP_PROXY_TYPE = 'multiplayer.http.proxy.type'
|
43
|
+
|
44
|
+
ATTR_MULTIPLAYER_HTTP_REQUEST_BODY = 'multiplayer.http.request.body'
|
45
|
+
|
46
|
+
ATTR_MULTIPLAYER_HTTP_RESPONSE_BODY = 'multiplayer.http.response.body'
|
47
|
+
|
48
|
+
ATTR_MULTIPLAYER_HTTP_REQUEST_HEADERS = 'multiplayer.http.request.headers'
|
49
|
+
|
50
|
+
ATTR_MULTIPLAYER_HTTP_RESPONSE_HEADERS = 'multiplayer.http.response.headers'
|
51
|
+
|
52
|
+
ATTR_MULTIPLAYER_HTTP_RESPONSE_BODY_ENCODING = 'multiplayer.http.response.body.encoding'
|
53
|
+
|
54
|
+
ATTR_MULTIPLAYER_RPC_REQUEST_MESSAGE = 'multiplayer.rpc.request.message'
|
55
|
+
|
56
|
+
ATTR_MULTIPLAYER_RPC_REQUEST_MESSAGE_ENCODING = 'multiplayer.rpc.request.message.encoding'
|
57
|
+
|
58
|
+
ATTR_MULTIPLAYER_RPC_RESPONSE_MESSAGE = 'multiplayer.rpc.response.message'
|
59
|
+
|
60
|
+
ATTR_MULTIPLAYER_GRPC_REQUEST_MESSAGE = 'multiplayer.rpc.grpc.request.message'
|
61
|
+
|
62
|
+
ATTR_MULTIPLAYER_GRPC_REQUEST_MESSAGE_ENCODING = 'multiplayer.rpc.request.message.encoding'
|
63
|
+
|
64
|
+
ATTR_MULTIPLAYER_GRPC_RESPONSE_MESSAGE = 'multiplayer.rpc.grpc.response.message'
|
65
|
+
|
66
|
+
ATTR_MULTIPLAYER_MESSAGING_MESSAGE_BODY = 'multiplayer.messaging.message.body'
|
67
|
+
|
68
|
+
ATTR_MULTIPLAYER_MESSAGING_MESSAGE_BODY_ENCODING = 'multiplayer.messaging.message.body.encoding'
|
69
|
+
|
70
|
+
ATTR_MULTIPLAYER_SESSION_RECORDER_VERSION = 'multiplayer.session-recorder.version'
|
71
|
+
|
72
|
+
MASK_PLACEHOLDER = '***MASKED***'
|
73
|
+
|
74
|
+
# Middleware constants
|
75
|
+
MAX_MASK_DEPTH = 10
|
76
|
+
|
77
|
+
# HTTP request/response size limits
|
78
|
+
MULTIPLAYER_MAX_HTTP_REQUEST_RESPONSE_SIZE = 1024 * 1024 # 1MB
|
79
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "opentelemetry/sdk"
|
5
|
+
require "opentelemetry/exporter/otlp"
|
6
|
+
|
7
|
+
module SessionRecorder
|
8
|
+
module Exporters
|
9
|
+
class SessionRecorderGrpcLogsExporter < OpenTelemetry::Exporter::OTLP::Exporter
|
10
|
+
def initialize(config = {})
|
11
|
+
endpoint = config[:endpoint] || SessionRecorder::MULTIPLAYER_OTEL_DEFAULT_LOGS_EXPORTER_GRPC_URL
|
12
|
+
api_key = config[:api_key]
|
13
|
+
|
14
|
+
raise ArgumentError, "api_key is required" if api_key.nil? || api_key.empty?
|
15
|
+
|
16
|
+
headers = { "Authorization" => api_key }
|
17
|
+
headers.merge!(config[:headers]) if config[:headers]
|
18
|
+
|
19
|
+
super(endpoint: endpoint, headers: headers)
|
20
|
+
end
|
21
|
+
|
22
|
+
def export(logs, timeout: nil)
|
23
|
+
# Filter logs by trace ID prefix
|
24
|
+
filtered_logs = logs.select do |log|
|
25
|
+
trace_id = log.trace_id
|
26
|
+
trace_id.start_with?(SessionRecorder::MULTIPLAYER_TRACE_DEBUG_PREFIX) ||
|
27
|
+
trace_id.start_with?(SessionRecorder::MULTIPLAYER_TRACE_CONTINUOUS_DEBUG_PREFIX)
|
28
|
+
end
|
29
|
+
|
30
|
+
return OpenTelemetry::SDK::Trace::Export::SUCCESS if filtered_logs.empty?
|
31
|
+
|
32
|
+
super(filtered_logs, timeout: timeout)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "opentelemetry/sdk"
|
5
|
+
require "opentelemetry/exporter/otlp"
|
6
|
+
|
7
|
+
module SessionRecorder
|
8
|
+
module Exporters
|
9
|
+
class SessionRecorderGrpcTraceExporter < OpenTelemetry::Exporter::OTLP::Exporter
|
10
|
+
def initialize(config = {})
|
11
|
+
endpoint = config[:endpoint] || SessionRecorder::MULTIPLAYER_OTEL_DEFAULT_TRACES_EXPORTER_GRPC_URL
|
12
|
+
api_key = config[:api_key]
|
13
|
+
|
14
|
+
raise ArgumentError, "api_key is required" if api_key.nil? || api_key.empty?
|
15
|
+
|
16
|
+
headers = { "Authorization" => api_key }
|
17
|
+
headers.merge!(config[:headers]) if config[:headers]
|
18
|
+
|
19
|
+
super(endpoint: endpoint, headers: headers)
|
20
|
+
end
|
21
|
+
|
22
|
+
def export(spans, timeout: nil)
|
23
|
+
# Filter spans by trace ID prefix
|
24
|
+
filtered_spans = spans.select do |span|
|
25
|
+
trace_id = span.trace_id
|
26
|
+
trace_id.start_with?(SessionRecorder::MULTIPLAYER_TRACE_DEBUG_PREFIX) ||
|
27
|
+
trace_id.start_with?(SessionRecorder::MULTIPLAYER_TRACE_CONTINUOUS_DEBUG_PREFIX)
|
28
|
+
end
|
29
|
+
|
30
|
+
return OpenTelemetry::SDK::Trace::Export::SUCCESS if filtered_spans.empty?
|
31
|
+
|
32
|
+
super(filtered_spans, timeout: timeout)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "opentelemetry/sdk"
|
5
|
+
require "opentelemetry/exporter/otlp"
|
6
|
+
|
7
|
+
module SessionRecorder
|
8
|
+
module Exporters
|
9
|
+
class SessionRecorderHttpLogsExporter < OpenTelemetry::Exporter::OTLP::Exporter
|
10
|
+
def initialize(config = {})
|
11
|
+
endpoint = config[:endpoint] || SessionRecorder::MULTIPLAYER_OTEL_DEFAULT_LOGS_EXPORTER_HTTP_URL
|
12
|
+
api_key = config[:api_key]
|
13
|
+
|
14
|
+
raise ArgumentError, "api_key is required" if api_key.nil? || api_key.empty?
|
15
|
+
|
16
|
+
headers = { "Authorization" => api_key }
|
17
|
+
headers.merge!(config[:headers]) if config[:headers]
|
18
|
+
|
19
|
+
super(endpoint: endpoint, headers: headers)
|
20
|
+
end
|
21
|
+
|
22
|
+
def export(logs, timeout: nil)
|
23
|
+
# Filter logs by trace ID prefix
|
24
|
+
filtered_logs = logs.select do |log|
|
25
|
+
trace_id = log.trace_id
|
26
|
+
trace_id.start_with?(SessionRecorder::MULTIPLAYER_TRACE_DEBUG_PREFIX) ||
|
27
|
+
trace_id.start_with?(SessionRecorder::MULTIPLAYER_TRACE_CONTINUOUS_DEBUG_PREFIX)
|
28
|
+
end
|
29
|
+
|
30
|
+
return OpenTelemetry::SDK::Trace::Export::SUCCESS if filtered_logs.empty?
|
31
|
+
|
32
|
+
super(filtered_logs, timeout: timeout)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "opentelemetry/sdk"
|
5
|
+
require "opentelemetry/exporter/otlp"
|
6
|
+
|
7
|
+
module SessionRecorder
|
8
|
+
module Exporters
|
9
|
+
class SessionRecorderHttpTraceExporter < OpenTelemetry::Exporter::OTLP::Exporter
|
10
|
+
def initialize(config = {})
|
11
|
+
endpoint = config[:endpoint] || SessionRecorder::MULTIPLAYER_OTEL_DEFAULT_TRACES_EXPORTER_HTTP_URL
|
12
|
+
api_key = config[:api_key]
|
13
|
+
|
14
|
+
raise ArgumentError, "api_key is required" if api_key.nil? || api_key.empty?
|
15
|
+
|
16
|
+
headers = { "Authorization" => api_key }
|
17
|
+
headers.merge!(config[:headers]) if config[:headers]
|
18
|
+
|
19
|
+
super(endpoint: endpoint, headers: headers)
|
20
|
+
end
|
21
|
+
|
22
|
+
def export(spans, timeout: nil)
|
23
|
+
# Filter spans by trace ID prefix
|
24
|
+
filtered_spans = spans.select do |span|
|
25
|
+
trace_id = span.trace_id
|
26
|
+
trace_id.start_with?(SessionRecorder::MULTIPLAYER_TRACE_DEBUG_PREFIX) ||
|
27
|
+
trace_id.start_with?(SessionRecorder::MULTIPLAYER_TRACE_CONTINUOUS_DEBUG_PREFIX)
|
28
|
+
end
|
29
|
+
|
30
|
+
return OpenTelemetry::SDK::Trace::Export::SUCCESS if filtered_spans.empty?
|
31
|
+
|
32
|
+
super(filtered_spans, timeout: timeout)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "opentelemetry/sdk"
|
4
|
+
|
5
|
+
module SessionRecorder
|
6
|
+
module Exporters
|
7
|
+
class SessionRecorderLogsExporterWrapper
|
8
|
+
def initialize(exporter)
|
9
|
+
@exporter = exporter
|
10
|
+
end
|
11
|
+
|
12
|
+
def export(logs, timeout: nil)
|
13
|
+
# Filter out multiplayer attributes from logs
|
14
|
+
filtered_logs = logs.map do |log|
|
15
|
+
filtered_log = log.dup
|
16
|
+
filtered_log[:attributes] = filter_attributes(log[:attributes])
|
17
|
+
filtered_log
|
18
|
+
end
|
19
|
+
|
20
|
+
@exporter.export(filtered_logs, timeout: timeout)
|
21
|
+
end
|
22
|
+
|
23
|
+
def shutdown(timeout: nil)
|
24
|
+
@exporter.shutdown(timeout: timeout)
|
25
|
+
end
|
26
|
+
|
27
|
+
def force_flush(timeout: nil)
|
28
|
+
@exporter.force_flush(timeout: timeout)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def filter_attributes(attributes)
|
34
|
+
return {} if attributes.nil?
|
35
|
+
|
36
|
+
attributes.each_with_object({}) do |(key, value), filtered|
|
37
|
+
unless key.to_s.start_with?(SessionRecorder::MULTIPLAYER_ATTRIBUTE_PREFIX)
|
38
|
+
filtered[key] = value
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "opentelemetry/sdk"
|
4
|
+
|
5
|
+
module SessionRecorder
|
6
|
+
module Exporters
|
7
|
+
class SessionRecorderTraceExporterWrapper
|
8
|
+
def initialize(exporter)
|
9
|
+
@exporter = exporter
|
10
|
+
end
|
11
|
+
|
12
|
+
def export(spans, timeout: nil)
|
13
|
+
# Filter out multiplayer attributes from spans
|
14
|
+
filtered_spans = spans.map do |span|
|
15
|
+
filtered_span = span.dup
|
16
|
+
filtered_span[:attributes] = filter_attributes(span[:attributes])
|
17
|
+
filtered_span
|
18
|
+
end
|
19
|
+
|
20
|
+
@exporter.export(filtered_spans, timeout: timeout)
|
21
|
+
end
|
22
|
+
|
23
|
+
def shutdown(timeout: nil)
|
24
|
+
@exporter.shutdown(timeout: timeout)
|
25
|
+
end
|
26
|
+
|
27
|
+
def force_flush(timeout: nil)
|
28
|
+
@exporter.force_flush(timeout: timeout)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def filter_attributes(attributes)
|
34
|
+
return {} if attributes.nil?
|
35
|
+
|
36
|
+
attributes.each_with_object({}) do |(key, value), filtered|
|
37
|
+
unless key.to_s.start_with?(SessionRecorder::MULTIPLAYER_ATTRIBUTE_PREFIX)
|
38
|
+
filtered[key] = value
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "exporters/http_trace_exporter"
|
4
|
+
require_relative "exporters/http_logs_exporter"
|
5
|
+
require_relative "exporters/grpc_trace_exporter"
|
6
|
+
require_relative "exporters/grpc_logs_exporter"
|
7
|
+
require_relative "exporters/trace_exporter_wrapper"
|
8
|
+
require_relative "exporters/logs_exporter_wrapper"
|
9
|
+
|
10
|
+
module SessionRecorder
|
11
|
+
module Exporters
|
12
|
+
# Convenience method to create HTTP trace exporter
|
13
|
+
def self.create_http_trace_exporter(config = {})
|
14
|
+
SessionRecorderHttpTraceExporter.new(config)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Convenience method to create HTTP logs exporter
|
18
|
+
def self.create_http_logs_exporter(config = {})
|
19
|
+
SessionRecorderHttpLogsExporter.new(config)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Convenience method to create gRPC trace exporter
|
23
|
+
def self.create_grpc_trace_exporter(config = {})
|
24
|
+
SessionRecorderGrpcTraceExporter.new(config)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Convenience method to create gRPC logs exporter
|
28
|
+
def self.create_grpc_logs_exporter(config = {})
|
29
|
+
SessionRecorderGrpcLogsExporter.new(config)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Convenience method to create trace exporter wrapper
|
33
|
+
def self.create_trace_exporter_wrapper(exporter)
|
34
|
+
SessionRecorderTraceExporterWrapper.new(exporter)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Convenience method to create logs exporter wrapper
|
38
|
+
def self.create_logs_exporter_wrapper(exporter)
|
39
|
+
SessionRecorderLogsExporterWrapper.new(exporter)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module SessionRecorder
|
6
|
+
class Middleware
|
7
|
+
attr_reader :mask_body, :mask_headers, :capture_headers, :capture_body,
|
8
|
+
:is_mask_body_enabled, :is_mask_headers_enabled, :max_payload_size_bytes
|
9
|
+
|
10
|
+
def initialize(app, options = {})
|
11
|
+
@app = app
|
12
|
+
|
13
|
+
# Masking functions
|
14
|
+
@mask_body = options[:maskBody] || method(:default_mask_body)
|
15
|
+
@mask_headers = options[:maskHeaders] || method(:default_mask_headers)
|
16
|
+
|
17
|
+
# Capture flags
|
18
|
+
@capture_headers = options.fetch(:captureHeaders, true)
|
19
|
+
@capture_body = options.fetch(:captureBody, true)
|
20
|
+
|
21
|
+
# Masking flags
|
22
|
+
@is_mask_body_enabled = options.fetch(:isMaskBodyEnabled, true)
|
23
|
+
@is_mask_headers_enabled = options.fetch(:isMaskHeadersEnabled, true)
|
24
|
+
|
25
|
+
# Payload size limit
|
26
|
+
@max_payload_size_bytes = options[:maxPayloadSizeBytes] || SessionRecorder::MULTIPLAYER_MAX_HTTP_REQUEST_RESPONSE_SIZE
|
27
|
+
end
|
28
|
+
|
29
|
+
protected
|
30
|
+
|
31
|
+
def default_mask_body(value)
|
32
|
+
return SessionRecorder::MASK_PLACEHOLDER unless @is_mask_body_enabled
|
33
|
+
|
34
|
+
payload_json = begin
|
35
|
+
JSON.parse(value)
|
36
|
+
rescue JSON::ParserError
|
37
|
+
value
|
38
|
+
end
|
39
|
+
|
40
|
+
masked_data = mask_primitives(payload_json)
|
41
|
+
|
42
|
+
unless masked_data.is_a?(String)
|
43
|
+
masked_data = masked_data.to_json
|
44
|
+
end
|
45
|
+
|
46
|
+
masked_data
|
47
|
+
end
|
48
|
+
|
49
|
+
def default_mask_headers(headers, custom_header_names_to_mask = [])
|
50
|
+
return headers unless @is_mask_headers_enabled
|
51
|
+
|
52
|
+
default_header_names_to_mask = ["set-cookie", "cookie", "authorization", "proxy-authorization"]
|
53
|
+
masked_headers = headers.dup
|
54
|
+
headers_to_mask = default_header_names_to_mask + custom_header_names_to_mask
|
55
|
+
|
56
|
+
headers_to_mask.each do |header_name|
|
57
|
+
masked_headers[header_name] = SessionRecorder::MASK_PLACEHOLDER if masked_headers.key?(header_name)
|
58
|
+
end
|
59
|
+
|
60
|
+
masked_headers
|
61
|
+
end
|
62
|
+
|
63
|
+
def mask_primitives(input, current_depth = 0)
|
64
|
+
return SessionRecorder::MASK_PLACEHOLDER if current_depth >= SessionRecorder::MAX_MASK_DEPTH
|
65
|
+
|
66
|
+
case input
|
67
|
+
when Hash
|
68
|
+
input.transform_values { |value| mask_primitives(value, current_depth + 1) }
|
69
|
+
when Array
|
70
|
+
input.map { |value| mask_primitives(value, current_depth + 1) }
|
71
|
+
when String, Numeric, TrueClass, FalseClass, NilClass, Symbol
|
72
|
+
SessionRecorder::MASK_PLACEHOLDER
|
73
|
+
else
|
74
|
+
input
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Extracts request headers from the Rack environment
|
79
|
+
def extract_request_headers(env)
|
80
|
+
return {} unless @capture_headers
|
81
|
+
|
82
|
+
env.select { |k, _| k.start_with?("HTTP_") }
|
83
|
+
.transform_keys { |k| k.sub(/^HTTP_/, "").split("_").map(&:downcase).join("-") }
|
84
|
+
end
|
85
|
+
|
86
|
+
# Reads the request body safely
|
87
|
+
def extract_request_body(request)
|
88
|
+
return nil unless @capture_body
|
89
|
+
|
90
|
+
body = request.body.read
|
91
|
+
request.body.rewind # Rewind the stream for downstream middlewares
|
92
|
+
body
|
93
|
+
end
|
94
|
+
|
95
|
+
# Reads the response body safely
|
96
|
+
def extract_response_body(response)
|
97
|
+
return nil unless @capture_body
|
98
|
+
|
99
|
+
body = []
|
100
|
+
response.each { |part| body << part }
|
101
|
+
body.join
|
102
|
+
end
|
103
|
+
|
104
|
+
# Truncates payload if maxPayloadSizeBytes is set
|
105
|
+
def truncate_if_needed(data)
|
106
|
+
return data unless data.to_s.size > @max_payload_size_bytes
|
107
|
+
"#{data[0...@max_payload_size_bytes]}...[TRUNCATED]"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SessionRecorder
|
4
|
+
class RequestMiddleware < SessionRecorder::Middleware
|
5
|
+
def call(env)
|
6
|
+
current_span = OpenTelemetry::Trace.current_span
|
7
|
+
trace_id = current_span.context.trace_id.unpack1("H*")
|
8
|
+
request = Rack::Request.new(env)
|
9
|
+
|
10
|
+
request_headers = extract_request_headers(env)
|
11
|
+
masked_headers = @mask_headers.call(request_headers, current_span)
|
12
|
+
current_span.set_attribute(SessionRecorder::ATTR_MULTIPLAYER_HTTP_REQUEST_HEADERS, masked_headers.to_json)
|
13
|
+
|
14
|
+
if request_headers.key?("content-type") && request_headers["content-type"] == "application/json"
|
15
|
+
request_body = extract_request_body(request)
|
16
|
+
|
17
|
+
if request_body
|
18
|
+
masked_body = @mask_body.call(request_body, current_span)
|
19
|
+
masked_body = truncate_if_needed(masked_body)
|
20
|
+
|
21
|
+
current_span.set_attribute(SessionRecorder::ATTR_MULTIPLAYER_HTTP_REQUEST_BODY,
|
22
|
+
masked_body.is_a?(String) ? masked_body : masked_body.to_json)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
@app.call(env)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SessionRecorder
|
4
|
+
class ResponseMiddleware < SessionRecorder::Middleware
|
5
|
+
def call(env)
|
6
|
+
status, headers, response = @app.call(env)
|
7
|
+
|
8
|
+
begin
|
9
|
+
current_span = OpenTelemetry::Trace.current_span
|
10
|
+
trace_id = current_span.context.trace_id.unpack1("H*")
|
11
|
+
|
12
|
+
headers["x-trace-id"] = trace_id
|
13
|
+
|
14
|
+
masked_headers = @mask_headers.call(headers, current_span)
|
15
|
+
response_body = extract_response_body(response)
|
16
|
+
|
17
|
+
if response_body
|
18
|
+
masked_body = @mask_body.call(response_body, current_span)
|
19
|
+
masked_body = truncate_if_needed(masked_body)
|
20
|
+
|
21
|
+
current_span.set_attribute(SessionRecorder::ATTR_MULTIPLAYER_HTTP_RESPONSE_HEADERS, masked_headers.to_json)
|
22
|
+
current_span.set_attribute(SessionRecorder::ATTR_MULTIPLAYER_HTTP_RESPONSE_BODY,
|
23
|
+
masked_body.is_a?(String) ? masked_body : masked_body.to_json)
|
24
|
+
end
|
25
|
+
|
26
|
+
[status, headers, response]
|
27
|
+
rescue
|
28
|
+
[status, headers, response]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "opentelemetry-sdk"
|
4
|
+
require_relative "../type/session_type"
|
5
|
+
|
6
|
+
module SessionRecorder
|
7
|
+
module Trace
|
8
|
+
class SessionRecorderIdGenerator
|
9
|
+
include OpenTelemetry::Trace
|
10
|
+
|
11
|
+
attr_accessor :session_short_id, :session_type
|
12
|
+
attr_reader :generate_long_id, :generate_short_id
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@generate_long_id = self.class.get_id_generator(16)
|
16
|
+
@generate_short_id = self.class.get_id_generator(8)
|
17
|
+
@session_short_id = ''
|
18
|
+
@session_type = SessionType::PLAIN
|
19
|
+
end
|
20
|
+
|
21
|
+
def generate_trace_id
|
22
|
+
trace_id = @generate_long_id.call
|
23
|
+
|
24
|
+
if @session_short_id && !@session_short_id.empty?
|
25
|
+
session_type_prefix = case @session_type
|
26
|
+
when SessionType::CONTINUOUS
|
27
|
+
SessionRecorder::MULTIPLAYER_TRACE_CONTINUOUS_DEBUG_PREFIX
|
28
|
+
else
|
29
|
+
SessionRecorder::MULTIPLAYER_TRACE_DEBUG_PREFIX
|
30
|
+
end
|
31
|
+
|
32
|
+
prefix = "#{session_type_prefix}#{@session_short_id}"
|
33
|
+
session_trace_id = "#{prefix}#{trace_id[prefix.length..-1]}"
|
34
|
+
|
35
|
+
return session_trace_id
|
36
|
+
end
|
37
|
+
|
38
|
+
trace_id
|
39
|
+
end
|
40
|
+
|
41
|
+
def generate_span_id
|
42
|
+
@generate_short_id.call
|
43
|
+
end
|
44
|
+
|
45
|
+
def set_session_id(session_short_id, session_type = SessionType::PLAIN)
|
46
|
+
@session_short_id = session_short_id
|
47
|
+
@session_type = session_type
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def self.get_id_generator(bytes)
|
53
|
+
lambda do
|
54
|
+
(0...(bytes * 2)).map do |i|
|
55
|
+
char_code = rand(16) + 48
|
56
|
+
# valid hex characters in the range 48-57 and 97-102
|
57
|
+
if char_code >= 58
|
58
|
+
char_code += 39
|
59
|
+
end
|
60
|
+
char_code.chr
|
61
|
+
end.join
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "opentelemetry-sdk"
|
4
|
+
|
5
|
+
module SessionRecorder
|
6
|
+
module Trace
|
7
|
+
class TraceIdRatioBasedSampler < OpenTelemetry::SDK::Trace::Samplers::TraceIdRatioBased
|
8
|
+
def initialize(ratio = 0)
|
9
|
+
@ratio = normalize(ratio)
|
10
|
+
@upper_bound = (@ratio * 0xffffffff).floor
|
11
|
+
super(@ratio)
|
12
|
+
end
|
13
|
+
|
14
|
+
def should_sample?(trace_id:, parent_context:, links:, name:, kind:, attributes:)
|
15
|
+
tracestate = OpenTelemetry::Trace.current_span(parent_context).context.tracestate
|
16
|
+
|
17
|
+
# Convert trace_id to hex string for comparison
|
18
|
+
trace_id_hex = trace_id.unpack1("H*")
|
19
|
+
|
20
|
+
# Always sample if trace ID begins with debug prefixes
|
21
|
+
if trace_id_hex.start_with?(SessionRecorder::MULTIPLAYER_TRACE_DEBUG_PREFIX) ||
|
22
|
+
trace_id_hex.start_with?(SessionRecorder::MULTIPLAYER_TRACE_CONTINUOUS_DEBUG_PREFIX)
|
23
|
+
return OpenTelemetry::SDK::Trace::Samplers::Result.new(
|
24
|
+
decision: OpenTelemetry::SDK::Trace::Samplers::Decision::RECORD_AND_SAMPLE,
|
25
|
+
tracestate: tracestate
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
# For all other trace IDs, use the provided sampling ratio
|
30
|
+
if valid_trace_id?(trace_id_hex) && accumulate(trace_id_hex) < @upper_bound
|
31
|
+
OpenTelemetry::SDK::Trace::Samplers::Result.new(
|
32
|
+
decision: OpenTelemetry::SDK::Trace::Samplers::Decision::RECORD_AND_SAMPLE,
|
33
|
+
tracestate: tracestate
|
34
|
+
)
|
35
|
+
else
|
36
|
+
OpenTelemetry::SDK::Trace::Samplers::Result.new(
|
37
|
+
decision: OpenTelemetry::SDK::Trace::Samplers::Decision::DROP,
|
38
|
+
tracestate: tracestate
|
39
|
+
)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def description
|
44
|
+
"SessionRecorderTraceIdRatioBasedSampler{ratio=#{@ratio}}"
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def valid_trace_id?(trace_id)
|
50
|
+
trace_id.is_a?(String) && trace_id.length == 32 && trace_id =~ /^([0-9a-f]{32})$/i
|
51
|
+
end
|
52
|
+
|
53
|
+
def normalize(ratio)
|
54
|
+
return 0 unless defined?(ratio) && ratio.is_a?(Numeric)
|
55
|
+
return 1 if ratio >= 1
|
56
|
+
return 0 if ratio <= 0
|
57
|
+
|
58
|
+
ratio
|
59
|
+
end
|
60
|
+
|
61
|
+
def accumulate(trace_id)
|
62
|
+
accumulation = 0
|
63
|
+
(0...trace_id.length / 8).each do |i|
|
64
|
+
pos = i * 8
|
65
|
+
part = trace_id[pos, 8].to_i(16)
|
66
|
+
accumulation = (accumulation ^ part) & 0xffffffff
|
67
|
+
end
|
68
|
+
accumulation
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
metadata
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: multiplayer-session-recorder
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.6
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Multiplayer Inc.
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-09-02 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
|
+
description:
|
42
|
+
email:
|
43
|
+
executables: []
|
44
|
+
extensions: []
|
45
|
+
extra_rdoc_files: []
|
46
|
+
files:
|
47
|
+
- lib/session-recorder.rb
|
48
|
+
- lib/session_recorder/constants.rb
|
49
|
+
- lib/session_recorder/exporters.rb
|
50
|
+
- lib/session_recorder/exporters/grpc_logs_exporter.rb
|
51
|
+
- lib/session_recorder/exporters/grpc_trace_exporter.rb
|
52
|
+
- lib/session_recorder/exporters/http_logs_exporter.rb
|
53
|
+
- lib/session_recorder/exporters/http_trace_exporter.rb
|
54
|
+
- lib/session_recorder/exporters/logs_exporter_wrapper.rb
|
55
|
+
- lib/session_recorder/exporters/trace_exporter_wrapper.rb
|
56
|
+
- lib/session_recorder/middleware/middleware.rb
|
57
|
+
- lib/session_recorder/middleware/request_middleware.rb
|
58
|
+
- lib/session_recorder/middleware/response_middleware.rb
|
59
|
+
- lib/session_recorder/trace/id_generator.rb
|
60
|
+
- lib/session_recorder/trace/trace_id_ratio_based_sampler.rb
|
61
|
+
- lib/session_recorder/type/session_type.rb
|
62
|
+
- lib/session_recorder/version.rb
|
63
|
+
homepage: https://rubygems.org/gems/multiplayer_session_recorder
|
64
|
+
licenses:
|
65
|
+
- MIT
|
66
|
+
metadata: {}
|
67
|
+
post_install_message:
|
68
|
+
rdoc_options: []
|
69
|
+
require_paths:
|
70
|
+
- lib
|
71
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '3.0'
|
76
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - ">="
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '0'
|
81
|
+
requirements: []
|
82
|
+
rubygems_version: 3.5.22
|
83
|
+
signing_key:
|
84
|
+
specification_version: 4
|
85
|
+
summary: Multiplayer Fullstack Session Recorder
|
86
|
+
test_files: []
|