From 845af37004d9e2a2688429d985678e99f987d4c9 Mon Sep 17 00:00:00 2001 From: Marco Costa Date: Wed, 10 Aug 2022 16:24:36 -0700 Subject: [PATCH 1/2] Sampling Propagation:Tag serializer --- .../tracing/distributed/datadog_tags_codec.rb | 84 ++++++++++++++ .../distributed/datadog_tags_codec_spec.rb | 109 ++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 lib/datadog/tracing/distributed/datadog_tags_codec.rb create mode 100644 spec/datadog/tracing/distributed/datadog_tags_codec_spec.rb diff --git a/lib/datadog/tracing/distributed/datadog_tags_codec.rb b/lib/datadog/tracing/distributed/datadog_tags_codec.rb new file mode 100644 index 0000000000..86e5956ced --- /dev/null +++ b/lib/datadog/tracing/distributed/datadog_tags_codec.rb @@ -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} into a `x-datadog-tags`-compatible + # String. + # + # @param tags [Hash] 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}. + # + # @param string [String] tags as serialized by {#encode} + # @return [Hash] 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 diff --git a/spec/datadog/tracing/distributed/datadog_tags_codec_spec.rb b/spec/datadog/tracing/distributed/datadog_tags_codec_spec.rb new file mode 100644 index 0000000000..858c75f62c --- /dev/null +++ b/spec/datadog/tracing/distributed/datadog_tags_codec_spec.rb @@ -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 From a08234d9a1b8d1b6bcbafe7ba1aaae5d8df812a0 Mon Sep 17 00:00:00 2001 From: Marco Costa Date: Thu, 11 Aug 2022 14:44:13 -0700 Subject: [PATCH 2/2] Fix Lint --- .../tracing/distributed/datadog_tags_codec_spec.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/datadog/tracing/distributed/datadog_tags_codec_spec.rb b/spec/datadog/tracing/distributed/datadog_tags_codec_spec.rb index 858c75f62c..822215cee4 100644 --- a/spec/datadog/tracing/distributed/datadog_tags_codec_spec.rb +++ b/spec/datadog/tracing/distributed/datadog_tags_codec_spec.rb @@ -72,13 +72,13 @@ context 'with an invalid input' do [ - { "key with" => 'space' }, - { "key,with" => 'comma' }, + { 'key with' => 'space' }, + { 'key,with' => 'comma' }, { 'value' => 'with,comma' }, - { "key=with" => 'equals' }, - { "" => 'empty_key' }, + { 'key=with' => 'equals' }, + { '' => 'empty_key' }, { 'empty_value' => '' }, - { "🙅️" => 'out of range characters' }, + { '🙅️' => 'out of range characters' }, { 'out_of_range_characters' => '🙅️' }, ].each do |input, _expected| context "of value `#{input}`" do