This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user