This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2021-2022, by Samuel Williams.
|
||||
|
||||
require_relative 'traces/version'
|
||||
require_relative 'traces/provider'
|
||||
|
||||
# @namespace
|
||||
module Traces
|
||||
end
|
||||
@@ -0,0 +1,26 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2021-2025, by Samuel Williams.
|
||||
|
||||
require_relative 'config'
|
||||
|
||||
module Traces
|
||||
# The backend implementation is responsible for recording and reporting traces.
|
||||
module Backend
|
||||
end
|
||||
|
||||
# This is a default implementation, which can be replaced by the backend.
|
||||
# @returns [Object] The current trace context.
|
||||
def self.trace_context
|
||||
nil
|
||||
end
|
||||
|
||||
# This is a default implementation, which can be replaced by the backend.
|
||||
# @returns [Boolean] Whether there is an active trace.
|
||||
def self.active?
|
||||
!!self.trace_context
|
||||
end
|
||||
|
||||
Config::DEFAULT.require_backend
|
||||
end
|
||||
@@ -0,0 +1,97 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2023, by Samuel Williams.
|
||||
|
||||
require_relative '../context'
|
||||
|
||||
require 'fiber'
|
||||
|
||||
Fiber.attr_accessor :traces_backend_context
|
||||
|
||||
module Traces
|
||||
module Backend
|
||||
# A backend which logs all spans to the Capture logger output.
|
||||
module Capture
|
||||
# A span which validates tag assignment.
|
||||
class Span
|
||||
# Initialize a new span.
|
||||
# @parameter context [Context] The context in which the span is recorded.
|
||||
# @parameter name [String] A useful name/annotation for the recorded span.
|
||||
# @parameter resource [String] The "resource" that the span is associated with.
|
||||
# @parameter attributes [Hash] Metadata for the recorded span.
|
||||
def initialize(context, name, resource, attributes)
|
||||
@context = context
|
||||
@name = name
|
||||
@resource = resource
|
||||
@attributes = attributes
|
||||
end
|
||||
|
||||
attr :context
|
||||
attr :name
|
||||
attr :resource
|
||||
attr :attributes
|
||||
|
||||
# Assign some metadata to the span.
|
||||
# @parameter key [String] The metadata key.
|
||||
# @parameter value [Object] The metadata value. Should be coercable to a string.
|
||||
def []= key, value
|
||||
@attributes[key] = value
|
||||
end
|
||||
|
||||
# Convert the span to a JSON representation.
|
||||
def as_json
|
||||
{
|
||||
name: @name,
|
||||
resource: @resource,
|
||||
attributes: @attributes,
|
||||
context: @context.as_json
|
||||
}
|
||||
end
|
||||
|
||||
# Convert the span to a JSON string.
|
||||
def to_json(...)
|
||||
as_json.to_json(...)
|
||||
end
|
||||
end
|
||||
|
||||
# All captured spans.
|
||||
def self.spans
|
||||
@spans ||= []
|
||||
end
|
||||
|
||||
# The capture backend interface.
|
||||
module Interface
|
||||
# Trace the given block of code and log the execution.
|
||||
# @parameter name [String] A useful name/annotation for the recorded span.
|
||||
# @parameter attributes [Hash] Metadata for the recorded span.
|
||||
def trace(name, resource: nil, attributes: {}, &block)
|
||||
context = Context.nested(Fiber.current.traces_backend_context)
|
||||
Fiber.current.traces_backend_context = context
|
||||
|
||||
span = Span.new(context, name, resource, attributes)
|
||||
Capture.spans << span
|
||||
|
||||
yield span
|
||||
end
|
||||
|
||||
# Assign a trace context to the current execution scope.
|
||||
def trace_context= context
|
||||
Fiber.current.traces_backend_context = context
|
||||
end
|
||||
|
||||
# Get a trace context from the current execution scope.
|
||||
def trace_context
|
||||
Fiber.current.traces_backend_context
|
||||
end
|
||||
|
||||
# @returns [Boolean] Whether there is an active trace.
|
||||
def active?
|
||||
!!Fiber.current.traces_backend_context
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Interface = Capture::Interface
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,75 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2021-2023, by Samuel Williams.
|
||||
|
||||
require_relative '../context'
|
||||
|
||||
require 'console'
|
||||
require 'fiber'
|
||||
|
||||
Fiber.attr_accessor :traces_backend_context
|
||||
|
||||
module Traces
|
||||
module Backend
|
||||
# A backend which logs all spans to the console logger output.
|
||||
module Console
|
||||
# A span which validates tag assignment.
|
||||
class Span
|
||||
# Initialize a new span.
|
||||
# @parameter context [Context] The context in which the span is recorded.
|
||||
# @parameter name [String] A useful name/annotation for the recorded span.
|
||||
def initialize(context, name)
|
||||
@context = context
|
||||
@name = name
|
||||
end
|
||||
|
||||
# @attribute [Context] The context in which the span is recorded.
|
||||
attr :context
|
||||
|
||||
# Assign some metadata to the span.
|
||||
# @parameter key [String] The metadata key.
|
||||
# @parameter value [Object] The metadata value. Should be coercable to a string.
|
||||
def []= key, value
|
||||
::Console.logger.info(@context, @name, "#{key} = #{value}")
|
||||
end
|
||||
end
|
||||
|
||||
# The console backend interface.
|
||||
module Interface
|
||||
# Trace the given block of code and log the execution.
|
||||
# @parameter name [String] A useful name/annotation for the recorded span.
|
||||
# @parameter attributes [Hash] Metadata for the recorded span.
|
||||
def trace(name, resource: nil, attributes: {}, &block)
|
||||
context = Context.nested(Fiber.current.traces_backend_context)
|
||||
Fiber.current.traces_backend_context = context
|
||||
|
||||
::Console.logger.info(resource || self, name, attributes)
|
||||
|
||||
if block.arity.zero?
|
||||
yield
|
||||
else
|
||||
yield Span.new(context, name)
|
||||
end
|
||||
end
|
||||
|
||||
# Assign a trace context to the current execution scope.
|
||||
def trace_context= context
|
||||
Fiber.current.traces_backend_context = context
|
||||
end
|
||||
|
||||
# Get a trace context from the current execution scope.
|
||||
def trace_context
|
||||
Fiber.current.traces_backend_context
|
||||
end
|
||||
|
||||
# @returns [Boolean] Whether there is an active trace.
|
||||
def active?
|
||||
!!Fiber.current.traces_backend_context
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Interface = Console::Interface
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,99 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2021-2023, by Samuel Williams.
|
||||
|
||||
require_relative '../context'
|
||||
|
||||
require 'fiber'
|
||||
|
||||
Fiber.attr_accessor :traces_backend_context
|
||||
|
||||
module Traces
|
||||
module Backend
|
||||
# A backend which validates interface usage.
|
||||
module Test
|
||||
# A span which validates tag assignment.
|
||||
class Span
|
||||
# Initialize a new span.
|
||||
# @parameter context [Context] The context in which the span is recorded.
|
||||
def initialize(context)
|
||||
@context = context
|
||||
end
|
||||
|
||||
# @attribute [Context] The context in which the span is recorded.
|
||||
attr :context
|
||||
|
||||
# Assign some metadata to the span.
|
||||
# @parameter key [String] The metadata key.
|
||||
# @parameter value [Object] The metadata value. Should be coercable to a string.
|
||||
def []= key, value
|
||||
unless key.is_a?(String) || key.is_a?(Symbol)
|
||||
raise ArgumentError, "Invalid attribute key (must be String or Symbol): #{key.inspect}!"
|
||||
end
|
||||
|
||||
begin
|
||||
String(value)
|
||||
rescue
|
||||
raise ArgumentError, "Invalid attribute value (must be convertible to String): #{value.inspect}!"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# The test backend interface.
|
||||
module Interface
|
||||
# Trace the given block of code and validate the interface usage.
|
||||
# @parameter name [String] A useful name/annotation for the recorded span.
|
||||
# @parameter resource [String] The context in which the trace operation is occuring.
|
||||
# @parameter attributes [Hash] Metadata for the recorded span.
|
||||
def trace(name, resource: nil, attributes: nil, &block)
|
||||
unless block_given?
|
||||
raise ArgumentError, "No block given!"
|
||||
end
|
||||
|
||||
unless name.is_a?(String)
|
||||
raise ArgumentError, "Invalid name (must be String): #{name.inspect}!"
|
||||
end
|
||||
|
||||
# It should be convertable:
|
||||
resource &&= resource.to_s
|
||||
|
||||
context = Context.nested(Fiber.current.traces_backend_context)
|
||||
|
||||
span = Span.new(context)
|
||||
|
||||
# Ensure the attributes are valid and follow the requirements:
|
||||
attributes&.each do |key, value|
|
||||
span[key] = value
|
||||
end
|
||||
|
||||
Fiber.current.traces_backend_context = context
|
||||
|
||||
if block.arity.zero?
|
||||
yield
|
||||
else
|
||||
yield span
|
||||
end
|
||||
end
|
||||
|
||||
# Assign a trace context to the current execution scope.
|
||||
def trace_context= context
|
||||
Fiber.current.traces_backend_context = context
|
||||
end
|
||||
|
||||
# Get a trace context from the current execution scope.
|
||||
def trace_context
|
||||
Fiber.current.traces_backend_context
|
||||
end
|
||||
|
||||
# @returns [Boolean] Whether there is an active trace.
|
||||
def active?
|
||||
# For the sake of testing, we always enable tracing.
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Interface = Test::Interface
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,55 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2024-2025, by Samuel Williams.
|
||||
|
||||
module Traces
|
||||
# Represents a configuration for the traces library.
|
||||
class Config
|
||||
DEFAULT_PATH = ENV.fetch("TRACES_CONFIG_DEFAULT_PATH", "config/traces.rb")
|
||||
|
||||
# Load the configuration from the given path.
|
||||
# @parameter path [String] The path to the configuration file.
|
||||
# @returns [Config] The loaded configuration.
|
||||
def self.load(path)
|
||||
config = self.new
|
||||
|
||||
if File.exist?(path)
|
||||
config.instance_eval(File.read(path), path)
|
||||
end
|
||||
|
||||
return config
|
||||
end
|
||||
|
||||
# Load the default configuration.
|
||||
# @returns [Config] The default configuration.
|
||||
def self.default
|
||||
@default ||= self.load(DEFAULT_PATH)
|
||||
end
|
||||
|
||||
# Prepare the backend, e.g. by loading additional libraries or instrumentation.
|
||||
def prepare
|
||||
end
|
||||
|
||||
# Require a specific traces backend implementation.
|
||||
def require_backend(env = ENV)
|
||||
if backend = env['TRACES_BACKEND']
|
||||
begin
|
||||
if require(backend)
|
||||
# We ensure that the interface methods replace any existing methods by prepending the module:
|
||||
Traces.singleton_class.prepend(Backend::Interface)
|
||||
|
||||
return true
|
||||
end
|
||||
rescue LoadError => error
|
||||
warn "Unable to load traces backend: #{backend.inspect}!"
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
# Load the default configuration.
|
||||
DEFAULT = self.default
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,112 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2021-2023, by Samuel Williams.
|
||||
|
||||
require 'securerandom'
|
||||
|
||||
module Traces
|
||||
# A generic representation of the current tracing context.
|
||||
class Context
|
||||
# Parse a string representation of a distributed trace.
|
||||
# @parameter parent [String] The parent trace context.
|
||||
# @parameter state [Array(String)] Any attached trace state.
|
||||
def self.parse(parent, state = nil, **options)
|
||||
version, trace_id, parent_id, flags = parent.split('-')
|
||||
|
||||
if version == '00'
|
||||
flags = Integer(flags, 16)
|
||||
|
||||
if state.is_a?(String)
|
||||
state = state.split(',')
|
||||
end
|
||||
|
||||
if state
|
||||
state = state.map{|item| item.split('=')}.to_h
|
||||
end
|
||||
|
||||
self.new(trace_id, parent_id, flags, state, **options)
|
||||
end
|
||||
end
|
||||
|
||||
# Create a local trace context which is likley to be globally unique.
|
||||
# @parameter flags [Integer] Any trace context flags.
|
||||
def self.local(flags = 0, **options)
|
||||
self.new(SecureRandom.hex(16), SecureRandom.hex(8), flags, **options)
|
||||
end
|
||||
|
||||
# Nest a local trace context in an optional parent context.
|
||||
# @parameter parent [Context] An optional parent context.
|
||||
def self.nested(parent, flags = 0)
|
||||
if parent
|
||||
parent.nested(flags)
|
||||
else
|
||||
self.local(flags)
|
||||
end
|
||||
end
|
||||
|
||||
SAMPLED = 0x01
|
||||
|
||||
# Initialize the trace context.
|
||||
# @parameter trace_id [String] The ID of the whole trace forest.
|
||||
# @parameter parent_id [String] The ID of this operation as known by the caller (sometimes referred to as the span ID).
|
||||
# @parameter flags [Integer] An 8-bit field that controls tracing flags such as sampling, trace level, etc.
|
||||
# @parameter state [Hash] Additional vendor-specific trace identification information.
|
||||
# @parameter remote [Boolean] Whether this context was created from a distributed trace header.
|
||||
def initialize(trace_id, parent_id, flags, state = nil, remote: false)
|
||||
@trace_id = trace_id
|
||||
@parent_id = parent_id
|
||||
@flags = flags
|
||||
@state = state
|
||||
@remote = remote
|
||||
end
|
||||
|
||||
# Create a new nested trace context in which spans can be recorded.
|
||||
def nested(flags = @flags)
|
||||
Context.new(@trace_id, SecureRandom.hex(8), flags, @state, remote: @remote)
|
||||
end
|
||||
|
||||
# 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.
|
||||
attr :trace_id
|
||||
|
||||
# The ID of this operation as known by the caller (in some tracing systems, this is known as the span-id, where a span is the execution of a client operation). It is represented as an 8-byte array, for example, 00f067aa0ba902b7. All bytes as zero (0000000000000000) is considered an invalid value.
|
||||
attr :parent_id
|
||||
|
||||
# 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.
|
||||
attr :flags
|
||||
|
||||
# Provides additional vendor-specific trace identification information across different distributed tracing systems. Conveys information about the operation's position in multiple distributed tracing graphs.
|
||||
attr :state
|
||||
|
||||
# Denotes that the caller may have recorded trace data. When unset, the caller did not record trace data out-of-band.
|
||||
def sampled?
|
||||
(@flags & SAMPLED) != 0
|
||||
end
|
||||
|
||||
# Whether this context was created from a distributed trace header.
|
||||
def remote?
|
||||
@remote
|
||||
end
|
||||
|
||||
# A string representation of the trace context (excluding trace state).
|
||||
def to_s
|
||||
"00-#{@trace_id}-#{@parent_id}-#{@flags.to_s(16)}"
|
||||
end
|
||||
|
||||
# Convert the trace context to a JSON representation, including trace state.
|
||||
def as_json
|
||||
{
|
||||
trace_id: @trace_id,
|
||||
parent_id: @parent_id,
|
||||
flags: @flags,
|
||||
state: @state,
|
||||
remote: @remote
|
||||
}
|
||||
end
|
||||
|
||||
# Convert the trace context to a JSON string.
|
||||
def to_json(...)
|
||||
as_json.to_json(...)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,46 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2021-2023, by Samuel Williams.
|
||||
|
||||
require_relative 'backend'
|
||||
|
||||
module Traces
|
||||
# @returns [Boolean] Whether there is an active backend.
|
||||
def self.enabled?
|
||||
Backend.const_defined?(:Interface)
|
||||
end
|
||||
|
||||
# @namespace
|
||||
module Provider
|
||||
end
|
||||
|
||||
module Singleton
|
||||
# A module which contains tracing specific wrappers.
|
||||
def traces_provider
|
||||
@traces_provider ||= Module.new
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :Singleton
|
||||
|
||||
# Bail out if there is no backend configured.
|
||||
if self.enabled?
|
||||
# Extend the specified class in order to emit traces.
|
||||
def self.Provider(klass, &block)
|
||||
klass.extend(Singleton)
|
||||
provider = klass.traces_provider
|
||||
klass.prepend(provider)
|
||||
|
||||
provider.module_exec(&block) if block_given?
|
||||
|
||||
return provider
|
||||
end
|
||||
|
||||
Config::DEFAULT.prepare
|
||||
else
|
||||
def self.Provider(klass, &block)
|
||||
# Tracing disabled.
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2021-2023, by Samuel Williams.
|
||||
|
||||
module Traces
|
||||
VERSION = "0.15.2"
|
||||
end
|
||||
@@ -0,0 +1,22 @@
|
||||
# MIT License
|
||||
|
||||
Copyright, 2021-2024, by Samuel Williams.
|
||||
Copyright, 2022, by Felix Yan.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,53 @@
|
||||
# Traces
|
||||
|
||||
Capture nested traces during code execution in a vendor agnostic way.
|
||||
|
||||
[](https://github.com/socketry/traces/actions?workflow=Test)
|
||||
|
||||
## Features
|
||||
|
||||
- Zero-overhead if tracing is disabled and minimal overhead if enabled.
|
||||
- Small opinionated interface with standardised semantics, consistent with the [W3C Trace Context Specification](https://github.com/w3c/trace-context).
|
||||
|
||||
## Usage
|
||||
|
||||
Please see the [project documentation](https://socketry.github.io/traces/) for more details.
|
||||
|
||||
- [Getting Started](https://socketry.github.io/traces/guides/getting-started/index) - This guide explains how to use `traces` for tracing code execution.
|
||||
|
||||
- [Testing](https://socketry.github.io/traces/guides/testing/index) - This guide explains how to test traces in your code.
|
||||
|
||||
- [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.
|
||||
|
||||
## Releases
|
||||
|
||||
Please see the [project releases](https://socketry.github.io/traces/releases/index) for all releases.
|
||||
|
||||
### v0.14.0
|
||||
|
||||
- [Introduce `Traces::Config` to Expose `prepare` Hook](https://socketry.github.io/traces/releases/index#introduce-traces::config-to-expose-prepare-hook)
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome contributions to this project.
|
||||
|
||||
1. Fork it.
|
||||
2. Create your feature branch (`git checkout -b my-new-feature`).
|
||||
3. Commit your changes (`git commit -am 'Add some feature'`).
|
||||
4. Push to the branch (`git push origin my-new-feature`).
|
||||
5. Create new Pull Request.
|
||||
|
||||
### Developer Certificate of Origin
|
||||
|
||||
In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed.
|
||||
|
||||
### Community Guidelines
|
||||
|
||||
This project is best served by a collaborative and respectful environment. Treat each other professionally, respect differing viewpoints, and engage constructively. Harassment, discrimination, or harmful behavior is not tolerated. Communicate clearly, listen actively, and support one another. If any issues arise, please inform the project maintainers.
|
||||
|
||||
## See Also
|
||||
|
||||
- [traces-backend-open\_telemetry](https://github.com/socketry/traces-backend-open_telemetry) — A backend for submitting traces to [OpenTelemetry](https://github.com/open-telemetry/opentelemetry-ruby), including [ScoutAPM](https://github.com/scoutapp/scout_apm_ruby).
|
||||
- [traces-backend-datadog](https://github.com/socketry/traces-backend-datadog) — A backend for submitting traces to [Datadog](https://github.com/DataDog/dd-trace-rb).
|
||||
- [traces-backend-newrelic](https://github.com/newrelic/traces-backend-newrelic) - A backend for submitting traces to [New Relic](https://github.com/newrelic/newrelic-ruby-agent).
|
||||
- [metrics](https://github.com/socketry/metrics) — A metrics interface which follows a similar pattern.
|
||||
@@ -0,0 +1,18 @@
|
||||
# Releases
|
||||
|
||||
## v0.14.0
|
||||
|
||||
### Introduce `Traces::Config` to Expose `prepare` Hook
|
||||
|
||||
The `traces` gem uses aspect-oriented programming to wrap existing methods to emit traces. However, while there are some reasonable defaults for emitting traces, it can be useful to customize the behavior and level of detail. To that end, the `traces` gem now optionally loads a `config/traces.rb` which includes a `prepare` hook that can be used to load additional providers.
|
||||
|
||||
``` ruby
|
||||
# config/traces.rb
|
||||
|
||||
def prepare
|
||||
require 'traces/provider/async'
|
||||
require 'traces/provider/async/http'
|
||||
end
|
||||
```
|
||||
|
||||
The `prepare` method is called immediately after the traces backend is loaded. You can require any provider you want in this file, or even add your own custom providers.
|
||||
Reference in New Issue
Block a user