traces 0.17.0 → 0.18.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: 38ed43ffa64552bd34d59a35b041cf9e21645a00fdad969358aad2b368b669ce
4
- data.tar.gz: 0120a1379df5a3722e1ff6ba364c1686e77790e707289ab2f494fa02eb1dd03c
3
+ metadata.gz: b0ef616a2189f63b63abe00e1dc84c0d13b48dc0099c90b4c5aa5ea420ea92fa
4
+ data.tar.gz: 310a045915e9831698ca681c61d53a6175a00a9b34e44e4d9b92ff8b6917dc51
5
5
  SHA512:
6
- metadata.gz: 32e7836b442a5c2740799059a631929f29ea7345ebd86d9a7f567361263b4254d05e9bd97b2fa87fb1ab9e70f558fce5a5f78f510983c8876195d79e9f464298
7
- data.tar.gz: 27e0c6d69a3a739985cf4e82f4f0b4f3c23a4c5b754c5bf9d7844bb4cc616229590202f9ad20a93e8eab37707a384961b5b556a9acbd989c5ce09fc1c00d7d19
6
+ metadata.gz: 3d5ab804b31b9440fd4cc36af5dfb9bd553d370681417f6441e86bcaeed6a41e7b02eff4f771144fac8f3fa528930bb457a7b60b902c2fe40b26e26ea901eb1e
7
+ data.tar.gz: 35918ff43dfac6158cb42dea57942a491b58ed906d67a5de03bd0bfffaa3cbda7e4ade3edd2e3c0ce1774a2c9e8caede58c8cb97b5bcf5c2cd5a1d921dc4d7d9
checksums.yaml.gz.sig CHANGED
Binary file
@@ -85,8 +85,7 @@ module Traces
85
85
 
86
86
  # @returns [Boolean] Whether there is an active trace.
87
87
  def active?
88
- # For the sake of testing, we always enable tracing.
89
- true
88
+ !!Fiber.current.traces_backend_context
90
89
  end
91
90
  end
92
91
  end
@@ -4,23 +4,104 @@
4
4
  # Copyright, 2021-2025, by Samuel Williams.
5
5
 
6
6
  require_relative "config"
7
+ require_relative "context"
7
8
 
8
9
  module Traces
9
10
  # The backend implementation is responsible for recording and reporting traces.
10
11
  module Backend
11
12
  end
12
13
 
14
+ # Capture the current trace context for remote propagation.
15
+ #
13
16
  # This is a default implementation, which can be replaced by the backend.
17
+ #
18
+ # You should prefer to use the new `Traces.current_context` family of methods.
19
+ #
14
20
  # @returns [Object] The current trace context.
15
21
  def self.trace_context
16
22
  nil
17
23
  end
18
24
 
25
+ # Whether there is an active trace context.
26
+ #
19
27
  # This is a default implementation, which can be replaced by the backend.
28
+ #
20
29
  # @returns [Boolean] Whether there is an active trace.
21
30
  def self.active?
22
31
  !!self.trace_context
23
32
  end
24
33
 
