Skip to content

Commit

Permalink
Merge pull request #2216 from DataDog/horizontal
Browse files Browse the repository at this point in the history
  • Loading branch information
marcotc authored Sep 7, 2022
2 parents bf6d048 + a08234d commit b2155fb
Show file tree
Hide file tree
Showing 2 changed files with 193 additions and 0 deletions.
84 changes: 84 additions & 0 deletions lib/datadog/tracing/distributed/datadog_tags_codec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# frozen_string_literal: true

module Datadog
module Tracing
module Distributed
# Encodes and decodes distributed 'x-datadog-tags' tags for transport
# to and from external processes.
module DatadogTagsCodec
# Backport `Regexp::match?` because it is measurably the most performant
# way to check if a string matches a regular expression.
module RefineRegexp
unless Regexp.method_defined?(:match?)
refine ::Regexp do
def match?(*args)
!match(*args).nil?
end
end
end
end
using RefineRegexp

# ASCII characters 32-126, except `,`, `=`, and ` `. At least one character.
VALID_KEY_CHARS = /\A(?:(?![,= ])[\u0020-\u007E])+\Z/.freeze
# ASCII characters 32-126, except `,`. At least one character.
VALID_VALUE_CHARS = /\A(?:(?!,)[\u0020-\u007E])+\Z/.freeze

# Serializes a {Hash<String,String>} into a `x-datadog-tags`-compatible
# String.
#
# @param tags [Hash<String,String>] trace tag hash
# @return [String] serialized tags hash
# @raise [EncodingError] if tags cannot be serialized to the `x-datadog-tags` format
def self.encode(tags)
begin
tags.map do |raw_key, raw_value|
key = raw_key.to_s
value = raw_value.to_s

raise EncodingError, "Invalid key `#{key}` for value `#{value}`" unless VALID_KEY_CHARS.match?(key)
raise EncodingError, "Invalid value `#{value}` for key `#{key}`" unless VALID_VALUE_CHARS.match?(value)

"#{key}=#{value.strip}"
end.join(',')
rescue => e
raise EncodingError, "Error encoding tags `#{tags}`: `#{e}`"
end
end

# Deserializes a `x-datadog-tags`-formatted String into a {Hash<String,String>}.
#
# @param string [String] tags as serialized by {#encode}
# @return [Hash<String,String>] decoded input as a hash of strings
# @raise [DecodingError] if string does not conform to the `x-datadog-tags` format
def self.decode(string)
result = Hash[string.split(',').map do |raw_tag|
raw_tag.split('=', 2).tap do |raw_key, raw_value|
key = raw_key.to_s
value = raw_value.to_s

raise DecodingError, "Invalid key: #{key}" unless VALID_KEY_CHARS.match?(key)
raise DecodingError, "Invalid value: #{value}" unless VALID_VALUE_CHARS.match?(value)

value.strip!
end
end]

raise DecodingError, "Invalid empty tags: #{string}" if result.empty? && !string.empty?

result
end

# An error occurred during distributed tags encoding.
# See {#message} for more information.
class EncodingError < StandardError
end

# An error occurred during distributed tags decoding.
# See {#message} for more information.
class DecodingError < StandardError
end
end
end
end
end
109 changes: 109 additions & 0 deletions spec/datadog/tracing/distributed/datadog_tags_codec_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# typed: false

require 'spec_helper'

require 'datadog/tracing/distributed/datadog_tags_codec'

RSpec.describe Datadog::Tracing::Distributed::DatadogTagsCodec do
let(:codec) { described_class }

describe '#decode' do
subject(:decode) { codec.decode(input) }

context 'with a valid input' do
[
['', {}],
['key=value', { 'key' => 'value' }],
['_key=value', { '_key' => 'value' }],
['1key=digit', { '1key' => 'digit' }],
['12345=678910', { '12345' => '678910' }],
['trailing=comma,', { 'trailing' => 'comma' }],
['value=with spaces', { 'value' => 'with spaces' }],
['value=with=equals', { 'value' => 'with=equals' }],
['trim= value ', { 'trim' => 'value' }],
['ascii@=~chars;', { 'ascii@' => '~chars;' }],
['a=1,b=2,c=3', { 'a' => '1', 'b' => '2', 'c' => '3' }],
].each do |input, expected|
context "of value `#{input}`" do
let(:input) { input }
it { is_expected.to eq(expected) }
end
end
end

context 'with an invalid input' do
[
'no_equals',
'no_value=',
'=no_key',
'=',
',',
',=,',
',leading=comma',
'key with=spaces',
"out_of=range\ncharacter",
"out\tof=range character",
].each do |input|
context "of value `#{input}`" do
let(:input) { input }
it { expect { decode }.to raise_error(Datadog::Tracing::Distributed::DatadogTagsCodec::DecodingError) }
end
end
end
end

describe '#encode' do
subject(:encode) { codec.encode(input) }

context 'with a valid input' do
[
[{}, ''],
[{ 'key' => 'value' }, 'key=value'],
[{ 'key' => 1 }, 'key=1'],
[{ 'a' => '1', 'b' => '2', 'c' => '3' }, 'a=1,b=2,c=3'],
[{ 'trim' => ' value ' }, 'trim=value'],
].each do |input, expected|
context "of value `#{input}`" do
let(:input) { input }
it { is_expected.to eq(expected) }
end
end
end

context 'with an invalid input' do
[
{ 'key with' => 'space' },
{ 'key,with' => 'comma' },
{ 'value' => 'with,comma' },
{ 'key=with' => 'equals' },
{ '' => 'empty_key' },
{ 'empty_value' => '' },
{ '🙅️' => 'out of range characters' },
{ 'out_of_range_characters' => '🙅️' },
].each do |input, _expected|
context "of value `#{input}`" do
let(:input) { input }
it { expect { encode }.to raise_error(Datadog::Tracing::Distributed::DatadogTagsCodec::EncodingError) }
end
end
end
end

describe 'encode and decode' do
let(:input) do
{ 'key' => 'value' }
end

let(:encoded_input) do
'key=value'
end

it 'decoding reverses encoding' do
expect(codec.decode(codec.encode(input))).to eq(input)
end

it 'encoding reverses decoding' do
expect(codec.encode(codec.decode(encoded_input))).to eq(encoded_input)
end
end
end

0 comments on commit b2155fb

Please sign in to comment.