Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ executors:
- image: public.ecr.aws/aws-dynamodb-local/aws-dynamodb-local
- image: quay.io/minio/minio
command: ["server", "/data"]
- image: public.ecr.aws/redbox-public/s12v/sns:latest
- image: s12v/sns
- image: public.ecr.aws/sprig/elasticmq-native
- image: public.ecr.aws/docker/library/mongo:5-focal
mysql2:
Expand Down
2 changes: 1 addition & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Rake::TestTask.new(:test) do |t|
else
t.test_files = Dir[
'test/*_test.rb',
'test/{agent,trace,backend,snapshot}/*_test.rb'
'test/{agent,trace,backend,snapshot,span_filtering}/*_test.rb'
]
end
end
Expand Down
1 change: 1 addition & 0 deletions lib/instana/setup.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
require 'instana/backend/agent'
require 'instana/trace'
require 'instana/trace/tracer_provider'
require 'instana/span_filtering'

::Instana.setup
::Instana.agent.setup
Expand Down
62 changes: 62 additions & 0 deletions lib/instana/span_filtering.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# frozen_string_literal: true

# (c) Copyright IBM Corp. 2025

require 'instana/span_filtering/configuration'
require 'instana/span_filtering/filter_rule'
require 'instana/span_filtering/condition'

module Instana
# SpanFiltering module provides functionality to filter spans based on configured rules
module SpanFiltering
class << self
attr_reader :configuration

# Initialize the span filtering configuration
# @return [Configuration] The span filtering configuration
def initialize
@configuration = Configuration.new
end

# Check if span filtering is deactivated
# @return [Boolean] True if span filtering is deactivated
def deactivated?
@configuration&.deactivated || false
end

# Check if a span should be filtered out
# @param span [Hash] The span to check
# @return [Hash, nil] A result hash with filtered and suppression keys if filtered, nil if not filtered
def filter_span(span)
return nil if deactivated?
return nil unless @configuration

# Check include rules first (whitelist)
if @configuration.include_rules.any?
# If we have include rules, only keep spans that match at least one include rule
unless @configuration.include_rules.any? { |rule| rule.matches?(span) }
return { filtered: true, suppression: false }
end
# If it matches an include rule, continue to exclude rules
end

# Check exclude rules (blacklist)
@configuration.exclude_rules.each do |rule|
if rule.matches?(span)
return { filtered: true, suppression: rule.suppression }
end
end

nil # Keep the span if no rules match
end

# Reset the configuration (mainly for testing)
def reset
@configuration = nil
end
end

# Initialize on module load
initialize
end
end
134 changes: 134 additions & 0 deletions lib/instana/span_filtering/condition.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# (c) Copyright IBM Corp. 2025

module Instana
module SpanFiltering
# Represents a condition for filtering spans
#
# A condition consists of:
# - key: The attribute to match against (category, kind, type, or span attribute)
# - values: List of values to match against (OR logic between values)
# - match_type: String matching strategy (strict, startswith, endswith, contains)
class Condition
attr_reader :key, :values, :match_type

def initialize(key, values, match_type = 'strict')
@key = key
@values = Array(values)
@match_type = match_type
end

# Check if a span matches this condition
# @param span [Hash] The span to check
# @return [Boolean] True if the span matches any of the values
def matches?(span)
attribute_value = extract_attribute(span, @key)
return false if attribute_value.nil?

@values.any? { |value| matches_value?(attribute_value, value) }
end

private

# Extract an attribute from a span
# @param span [Hash] The span to extract from
# @param key [String] The key to extract
# @return [Object, nil] The attribute value or nil if not found
def extract_attribute(span, key)
case key
when 'category'
# Map to appropriate span attribute for category
determine_category(span)
when 'kind'
# Map to appropriate span attribute for kind
span[:k] || span['k']
when 'type'
# Map to appropriate span attribute for type
span[:n] || span['n']
else
# Handle nested attributes with dot notation
extract_nested_attribute(span[:data] || span['data'], key)
end
end

# Determine the category of a span based on its properties
# @param span [Hash] The span to categorize
# @return [String, nil] The category or nil if not determinable
def determine_category(span)
data = span[:data] || span['data']
return nil unless data

if data[:http] || data['http']
'protocols'
elsif data[:redis] || data[:mysql] || data[:pg] || data[:db]
'databases'
elsif data[:sqs] || data[:sns] || data[:mq]
'messaging'
elsif (span[:n] || span['n'])&.start_with?('log.')
'logging'
end
end

# Extract a nested attribute using dot notation
# @param data [Hash] The data hash to extract from
# @param key [String] The key in dot notation
# @return [Object, nil] The attribute value or nil if not found
def extract_nested_attribute(data, key)
return nil unless data

parts = key.split('.')
current = data

parts.each do |part|
# Try symbol key first, then string key
if current.key?(part.to_sym)
current = current[part.to_sym]
elsif current.key?(part)
current = current[part]
else
return nil # Key not found
end

# Only return nil if the value is actually nil, not for false values
end

current
end

# Check if an attribute value matches a condition value
# @param attribute_value [Object] The attribute value
# @param condition_value [String] The condition value
# @return [Boolean] True if the attribute value matches the condition value
def matches_value?(attribute_value, condition_value)
# Handle wildcard
return true if condition_value == '*'

# Direct comparison first - this should handle boolean values correctly
return true if attribute_value == condition_value

# For strict matching with type conversion
if @match_type == 'strict'
# Convert to strings and compare
attribute_str = attribute_value.to_s
condition_str = condition_value.to_s
return attribute_str == condition_str
end

# For other match types, convert both to strings
attribute_str = attribute_value.to_s
condition_str = condition_value.to_s

case @match_type
when 'startswith'
attribute_str.start_with?(condition_str)
when 'endswith'
attribute_str.end_with?(condition_str)
when 'contains'
attribute_str.include?(condition_str)
else
# Default to strict matching
attribute_str == condition_str
end
end
end
end
end
Loading
Loading