34
+ # Capture the current trace context for local propagation between execution contexts.
35
+ #
36
+ # This method returns the current trace context that can be safely passed between threads, fibers, or other execution contexts within the same process.
37
+ #
38
+ # The returned object is opaque, in other words, you should not make assumptions about its structure.
39
+ #
40
+ # This is a default implementation, which can be replaced by the backend.
41
+ #
42
+ # @returns [Context | Nil] The current trace context, or nil if no active trace.
43
+ def self.current_context
44
+ trace_context
45
+ end
46
+
47
+ # Execute a block within a specific trace context for local execution.
48
+ #
49
+ # This method is designed for propagating trace context between execution contexts within the same process (threads, fibers, etc.). It temporarily switches to the specified trace context for the duration of the block execution, then restores the previous context.
50
+ #
51
+ # When called without a block, permanently switches to the specified context. This enables manual context management for scenarios where automatic restoration isn't desired.
52
+ #
53
+ # This is a default implementation, which can be replaced by the backend.
54
+ #
55
+ # @parameter context [Context] A trace context obtained from `Traces.current_context`.
56
+ # @yields {...} If a block is given, the block is executed within the specified trace context.
57
+ def self.with_context(context)
58
+ if block_given?
59
+ # This implementation is not ideal but the best we can do with the current interface.
60
+ previous_context = self.trace_context
61
+ begin
62
+ self.trace_context = context
63
+ yield
64
+ ensure
65
+ self.trace_context = previous_context
66
+ end
67
+ else
68
+ self.trace_context = context
69
+ end
70
+ end
71
+
72
+ # Inject trace context into a headers hash for distributed propagation.
73
+ #
74
+ # This method adds W3C Trace Context headers (traceparent, tracestate) and W3C Baggage headers to the provided headers hash, enabling distributed tracing across service boundaries. The headers hash is mutated in place.
75
+ #
76
+ # This is a default implementation, which can be replaced by the backend.
77
+ #
78
+ # @parameter headers [Hash] The headers object to mutate with trace context headers.
79
+ # @parameter context [Context] A trace context, or nil to use current context.
80
+ # @returns [Hash | Nil] The headers hash, or nil if no context is available.
81
+ def self.inject(headers = nil, context = nil)
82
+ context ||= self.trace_context
83
+
84
+ if context
85
+ headers ||= Hash.new
86
+ context.inject(headers)
87
+ else
88
+ headers = nil
89
+ end
90
+
91
+ return headers
92
+ end
93
+
94
+ # Extract trace context from headers for distributed propagation.
95
+ #
96
+ # The returned object is opaque, in other words, you should not make assumptions about its structure.
97
+ #
98
+ # This is a default implementation, which can be replaced by the backend.
99
+ #
100
+ # @parameter headers [Hash] The headers object containing trace context.
101
+ # @returns [Context, nil] The extracted trace context, or nil if no valid context found.
102
+ def self.extract(headers)
103
+ Context.extract(headers)
104
+ end
105
+
25
106
  Config::DEFAULT.require_backend
26
107
  end
data/lib/traces/config.rb CHANGED
@@ -35,12 +35,12 @@ module Traces
35
35
  def require_backend(env = ENV)
36
36
  if backend = env["TRACES_BACKEND"]
37
37
  begin
38
- if require(backend)
39
- # We ensure that the interface methods replace any existing methods by prepending the module:
40
- Traces.singleton_class.prepend(Backend::Interface)
41
-
42
- return true
43
- end
38
+ require(backend)
39
+
40
+ # We ensure that the interface methods replace any existing methods by prepending the module:
41
+ Traces.singleton_class.prepend(Backend::Interface)
42
+
43
+ return true
44
44
  rescue LoadError => error
45
45
  warn "Unable to load traces backend: #{backend.inspect}!"
46
46
  end
@@ -11,10 +11,10 @@ module Traces
11
11
  # Parse a string representation of a distributed trace.
12
12
  # @parameter parent [String] The parent trace context.
13
13
  # @parameter state [Array(String)] Any attached trace state.
14
- def self.parse(parent, state = nil, **options)
14
+ def self.parse(parent, state = nil, baggage = nil, **options)
15
15
  version, trace_id, parent_id, flags = parent.split("-")
16
16
 
17
- if version == "00"
17
+ if version == "00" && trace_id && parent_id && flags
18
18
  flags = Integer(flags, 16)
19
19
 
20
20
  if state.is_a?(String)
@@ -25,11 +25,19 @@ module Traces
25
25
  state = state.map{|item| item.split("=")}.to_h
26
26
  end
27
27
 
28
- self.new(trace_id, parent_id, flags, state, **options)
28
+ if baggage.is_a?(String)
29
+ baggage = baggage.split(",")
30
+ end
31
+
32
+ if baggage
33
+ baggage = baggage.map{|item| item.split("=")}.to_h
34
+ end
35
+
36
+ self.new(trace_id, parent_id, flags, state, baggage, **options)
29
37
  end
30
38
  end
31
39
 
32
- # Create a local trace context which is likley to be globally unique.
40
+ # Create a local trace context which is likely to be globally unique.
33
41
  # @parameter flags [Integer] Any trace context flags.
34
42
  def self.local(flags = 0, **options)
