traces 0.15.2 → 0.18.2

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: 6cba8b363fbd6a0df7f971924f3b8d3f358bc5b91ea2eb57045716285081fc35
4
- data.tar.gz: 612777384a90dbc375f1a349e8291cabd7bb8ebf77830075d29703e54d5f4d60
3
+ metadata.gz: 53f12f0b9d29fc4be9a9e701a4946bdf3ad74c226e3ba6059fccab724625c02d
4
+ data.tar.gz: 02b432f57ec9f3b2ddd2c0b793d490db2e5c4be827baeb63bee26c6e1d0a19ed
5
5
  SHA512:
6
- metadata.gz: 162910bc810cdb69c55e6bb59354dd7b0859237a4cc47c5d1a4ce4ccb6cafee24913680d9f1d815540b4524941eb9fd8bcda7cd49bebcd3fbdac534708cb828e
7
- data.tar.gz: 4e5432869b0470ae85fbee9d6efbc57130b07944f5960eb7ee4832a848068caf56445b5846434075206111262f17b12ce92564417ff56d90c3162ee5338f0bbc
6
+ metadata.gz: 36710faff7e4ba0a5f22318d5e74304d92ccd8f82761f89fc7c75608bbbba81e7a221996195c1eda59f37c569a3f0ed0fb6c54fd0bdd9088c01508fce0a4ab8f
7
+ data.tar.gz: ee106e0da529d689f268c4a92f8bc7dacfb2103fb13f15d55577c826c6ae07b495c85fd47ea67812179437074f82f0bac4affc1f836aa75e402c053295e6c550
checksums.yaml.gz.sig CHANGED
@@ -1,3 +1 @@
1
- ��ބ�U!��ܰ} ��2q�>��
2
- �)Ϸ��'A�݉�>H0t���@&/�/��kW?�$q�a�a,�()k���r��"�/9)�m�p�uNG)�uhˊ�~#�CȈ��U�Ԝg�%w͹��5���$��v����p�nUQ��.MT�7���%�`���:��'��B�a+����M���� ����{�0�T a�2.:�X�1���'�6��}ٱ�#���/#Ԭ�_y�j�]QU7A��"��͘�Q~�{B��v�O�9cw�y�w�������U��-�a��%2k.A-\t���s���
3
- "�����Z�M���
1
+ BK �m�Aᨿ��$Χ��xf/v��ls�,NPe��!^�E���Y�5|ީȀ�Ԯ,��%�?.T�u�Kɥ�sj��� Ĕ� 2Mg΅ܖ�=K���3�d�MYl���7�q'��~채�ϴu���޾�I���s献H��`s���D��exC��R��KX���6�AsB���H%�3�=[S�Jn��LyH�z��H��#s�7(�:��eB�sf�Cw�N9w�g��`%o�������y��2c5Iq1%
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2023-2025, by Samuel Williams.
5
+
6
+ # Enable capturing traces.
7
+ def capture
8
+ ENV["TRACES_BACKEND"] = "traces/backend/capture"
9
+ require "traces"
10
+ end
11
+
12
+ # Generate a list of traces that have been captured.
13
+ def list
14
+ Traces::Backend::Capture.spans.sort_by!{|span| span.name}
15
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ # List available providers for traces from all loaded gems.
7
+ def list
8
+ available = {}
9
+
10
+ Gem.loaded_specs.each do |name, spec|
11
+ spec.require_paths.each do |require_path|
12
+ root = File.expand_path(require_path, spec.full_gem_path)
13
+ Dir.glob("traces/provider/**/*.rb", base: root).each do |path|
14
+ (available[name] ||= []) << path
15
+ end
16
+ end
17
+ end
18
+
19
+ return available
20
+ end
@@ -0,0 +1,39 @@
1
+ # Capture
2
+
3
+ This guide explains how to use `traces` for exporting traces from your application. This can be used to document all possible traces.
4
+
5
+ ## With Test Suite
6
+
7
+ If your application defines one or more traces and emits them as part of a test suite, you can export them using the `bake traces:capture` command.
8
+
9
+ ```bash
10
+ $ cd test/traces/backend/.capture/
11
+ $ bake traces:capture run traces:capture:list output --format json
12
+ [
13
+ {
14
+ "name": "my_trace",
15
+ "attributes": {
16
+ "foo": "baz"
17
+ },
18
+ "context": {
19
+ "trace_id": "038d110379a499a8ebcfb2b77cd69e1a",
20
+ "parent_id": "bf134b25de4f4a82",
21
+ "flags": 0,
22
+ "state": null,
23
+ "remote": false
24
+ }
25
+ },
26
+ {
27
+ "name": "nested",
28
+ "attributes": {
29
+ },
30
+ "context": {
31
+ "trace_id": "038d110379a499a8ebcfb2b77cd69e1a",
32
+ "parent_id": "2dd5510eb8fffc5f",
33
+ "flags": 0,
34
+ "state": null,
35
+ "remote": false
36
+ }
37
+ }
38
+ ]
39
+ ```
@@ -0,0 +1,212 @@
1
+ # Context Propagation
2
+
3
+ This guide explains how to propagate trace context between different execution contexts within your application using `Traces.current_context` and `Traces.with_context`.
4
+
5
+ ## Overview
6
+
7
+ The `traces` library provides two complementary approaches for managing trace context:
8
+
9
+ - **Local context propagation** (`Traces.current_context` / `Traces.with_context`): For passing context between execution contexts within the same process (threads, fibers, async tasks).
10
+ - **Distributed context propagation** (`Traces.inject` / `Traces.extract`): For transmitting context across process and service boundaries via serialization (HTTP headers, message metadata, etc.).
11
+
12
+ There is a legacy interface `Traces.trace_context` and `Traces.trace_context=` but you should prefer to use the new methods outlined above.
13
+
14
+ ## Local Context Propagation
15
+
16
+ Local context propagation involves passing trace context between different execution contexts within the same process. This is essential for maintaining trace continuity when code execution moves between threads, fibers, async tasks, or other concurrent execution contexts. Unlike distributed propagation which requires serialization over network boundaries, local propagation uses Context objects directly.
17
+
18
+ ### Capturing the Current Context
19
+
20
+ Use `Traces.current_context` to capture the current trace context as a Context object:
21
+
22
+ ~~~ ruby
23
+ current_context = Traces.current_context
24
+ # Returns a Traces::Context object or nil if no active trace
25
+ ~~~
26
+
27
+ ### Using the Context
28
+
29
+ Use `Traces.with_context(context)` to execute code within a specific trace context:
30
+
31
+ ~~~ ruby
32
+ # With block (automatic restoration):
33
+ Traces.with_context(context) do
34
+ # Code runs with the specified context.
35
+ end
36
+
37
+ # Without block (permanent switch):
38
+ Traces.with_context(context)
39
+ # Context remains active.
40
+ ~~~
41
+
42
+ ### Use Cases
43
+
44
+ #### Thread-Safe Context Propagation
45
+
46
+ When spawning background threads, you often want them to inherit the current trace context:
47
+
48
+ ~~~ ruby
49
+ require 'traces'
50
+
51
+ # Main thread has active tracing
52
+ Traces.trace("main_operation") do
53
+ # Capture current context before spawning thread:
54
+ current_context = Traces.current_context
55
+
56
+ # Spawn background thread:
57
+ Thread.new do
58
+ # Restore context in the new thread:
59
+ Traces.with_context(current_context) do
60
+ # This thread now has the same trace context as main thread:
61
+ Traces.trace("background_work") do
62
+ perform_heavy_computation
63
+ end
64
+ end
65
+ end.join
66
+ end
67
+ ~~~
68
+
69
+ #### Fiber-Based Async Operations
70
+
71
+ For fiber-based concurrency (like in async frameworks), context propagation ensures trace continuity:
72
+
73
+ ~~~ ruby
74
+ require 'traces'
75
+
76
+ Traces.trace("main_operation") do
77
+ current_context = Traces.current_context
78
+
79
+ # Create fiber for async work:
80
+ fiber = Fiber.new do
81
+ Traces.with_context(current_context) do
82
+ # Fiber inherits the trace context:
83
+ Traces.trace("fiber_work") do
84
+ perform_async_operation
85
+ end
86
+ end
87
+ end
88
+
89
+ fiber.resume
90
+ end
91
+ ~~~
92
+
93
+ ### Context Propagation vs. New Spans
94
+
95
+ Remember that context propagation maintains the same trace, while `trace()` creates new spans:
96
+
97
+ ~~~ ruby
98
+ Traces.trace("parent") do
99
+ context = Traces.current_context
100
+
101
+ Thread.new do
102
+ # This maintains the same trace context:
103
+ Traces.with_context(context) do
104
+ # This creates a NEW span within the same trace:
105
+ Traces.trace("child") do
106
+ # Child span, same trace as parent
107
+ end
108
+ end
109
+ end
110
+ end
111
+ ~~~
112
+
113
+ ## Distributed Context Propagation
114
+
115
+ Distributed context propagation involves transmitting trace context across process and service boundaries. Unlike local propagation which works within a single process, distributed propagation requires serializing context data and transmitting it over network protocols.
116
+
117
+ ### Injecting Context into Headers
118
+
119
+ Use `Traces.inject(headers, context = nil)` to add W3C Trace Context headers to a headers hash for transmission over network boundaries:
120
+
121
+ ~~~ ruby
122
+ require 'traces'
123
+
124
+ # Capture current context:
125
+ context = Traces.current_context
126
+ headers = {'Content-Type' => 'application/json'}
127
+
128
+ # Inject trace headers:
129
+ Traces.inject(headers, context)
130
+ # headers now contains: {'Content-Type' => '...', 'traceparent' => '00-...'}
131
+
132
+ # Or use current context by default:
133
+ Traces.inject(headers) # Uses current trace context
134
+ ~~~
135
+
136
+ ### Extracting Context from Headers
137
+
138
+ Use `Traces.extract(headers)` to extract trace context from W3C headers received over the network:
139
+
140
+ ~~~ ruby
141
+ # Receive headers from incoming request:
142
+ incoming_headers = request.headers
143
+
144
+ # Extract context:
145
+ context = Traces.extract(incoming_headers)
146
+ # Returns a Traces::Context object or nil if no valid context
147
+
148
+ # Use the extracted context:
149
+ if context
150
+ Traces.with_context(context) do
151
+ # Process request with distributed trace context
152
+ end
153
+ end
154
+ ~~~
155
+
156
+ ### Use Cases
157
+
158
+ #### Outgoing HTTP Requests
159
+
160
+ ~~~ ruby
161
+ require 'traces'
162
+
163
+ class ApiClient
164
+ def make_request(endpoint, data)
165
+ Traces.trace("api_request", attributes: {endpoint: endpoint}) do
166
+ headers = {
167
+ 'content-type' => 'application/json'
168
+ }
169
+
170
+ # Add trace context to outgoing request:
171
+ Traces.inject(headers)
172
+
173
+ http_client.post(endpoint,
174
+ body: data.to_json,
175
+ headers: headers
176
+ )
177
+ end
178
+ end
179
+ end
180
+ ~~~
181
+
182
+ #### Incoming HTTP Requests
183
+
184
+ ~~~ ruby
185
+ require 'traces'
186
+
187
+ class WebController
188
+ def handle_request(request)
189
+ # Extract trace context from incoming headers:
190
+ context = Traces.extract(request.headers)
191
+
192
+ # Process request with inherited context:
193
+ if context
194
+ Traces.with_context(context) do
195
+ Traces.trace("web_request", attributes: {
196
+ path: request.path,
197
+ method: request.method
198
+ }) do
199
+ process_business_logic
200
+ end
201
+ end
202
+ else
203
+ Traces.trace("web_request", attributes: {
204
+ path: request.path,
205
+ method: request.method
206
+ }) do
207
+ process_business_logic
208
+ end
209
+ end
210
+ end
211
+ end
212
+ ~~~
@@ -0,0 +1,126 @@
1
+ # Getting Started
2
+
3
+ This guide explains how to use `traces` for tracing code execution.
4
+
5
+ ## Installation
6
+
7
+ Add the gem to your project:
8
+
9
+ ~~~ bash
10
+ $ bundle add traces
11
+ ~~~
12
+
13
+ ## Core Concepts
14
+
15
+ `traces` has several core concepts:
16
+
17
+ - A {ruby Traces::Provider} which implements custom logic for wrapping existing code in traces.
18
+ - A {ruby Traces::Context} which represents the current tracing environment which can include distributed tracing.
19
+ - A {ruby Traces::Backend} which connects traces to a specific backend system for processing.
20
+
21
+ ## Usage
22
+
23
+ There are two main aspects to integrating within this gem.
24
+
25
+ 1. Libraries and applications must provide traces.
26
+ 2. Those traces must be consumed or emitted somewhere.
27
+
28
+ ### Providing Traces
29
+
30
+ Adding tracing to libraries requires the use of {ruby Traces::Provider}:
31
+
32
+ ~~~ ruby
33
+ require 'traces'
34
+
35
+ class MyClass
36
+ def my_method
37
+ puts "Hello World"
38
+ end
39
+ end
40
+
41
+ # If tracing is disabled, this is a no-op.
42
+ Traces::Provider(MyClass) do
43
+ def my_method
44
+ attributes = {
45
+ 'foo' => 'bar'
46
+ }
47
+
48
+ Traces.trace('my_method', attributes: attributes) do
49
+ super
50
+ end
51
+ end
52
+ end
53
+
54
+ MyClass.new.my_method
55
+ ~~~
56
+
57
+ This code by itself will not create any traces. In order to execute it and output traces, you must set up a backend to consume them.
58
+
59
+ In addition, to trace class methods:
60
+
61
+ ~~~ ruby
62
+ require 'traces'
63
+
64
+ class MyClass
65
+ def self.my_method
66
+ puts "Hello World"
67
+ end
68
+ end
69
+
70
+ # If tracing is disabled, this is a no-op.
71
+ Traces::Provider(MyClass.singleton_class) do
72
+ def my_method
73
+ attributes = {
74
+ 'foo' => 'bar'
75
+ }
76
+
77
+ Traces.trace('my_method', attributes: attributes) do
78
+ super
79
+ end
80
+ end
81
+ end
82
+
83
+ MyClass.my_method
84
+ ~~~
85
+
86
+ ### Consuming Traces
87
+
88
+ Consuming traces means proving a backend implementation which can emit those traces to some log or service. There are several options, but two backends are included by default:
89
+
90
+ - `traces/backend/test` does not emit any traces, but validates the usage of the tracing interface.
91
+ - `traces/backend/console` emits traces using the [`console`](https://github.com/socketry/console) gem.
92
+
93
+ In order to use a specific backend, set the `TRACES_BACKEND` environment variable, e.g.
94
+
95
+ ~~~ shell
96
+ $ TRACES_BACKEND=traces/backend/console ./my_script.rb
97
+ ~~~
98
+
99
+ Separate implementations are provided for specific APMs:
100
+
101
+ - [OpenTelemetry](https://github.com/socketry/traces-backend-open_telemetry)
102
+ - [Datadog](https://github.com/socketry/traces-backend-datadog)
103
+ - [New Relic](https://github.com/newrelic/traces-backend-newrelic)
104
+
105
+ ### Configuration
106
+
107
+ By default, you may not have many traces available, as they are typically opt-in. To enable more traces, create a `config/traces.rb` file in your project root and require the providers you want to use:
108
+
109
+ ```ruby
110
+ # config/traces.rb
111
+ def prepare
112
+ require "traces/provider/async"
113
+ require "traces/provider/async/pool"
114
+ end
115
+ ```
116
+
117
+ To get a list of all available providers, you can use the `bake` command:
118
+
119
+ ~~~ shell
120
+ $ bundle exec bake traces:provider:list
121
+ {"async" => ["traces/provider/async/barrier.rb", "traces/provider/async/task.rb", "traces/provider/async.rb"],
122
+ "async-pool" => ["traces/provider/async/pool/controller.rb"],
123
+ "protocol-http2" => ["traces/provider/protocol/http2/framer.rb", "traces/provider/protocol/http2.rb"]}
124
+ ~~~
125
+
126
+ You can then add the providers you want to use to your `config/traces.rb` file.
@@ -0,0 +1,23 @@
1
+ # Automatically generated context index for Utopia::Project guides.
2
+ # Do not edit then files in this directory directly, instead edit the guides and then run `bake utopia:project:agent:context:update`.
3
+ ---
4
+ description: Application instrumentation and tracing.
5
+ metadata:
6
+ documentation_uri: https://socketry.github.io/traces/
7
+ source_code_uri: https://github.com/socketry/traces.git
8
+ files:
9
+ - path: getting-started.md
10
+ title: Getting Started
11
+ description: This guide explains how to use `traces` for tracing code execution.
12
+ - path: context-propagation.md
13
+ title: Context Propagation
14
+ description: This guide explains how to propagate trace context between different
15
+ execution contexts within your application using `Traces.current_context` and
16
+ `Traces.with_context`.
17
+ - path: testing.md
18
+ title: Testing
19
+ description: This guide explains how to test traces in your code.
20
+ - path: capture.md
21
+ title: Capture
22
+ description: This guide explains how to use `traces` for exporting traces from your
23
+ application. This can be used to document all possible traces.
@@ -0,0 +1,32 @@
1
+ # Testing
2
+
3
+ This guide explains how to test traces in your code.
4
+
5
+ ## Expectations
6
+
7
+ One approach to testing traces are emitted, is by using mocks to verify that methods are called with the expected arguments.
8
+
9
+ ```ruby
10
+ it "should trace the operation" do
11
+ expect(Traces).to receive(:trace).with("my_controller.do_something")
12
+
13
+ my_controller.do_something
14
+ end
15
+ ```
16
+
17
+ This is generally a good appoach for testing that specific traces are emitted.
18
+
19
+ ## Validation
20
+
21
+ The traces gem supports a variety of backends, and each backend may have different requirements for the data that is submitted. The test backend is designed to be used for testing that the data submitted is valid.
22
+
23
+ ```ruby
24
+ ENV['TRACES_BACKEND'] = 'traces/backend/test'
25
+
26
+ require 'traces'
27
+
28
+ Traces.trace(5) do
29
+ puts "Hello"
30
+ end
31
+ # => lib/traces/backend/test.rb:52:in `trace': Invalid name (must be String): 5! (ArgumentError)
32
+ ```
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2023, by Samuel Williams.
4
+ # Copyright, 2023-2025, by Samuel Williams.
5
5
 
6
- require_relative '../context'
6
+ require_relative "../context"
7
7
 
8
- require 'fiber'
8
+ require "fiber"
9
9
 
10
10
  Fiber.attr_accessor :traces_backend_context
11
11
 
@@ -20,16 +20,14 @@ module Traces
20
20
  # @parameter name [String] A useful name/annotation for the recorded span.
21
21
  # @parameter resource [String] The "resource" that the span is associated with.
22
22
  # @parameter attributes [Hash] Metadata for the recorded span.
23
- def initialize(context, name, resource, attributes)
23
+ def initialize(context, name, attributes)
24
24
  @context = context
25
25
  @name = name
26
- @resource = resource
27
26
  @attributes = attributes
28
27
  end
29
28
 
30
29
  attr :context
31
30
  attr :name
32
- attr :resource
33
31
  attr :attributes
34
32
 
35
33
  # Assign some metadata to the span.
@@ -43,7 +41,6 @@ module Traces
43
41
  def as_json
44
42
  {
45
43
  name: @name,
46
- resource: @resource,
47
44
  attributes: @attributes,
48
45
  context: @context.as_json
49
46
  }
@@ -65,11 +62,11 @@ module Traces
65
62
  # Trace the given block of code and log the execution.
66
63
  # @parameter name [String] A useful name/annotation for the recorded span.
67
64
  # @parameter attributes [Hash] Metadata for the recorded span.
68
- def trace(name, resource: nil, attributes: {}, &block)
65
+ def trace(name, attributes: {}, &block)
69
66
  context = Context.nested(Fiber.current.traces_backend_context)
70
67
  Fiber.current.traces_backend_context = context
71
68
 
72
- span = Span.new(context, name, resource, attributes)
69
+ span = Span.new(context, name, attributes)
73
70
  Capture.spans << span
74
71
 
75
72
  yield span
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2021-2023, by Samuel Williams.
4
+ # Copyright, 2021-2025, by Samuel Williams.
5
5
 
6
- require_relative '../context'
6
+ require_relative "../context"
7
7
 
8
- require 'console'
9
- require 'fiber'
8
+ require "console"
9
+ require "fiber"
10
10
 
11
11
  Fiber.attr_accessor :traces_backend_context
12
12
 
@@ -40,11 +40,11 @@ module Traces
40
40
  # Trace the given block of code and log the execution.
41
41
  # @parameter name [String] A useful name/annotation for the recorded span.
42
42
  # @parameter attributes [Hash] Metadata for the recorded span.
43
- def trace(name, resource: nil, attributes: {}, &block)
43
+ def trace(name, attributes: {}, &block)
44
44
  context = Context.nested(Fiber.current.traces_backend_context)
45
45
  Fiber.current.traces_backend_context = context
46
46
 
47
- ::Console.logger.info(resource || self, name, attributes)
47
+ ::Console.logger.info(self, name, attributes)
48
48
 
49
49
  if block.arity.zero?
50
50
  yield
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2021-2023, by Samuel Williams.
4
+ # Copyright, 2021-2025, by Samuel Williams.
5
5
 
6
- require_relative '../context'
6
+ require_relative "../context"
7
7
 
8
- require 'fiber'
8
+ require "fiber"
9
9
 
10
10
  Fiber.attr_accessor :traces_backend_context
11
11
 
@@ -46,7 +46,7 @@ module Traces
46
46
  # @parameter name [String] A useful name/annotation for the recorded span.
47
47
  # @parameter resource [String] The context in which the trace operation is occuring.
48
48
  # @parameter attributes [Hash] Metadata for the recorded span.
49
- def trace(name, resource: nil, attributes: nil, &block)
49
+ def trace(name, attributes: nil, &block)
50
50
  unless block_given?
51
51
  raise ArgumentError, "No block given!"
52
52
  end
@@ -55,9 +55,6 @@ module Traces
55
55
  raise ArgumentError, "Invalid name (must be String): #{name.inspect}!"
56
56
  end
57
57
 
58
- # It should be convertable:
59
- resource &&= resource.to_s
60
-
61
58
  context = Context.nested(Fiber.current.traces_backend_context)
62
59
 
63
60
  span = Span.new(context)
@@ -88,8 +85,7 @@ module Traces
88
85
 
89
86
  # @returns [Boolean] Whether there is an active trace.
90
87
  def active?
91
- # For the sake of testing, we always enable tracing.
92
- true
88
+ !!Fiber.current.traces_backend_context
93
89
  end
94
90
  end
95
91
  end
@@ -3,24 +3,105 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2021-2025, by Samuel Williams.
5
5
 
6
- require_relative 'config'
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
@@ -33,14 +33,14 @@ module Traces
33
33
 
34
34
  # Require a specific traces backend implementation.
35
35
  def require_backend(env = ENV)
36
- if backend = env['TRACES_BACKEND']
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
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2021-2023, by Samuel Williams.
4
+ # Copyright, 2021-2025, by Samuel Williams.
5
5
 
6
- require 'securerandom'
6
+ require "securerandom"
7
7
 
8
8
  module Traces
9
9
  # A generic representation of the current tracing context.
@@ -11,25 +11,33 @@ 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)
15
- version, trace_id, parent_id, flags = parent.split('-')
14
+ def self.parse(parent, state = nil, baggage = nil, **options)
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)
21
- state = state.split(',')
21
+ state = state.split(",")
22
22
  end
23
23
 
24
24
  if state
25
- state = state.map{|item| item.split('=')}.to_h
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
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2021-2023, by Samuel Williams.
4
+ # Copyright, 2021-2025, by Samuel Williams.
5
5
 
6
- require_relative 'backend'
6
+ require_relative "backend"
7
7
 
8
8
  module Traces
9
9
  # @returns [Boolean] Whether there is an active backend.
@@ -36,8 +36,6 @@ module Traces
36
36
 
37
37
  return provider
38
38
  end
39
-
40
- Config::DEFAULT.prepare
41
39
  else
42
40
  def self.Provider(klass, &block)
43
41
  # Tracing disabled.
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2021-2023, by Samuel Williams.
4
+ # Copyright, 2021-2025, by Samuel Williams.
5
5
 
6
6
  module Traces
7
- VERSION = "0.15.2"
7
+ VERSION = "0.18.2"
8
8
  end
data/lib/traces.rb CHANGED
@@ -1,11 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2021-2022, by Samuel Williams.
4
+ # Copyright, 2021-2025, by Samuel Williams.
5
5
 
6
- require_relative 'traces/version'
7
- require_relative 'traces/provider'
6
+ require_relative "traces/version"
7
+ require_relative "traces/provider"
8
8
 
9
9
  # @namespace
10
10
  module Traces
11
+ if self.enabled?
12
+ Config::DEFAULT.prepare
13
+ end
11
14
  end
data/license.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # MIT License
2
2
 
3
- Copyright, 2021-2024, by Samuel Williams.
3
+ Copyright, 2021-2025, by Samuel Williams.
4
4
  Copyright, 2022, by Felix Yan.
5
+ Copyright, 2024, by Zach Taylor.
5
6
 
6
7
  Permission is hereby granted, free of charge, to any person obtaining a copy
7
8
  of this software and associated documentation files (the "Software"), to deal
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,23 @@ 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.1
29
+
30
+ - Don't call `prepare` in `traces/provider.rb`. It can cause circular loading warnings.
31
+
32
+ ### v0.18.0
33
+
34
+ - **W3C Baggage Support** - Full support for W3C Baggage specification for application-specific context propagation.
35
+ - [New Context Propagation Interfaces](https://socketry.github.io/traces/releases/index#new-context-propagation-interfaces)
36
+
37
+ ### v0.17.0
38
+
39
+ - Remove support for `resource:` keyword argument with no direct replacement – use an attribute instead.
40
+
41
+ ### v0.16.0
42
+
43
+ - Introduce `traces:provider:list` command to list all available trace providers.
44
+
26
45
  ### v0.14.0
27
46
 
28
47
  - [Introduce `Traces::Config` to Expose `prepare` Hook](https://socketry.github.io/traces/releases/index#introduce-traces::config-to-expose-prepare-hook)
data/releases.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Releases
2
2
 
3
+ ## v0.18.1
4
+
5
+ - Don't call `prepare` in `traces/provider.rb`. It can cause circular loading warnings.
6
+
7
+ ## v0.18.0
8
+
9
+ - **W3C Baggage Support** - Full support for W3C Baggage specification for application-specific context propagation.
10
+
11
+ ### New Context Propagation Interfaces
12
+
13
+ `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.
14
+
15
+ Therefore, in order to propagate this context, we introduce 4 new methods:
16
+
17
+ - `Traces.current_context` - Capture the current trace context for local propagation between execution contexts (threads, fibers).
18
+ - `Traces.with_context(context)` - Execute code within a specific trace context, with automatic restoration when used with blocks.
19
+ - `Traces.inject(headers = nil, context = nil)` - Inject W3C Trace Context headers into a headers hash for distributed propagation.
20
+ - `Traces.extract(headers)` - Extract trace context from W3C Trace Context headers.
21
+
22
+ 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`.
23
+
24
+ ## v0.17.0
25
+
26
+ - Remove support for `resource:` keyword argument with no direct replacement – use an attribute instead.
27
+
28
+ ## v0.16.0
29
+
30
+ - Introduce `traces:provider:list` command to list all available trace providers.
31
+
3
32
  ## v0.14.0
4
33
 
5
34
  ### Introduce `Traces::Config` to Expose `prepare` Hook
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,11 +1,12 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: traces
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.15.2
4
+ version: 0.18.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
8
8
  - Felix Yan
9
+ - Zach Taylor
9
10
  bindir: bin
10
11
  cert_chain:
11
12
  - |
@@ -37,12 +38,19 @@ cert_chain:
37
38
  Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
38
39
  voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
39
40
  -----END CERTIFICATE-----
40
- date: 2025-02-01 00:00:00.000000000 Z
41
+ date: 1980-01-02 00:00:00.000000000 Z
41
42
  dependencies: []
42
43
  executables: []
43
44
  extensions: []
44
45
  extra_rdoc_files: []
45
46
  files:
47
+ - bake/traces/capture.rb
48
+ - bake/traces/provider.rb
49
+ - context/capture.md
50
+ - context/context-propagation.md
51
+ - context/getting-started.md
52
+ - context/index.yaml
53
+ - context/testing.md
46
54
  - lib/traces.rb
47
55
  - lib/traces/backend.rb
48
56
  - lib/traces/backend/capture.rb
@@ -68,14 +76,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
68
76
  requirements:
69
77
  - - ">="
70
78
  - !ruby/object:Gem::Version
71
- version: '3.1'
79
+ version: '3.2'
72
80
  required_rubygems_version: !ruby/object:Gem::Requirement
73
81
  requirements:
74
82
  - - ">="
75
83
  - !ruby/object:Gem::Version
76
84
  version: '0'
77
85
  requirements: []
78
- rubygems_version: 3.6.2
86
+ rubygems_version: 3.6.9
79
87
  specification_version: 4
80
88
  summary: Application instrumentation and tracing.
81
89
  test_files: []
metadata.gz.sig CHANGED
Binary file