35
43
  self.new(SecureRandom.hex(16), SecureRandom.hex(8), flags, **options)
@@ -53,17 +61,18 @@ module Traces
53
61
  # @parameter flags [Integer] An 8-bit field that controls tracing flags such as sampling, trace level, etc.
54
62
  # @parameter state [Hash] Additional vendor-specific trace identification information.
55
63
  # @parameter remote [Boolean] Whether this context was created from a distributed trace header.
56
- def initialize(trace_id, parent_id, flags, state = nil, remote: false)
64
+ def initialize(trace_id, parent_id, flags, state = nil, baggage = nil, remote: false)
57
65
  @trace_id = trace_id
58
66
  @parent_id = parent_id
59
67
  @flags = flags
60
68
  @state = state
69
+ @baggage = baggage
61
70
  @remote = remote
62
71
  end
63
72
 
64
73
  # Create a new nested trace context in which spans can be recorded.
65
74
  def nested(flags = @flags)
66
- Context.new(@trace_id, SecureRandom.hex(8), flags, @state, remote: @remote)
75
+ Context.new(@trace_id, SecureRandom.hex(8), flags, @state, @baggage, remote: @remote)
67
76
  end
68
77
 
69
78
  # The ID of the whole trace forest and is used to uniquely identify a distributed trace through a system. It is represented as a 16-byte array, for example, 4bf92f3577b34da6a3ce929d0e0e4736. All bytes as zero (00000000000000000000000000000000) is considered an invalid value.
@@ -75,9 +84,12 @@ module Traces
75
84
  # An 8-bit field that controls tracing flags such as sampling, trace level, etc. These flags are recommendations given by the caller rather than strict rules.
76
85
  attr :flags
77
86
 
78
- # Provides additional vendor-specific trace identification information across different distributed tracing systems. Conveys information about the operation's position in multiple distributed tracing graphs.
87
+ # Provides additional vendor-specific trace identification information across different distributed tracing systems.
79
88
  attr :state
80
89
 
90
+ # Provides additional application-specific trace identification information across different distributed tracing systems.
91
+ attr :baggage
92
+
81
93
  # Denotes that the caller may have recorded trace data. When unset, the caller did not record trace data out-of-band.
82
94
  def sampled?
83
95
  (@flags & SAMPLED) != 0
@@ -100,6 +112,7 @@ module Traces
100
112
  parent_id: @parent_id,
101
113
  flags: @flags,
102
114
  state: @state,
115
+ baggage: @baggage,
103
116
  remote: @remote
104
117
  }
105
118
  end
@@ -108,5 +121,50 @@ module Traces
108
121
  def to_json(...)
109
122
  as_json.to_json(...)
110
123
  end
124
+
125
+ # Inject the trace context into the headers, including the `"traceparent"`, `"tracestate"`, and `"baggage"` headers.
126
+ #
127
+ # @parameter headers [Hash] The headers hash to inject the trace context into.
128
+ #
129
+ # @returns [Hash] The modified headers hash.
130
+ def inject(headers)
131
+ headers["traceparent"] = self.to_s
132
+
133
+ if @state and !@state.empty?
134
+ headers["tracestate"] = self.state.map{|key, value| "#{key}=#{value}"}.join(",")
135
+ end
136
+
137
+ if @baggage and !@baggage.empty?
138
+ headers["baggage"] = self.baggage.map{|key, value| "#{key}=#{value}"}.join(",")
139
+ end
140
+
141
+ return headers
142
+ end
143
+
144
+ # Extract the trace context from the headers.
145
+ #
146
+ # The `"traceparent"` header is a string representation of the trace context. If it is an Array, the first element is used, otherwise it is used as is.
147
+ # The `"tracestate"` header is a string representation of the trace state. If it is a String, it is split on commas before being processed.
148
+ # The `"baggage"` header is a string representation of the baggage. If it is a String, it is split on commas before being processed.
149
+ #
150
+ # @parameter headers [Hash] The headers hash containing trace context.
151
+ # @returns [Context | Nil] The extracted trace context, or nil if no valid context found.
152
+ # @raises [ArgumentError] If headers is not a Hash or contains malformed trace data.
153
+ def self.extract(headers)
154
+ if traceparent = headers["traceparent"]
155
+ if traceparent.is_a?(Array)
156
+ traceparent = traceparent.first
157
+ end
158
+
159
+ if traceparent.empty?
160
+ return nil
161
+ end
162
+
163
+ tracestate = headers["tracestate"]
164
+ baggage = headers["baggage"]
165
+
166
+ return self.parse(traceparent, tracestate, baggage, remote: true)
167
+ end
168
+ end
111
169
  end
112
170
  end
@@ -4,5 +4,5 @@
4
4
  # Copyright, 2021-2025, by Samuel Williams.
5
5
 
6
6
  module Traces
7
- VERSION = "0.17.0"
7
+ VERSION = "0.18.0"
8
8
  end
data/lib/traces.rb CHANGED
@@ -5,6 +5,7 @@
5
5
 
6
6
  require_relative "traces/version"
7
7
  require_relative "traces/provider"
8
+ require_relative "traces/context"
8
9
 
9
10
  # @namespace
10
11
  module Traces
data/readme.md CHANGED
@@ -15,6 +15,8 @@ Please see the [project documentation](https://socketry.github.io/traces/) for m
15
15
 
16
16
  - [Getting Started](https://socketry.github.io/traces/guides/getting-started/index) - This guide explains how to use `traces` for tracing code execution.
17
17
 
18
+ - [Context Propagation](https://socketry.github.io/traces/guides/context-propagation/index) - This guide explains how to propagate trace context between different execution contexts within your application using `Traces.current_context` and `Traces.with_context`.
19
+
18
20
  - [Testing](https://socketry.github.io/traces/guides/testing/index) - This guide explains how to test traces in your code.
19
21
 
20
22
  - [Capture](https://socketry.github.io/traces/guides/capture/index) - This guide explains how to use `traces` for exporting traces from your application. This can be used to document all possible traces.
@@ -23,6 +25,11 @@ Please see the [project documentation](https://socketry.github.io/traces/) for m
23
25
 
24
26
  Please see the [project releases](https://socketry.github.io/traces/releases/index) for all releases.
25
27
 
28
+ ### v0.18.0
29
+
30
+ - **W3C Baggage Support** - Full support for W3C Baggage specification for application-specific context propagation.
31
+ - [New Context Propagation Interfaces](https://socketry.github.io/traces/releases/index#new-context-propagation-interfaces)
32
+
26
33
  ### v0.17.0
27
34
 
28
35
  - Remove support for `resource:` keyword argument with no direct replacement – use an attribute instead.
data/releases.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Releases
2
2
 
3
+ ## v0.18.0
4
+
5
+ - **W3C Baggage Support** - Full support for W3C Baggage specification for application-specific context propagation.
6
+
7
+ ### New Context Propagation Interfaces
8
+
9
+ `Traces#trace_context` and `Traces.trace_context` are insufficient for efficient inter-process tracing when using OpenTelemetry. That is because OpenTelemetry has it's own "Context" concept with arbitrary key-value storage (of which the current span is one such key/value pair). Unfortunately, OpenTelemetry requires those values to be propagated "inter-process" while ignores them for "intra-process" tracing.
10
+
11
+ Therefore, in order to propagate this context, we introduce 4 new methods:
12
+
13
+ - `Traces.current_context` - Capture the current trace context for local propagation between execution contexts (threads, fibers).
14
+ - `Traces.with_context(context)` - Execute code within a specific trace context, with automatic restoration when used with blocks.
15
+ - `Traces.inject(headers = nil, context = nil)` - Inject W3C Trace Context headers into a headers hash for distributed propagation.
16
+ - `Traces.extract(headers)` - Extract trace context from W3C Trace Context headers.
17
+
18
+ The default implementation is built on top of `Traces.trace_context`, however these methods can be replaced by the backend. In that case, the `context` object is opaque, in other words it is library-specific, and you should not assume it is an instance of `Traces::Context`.
19
+
3
20
  ## v0.17.0
4
21
 
5
22
  - Remove support for `resource:` keyword argument with no direct replacement – use an attribute instead.
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: traces
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.17.0
4
+ version: 0.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
metadata.gz.sig CHANGED
Binary file