From 04e7ec94c8eac1684e8a6e4dc4318f00dcc3656f Mon Sep 17 00:00:00 2001 From: Jiren Patel Date: Mon, 1 Apr 2019 18:42:30 +0530 Subject: [PATCH 1/4] Added tag class with metdata ttl - refactor stats and tags code to accommodate tag class - updated test cases --- lib/opencensus/stats.rb | 21 ++- lib/opencensus/stats/measure.rb | 3 +- lib/opencensus/stats/measurement.rb | 11 +- lib/opencensus/stats/recorder.rb | 9 +- lib/opencensus/stats/view_data.rb | 6 +- lib/opencensus/tags/formatters.rb | 14 ++ lib/opencensus/tags/formatters/binary.rb | 29 ++++- lib/opencensus/tags/tag.rb | 129 ++++++++++++++++++ lib/opencensus/tags/tag_map.rb | 158 +++++++++++------------ test/stats/exporters/logger_test.rb | 13 +- test/stats/measure_test.rb | 13 +- test/stats/recorder_test.rb | 24 ++-- test/stats_test.rb | 5 +- test/tags/formatters/binary_test.rb | 41 +++--- test/tags/tag_map_test.rb | 157 ++++------------------ test/tags/tag_test.rb | 87 +++++++++++++ test/tags_test.rb | 3 +- 17 files changed, 451 insertions(+), 272 deletions(-) create mode 100644 lib/opencensus/tags/tag.rb create mode 100644 test/tags/tag_test.rb diff --git a/lib/opencensus/stats.rb b/lib/opencensus/stats.rb index e892b8dd..0d182f08 100644 --- a/lib/opencensus/stats.rb +++ b/lib/opencensus/stats.rb @@ -108,12 +108,23 @@ def registered_measures # # @param [String] name Name of the registered measure # @param [Integer, Float] value Value of the measurement - # @param [Hash] tags Tags to which the value is recorded - # @raise [ArgumentError] if givem measure is not register - def create_measurement name:, value:, tags: + # @param [Tags::TagMap] tags A map of tags to which the value is recorded. + # Tags could either be explicitly passed, or implicitly read from + # current tags context. + # @raise [ArgumentError] + # if given measure is not register. + # if given tags are nil and tags global context is nil. + def create_measurement name:, value:, tags: nil measure = MeasureRegistry.get name - return measure.create_measurement(value: value, tags: tags) if measure - raise ArgumentError, "#{name} measure is not registered" + + unless measure + raise ArgumentError, "#{name} measure is not registered" + end + + tags = OpenCensus::Tags.tag_map_context unless tags + raise ArgumentError, "pass tags or set tags global context" unless tags + + measure.create_measurement value: value, tags: tags end # Create and register a view to current stats recorder context. diff --git a/lib/opencensus/stats/measure.rb b/lib/opencensus/stats/measure.rb index 16763348..33e4ebe3 100644 --- a/lib/opencensus/stats/measure.rb +++ b/lib/opencensus/stats/measure.rb @@ -76,8 +76,9 @@ def initialize name:, unit:, type:, description: nil end # Create new measurement + # # @param [Integer, Float] value - # @param [Hash] tags Tags to which the value is recorded + # @param [Tags::TagMap] tags Tags to which the value is recorded # @return [Measurement] def create_measurement value:, tags: Measurement.new measure: self, value: value, tags: tags diff --git a/lib/opencensus/stats/measurement.rb b/lib/opencensus/stats/measurement.rb index d334d0cd..ee788380 100644 --- a/lib/opencensus/stats/measurement.rb +++ b/lib/opencensus/stats/measurement.rb @@ -13,7 +13,8 @@ class Measurement # @return [Integer,Float] The recorded value attr_reader :value - # @return [TagMap] The tags to which the value is applied + # @return [Tags::TagMap] The collection of tags to which the value is + # applied attr_reader :tags # @return [Time] The time when measurement was created. @@ -22,12 +23,12 @@ class Measurement # Create a instance of measurement # # @param [Measure] measure A measure to which the value is applied. - # @param [Integer,Float] value Measurement value. - # @param [Hash] tags The tags to which the value is applied - def initialize measure:, value:, tags: + # @param [Integer, Float] value Measurement value. + # @param [Tags::TagMap, nil] tags The tags to which the value is applied + def initialize measure:, value:, tags: nil @measure = measure @value = value - @tags = Tags::TagMap.new tags + @tags = tags || Tags::TagMap.new @time = Time.now.utc end end diff --git a/lib/opencensus/stats/recorder.rb b/lib/opencensus/stats/recorder.rb index 57e5dc03..42bcddce 100644 --- a/lib/opencensus/stats/recorder.rb +++ b/lib/opencensus/stats/recorder.rb @@ -54,10 +54,15 @@ def register_view view view end - # Record measurements + # Record measurements. # # @param [Array, Measurement] measurements - # @param [Hash] attachments + # A list of measurements to or single measurement. + # All passed measurements will be rejected If any one of the measurement + # has negative value. + # @param [Hash] attachments The contextual information + # associated with an example value. The contextual information is + # represented as key, value string pairs. def record *measurements, attachments: nil return if measurements.any? { |m| m.value < 0 } diff --git a/lib/opencensus/stats/view_data.rb b/lib/opencensus/stats/view_data.rb index 2bd7eba9..02ed80b8 100644 --- a/lib/opencensus/stats/view_data.rb +++ b/lib/opencensus/stats/view_data.rb @@ -46,7 +46,9 @@ def stop # @param [Measurement] measurement # @param [Hash] attachments def record measurement, attachments: nil - tag_values = @view.columns.map { |column| measurement.tags[column] } + tag_values = @view.columns.map do |column| + measurement.tags[column].value if measurement.tags[column] + end unless @data.key? tag_values @data[tag_values] = @view.aggregation.create_aggregation_data @@ -59,7 +61,7 @@ def record measurement, attachments: nil ) end - # Clear recorded ata + # Clear recorded data def clear data.clear end diff --git a/lib/opencensus/tags/formatters.rb b/lib/opencensus/tags/formatters.rb index 5fb4a1c8..4dbfdf4a 100644 --- a/lib/opencensus/tags/formatters.rb +++ b/lib/opencensus/tags/formatters.rb @@ -1,5 +1,19 @@ # frozen_string_literal: true +# Copyright 2019 OpenCensus Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + require "opencensus/tags/formatters/binary" module OpenCensus diff --git a/lib/opencensus/tags/formatters/binary.rb b/lib/opencensus/tags/formatters/binary.rb index afd352ee..460ea113 100644 --- a/lib/opencensus/tags/formatters/binary.rb +++ b/lib/opencensus/tags/formatters/binary.rb @@ -1,5 +1,20 @@ # frozen_string_literal: true +# Copyright 2019 OpenCensus Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + module OpenCensus module Tags module Formatters @@ -48,12 +63,14 @@ class BinaryFormatterError < StandardError; end def serialize tags_context binary = [int_to_varint(VERSION_ID)] - tags_context.each do |key, value| + tags_context.each do |tag| + next unless tag.propagate? + binary << int_to_varint(TAG_FIELD_ID) - binary << int_to_varint(key.length) - binary << key.encode(Encoding::UTF_8) - binary << int_to_varint(value ? value.length : 0) - binary << value.to_s.encode(Encoding::UTF_8) + binary << int_to_varint(tag.key.length) + binary << tag.key.encode(Encoding::UTF_8) + binary << int_to_varint(tag.value ? tag.value.length : 0) + binary << tag.value.to_s.encode(Encoding::UTF_8) end binary = binary.join @@ -87,7 +104,7 @@ def deserialize binary key = io.gets key_length value_length = varint_to_int io value = io.gets value_length - tag_map[key] = value + tag_map << Tag.new(key, value) end io.close diff --git a/lib/opencensus/tags/tag.rb b/lib/opencensus/tags/tag.rb new file mode 100644 index 00000000..3ed7e7a2 --- /dev/null +++ b/lib/opencensus/tags/tag.rb @@ -0,0 +1,129 @@ +# frozen_string_literal: true + +# Copyright 2019 OpenCensus Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +module OpenCensus + module Tags + # # Tag + # + # A Tag consists of key, value and tag metadata(TagTTL) + # + class Tag + # Invalid tag error. + class InvalidTagError < StandardError; end + + # The maximum length for a tag key and tag value + MAX_LENGTH = 255 + + # A tag with no propagation is considered to have local scope and is + # used within the process where it's created. + NO_PROPAGATION = 0 + + # A tag unlimited propagation can propagate unlimited hops. + # It is typical used to track a request, which may be processed across + # multiple entities. + UNLIMITED_PROPAGATION = -1 + + # @return [String] + attr_reader :key + + # @return [String] + attr_reader :value + + # @return [Integer] Represents number of hops a tag can propagate. + attr_reader :ttl + + # Create a tag + # + # @param [String] key Tag key + # Maximum allowed length of the key is {MAX_LENGTH} and must contains + # only printable characters. + # @param [String] value Tag value + # Maximum allowed length of the key is {MAX_LENGTH}. Value can be a + # nil value but if it present it must contains only printable + # characters. + # @param [Integer] ttl Tag Represents number of hops a tag can propagate. + # Default value is {UNLIMITED_PROPAGATION} unlimited propagation. + # Currently only two special values {NO_PROPAGATION} and + # {UNLIMITED_PROPAGATION} are supported. + # @raise [InvalidTagError] If invalid tag key or value. + # key: If key is empty, length grater then {MAX_LENGTH} characters or + # contains non printable characters + # value: If value length grater then 255 characters + # or contains non printable characters + # + def initialize key, value, ttl: nil + validate_key! key + validate_value! value + @key = key + @value = value + @ttl = ttl || UNLIMITED_PROPAGATION + end + + # Check tag can propagate + # @return [Boolean] + def propagate? + @ttl == -1 || @ttl > 0 + end + + # Set no propagation A tag with no propagation is considered to have + # local scope and is used within the process where it's created. + def set_no_propagation + @ttl = NO_PROPAGATION + end + + # Set unlimited propagation + # A tag unlimited propagation can propagate unlimited hops. + # It is typical used to track a request, which may be processed across + # multiple entities. + def set_unlimited_propagation + @ttl = UNLIMITED_PROPAGATION + end + + private + + # Validate tag key. + # @param [String] value + # @raise [InvalidTagError] If key is empty, length grater then 255 + # characters or contains non printable characters + # + def validate_key! value + if value.empty? || value.length > MAX_LENGTH || !printable_str?(value) + raise InvalidTagError, "Invalid tag key #{key}" + end + end + + # Validate tag value. + # @param [String] value + # @raise [InvalidTagError] If value length grater then 255 characters + # or contains non printable characters + # + def validate_value! value + if (value && value.length > MAX_LENGTH) || !printable_str?(value) + raise InvalidTagError, "Invalid tag value #{value}" + end + end + + # Check string is printable. + # @param [String] str + # @return [Boolean] + # + def printable_str? str + str.bytes.none? { |b| b < 32 || b > 126 } + end + end + end +end diff --git a/lib/opencensus/tags/tag_map.rb b/lib/opencensus/tags/tag_map.rb index 69a2b16b..396cfb53 100644 --- a/lib/opencensus/tags/tag_map.rb +++ b/lib/opencensus/tags/tag_map.rb @@ -1,71 +1,116 @@ # frozen_string_literal: true +# Copyright 2019 OpenCensus Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + require "forwardable" +require "opencensus/tags/tag" module OpenCensus module Tags # # TagMap # - # Collection of tag key and value. + # Collection of tags that can be used to label anything that is + # associated with a stats or trace operations. + # # @example # - # tag_map = OpenCensus::Tags::OpenCensus.new + # tag_map = OpenCensus::Tags::TagMap.new + # + # # Add tag + # tag_map << OpenCensus::Tags::Tag.new "key1", "val1" # - # # Add or update - # tag_map["key1"] = "value1" - # tag_map["key2"] = "value2" + # # Add tag with TagTTL + # tag_map << OpenCensus::Tags::Tag.new "key2", "val2", ttl: 0 # - # # Get value - # tag_map["key1"] # value1 + # # Update tag + # tag_map << OpenCensus::Tags::Tag.new "key3", "val3" + # tag_map << OpenCensus::Tags::Tag.new "key3", "updatedval3" + # + # # Get tag by key + # p tag_map["key1"] + # # # # # Delete # tag_map.delete "key1" # # # Iterate - # tag_map.each do |key, value| - # p key - # p value + # tag_map.each do |tag| + # p tag.key + # p tag.value # end # # # Length # tag_map.length # 1 # - # @example Create tag map from hash + # @example Create tag map with list of tags. # - # tag_map = OpenCensus::Tags::OpenCensus.new({ "key1" => "value1"}) + # tag_map = OpenCensus::Tags::OpenCensus.new([ + # OpenCensus::Tags::Tag.new "key1", "val1", + # OpenCensus::Tags::Tag.new "key2", "val2" + # ]) # class TagMap extend Forwardable - # The maximum length for a tag key and tag value - MAX_LENGTH = 255 - - # Create a tag map. It is a map of tags from key to value. - # @param [Hash{String=>String}] tags Tags hash with string key and value. + # Create a tag map. # - def initialize tags = {} - @tags = {} + # @param [Array] tags Tags list with string key and value and + # metadata. + # + def initialize tags = [] + @tags = tags.each_with_object({}) { |tag, r| r[tag.key] = tag } + end - tags.each do |key, value| - self[key] = value - end + # Insert tag. + # + # @param [Tag] tag + def << tag + @tags[tag.key] = tag end - # Set tag key value + # Get all tags # - # @param [String] key Tag key - # @param [String] value Tag value - # @raise [InvalidTagError] If invalid tag key or value. + # @returns [Array] + def tags + @tags.values + end + + # Get tag by key # - def []= key, value - validate_key! key - validate_value! value + # @return [Tag, nil] + def [] key + @tags[key] + end - @tags[key] = value + # Delete tag by key + # @param [String] key Tag key + def delete key + @tags.delete key end + # @!method each + # @see Array#each + # @!method length + # @see Array#length + # @!method empty? + # @see Array#empty? + def_delegators :tags, :each, :length, :empty? + # Convert tag map to binary string format. + # # @see [documentation](https://github.com/census-instrumentation/opencensus-specs/blob/master/encodings/BinaryEncoding.md#tag-context) # @return [String] Binary string # @@ -74,64 +119,13 @@ def to_binary end # Create a tag map from the binary string. + # # @param [String] data Binary string data # @return [TagMap] # def self.from_binary data Formatters::Binary.new.deserialize data end - - # @!method [] - # @see Hash#[] - # @!method each - # @see Hash#each - # @!method delete - # @see Hash#delete - # @!method delete_if - # @see Hash#delete_if - # @!method length - # @see Hash#length - # @!method to_h - # @see Hash#to_h - # @!method empty? - # @see Hash#empty? - def_delegators :@tags, :[], :each, :delete, :delete_if, :length, \ - :to_h, :empty? - - # Invalid tag error. - class InvalidTagError < StandardError; end - - private - - # Validate tag key. - # @param [String] key - # @raise [InvalidTagError] If key is empty, length grater then 255 - # characters or contains non printable characters - # - def validate_key! key - if key.empty? || key.length > MAX_LENGTH || !printable_str?(key) - raise InvalidTagError, "Invalid tag key #{key}" - end - end - - # Validate tag value. - # @param [String] value - # @raise [InvalidTagError] If value length grater then 255 characters - # or contains non printable characters - # - def validate_value! value - if (value && value.length > MAX_LENGTH) || !printable_str?(value) - raise InvalidTagError, "Invalid tag value #{value}" - end - end - - # Check string is printable. - # @param [String] str - # @return [Boolean] - # - def printable_str? str - str.bytes.none? { |b| b < 32 || b > 126 } - end end end end diff --git a/test/stats/exporters/logger_test.rb b/test/stats/exporters/logger_test.rb index 77ca93a8..6439df7f 100644 --- a/test/stats/exporters/logger_test.rb +++ b/test/stats/exporters/logger_test.rb @@ -20,7 +20,9 @@ ) } let(:aggregation){ OpenCensus::Stats::Aggregation::Sum.new } - let(:tag_keys) { ["frontend"]} + let(:tag_key) { "frontend" } + let(:tag_value) { "mobile-ios9.3.5" } + let(:tag_keys) { [tag_key] } let(:view) { OpenCensus::Stats::View.new( name: "test.view", @@ -30,12 +32,13 @@ columns: tag_keys ) } - let(:tag_map) { - OpenCensus::Tags::TagMap.new(tag_keys.first => "mobile-ios9.3.5") + let(:tag){ + OpenCensus::Tags::Tag.new tag_key, tag_value } - let(:tags){ - { tag_keys.first => "mobile-ios9.3.5" } + let(:tags) { + OpenCensus::Tags::TagMap.new [tag] } + let(:view_data){ recorder = OpenCensus::Stats::Recorder.new recorder.register_view view diff --git a/test/stats/measure_test.rb b/test/stats/measure_test.rb index c9e69698..1b018fe4 100644 --- a/test/stats/measure_test.rb +++ b/test/stats/measure_test.rb @@ -1,6 +1,15 @@ require "test_helper" describe OpenCensus::Stats::Measurement do + let(:tag_key) { "key1" } + let(:tag_value) { "val1" } + let(:tag){ + OpenCensus::Tags::Tag.new tag_key, tag_value + } + let(:tags) { + OpenCensus::Tags::TagMap.new [tag] + } + describe "create" do it "int64 type measure" do measure = OpenCensus::Stats::Measure.new( @@ -42,11 +51,11 @@ type: OpenCensus::Stats::Measure::DOUBLE_TYPE, description: "storage desc" ) - tags = { "key1" => "val1" } + measurement = measure.create_measurement value: 10.10, tags: tags measurement.value.must_equal 10.10 measurement.measure.must_equal measure measurement.tags.must_be_kind_of OpenCensus::Tags::TagMap - measurement.tags.to_h.must_equal tags + measurement.tags[tag_key].value.must_equal tag_value end end diff --git a/test/stats/recorder_test.rb b/test/stats/recorder_test.rb index 737566c6..0ba86b54 100644 --- a/test/stats/recorder_test.rb +++ b/test/stats/recorder_test.rb @@ -14,7 +14,9 @@ ) } let(:aggregation){ OpenCensus::Stats::Aggregation::Sum.new } - let(:tag_keys) { ["frontend"]} + let(:tag_key) { "frontend" } + let(:tag_value) { "mobile.1.0.1" } + let(:tag_keys) { [tag_key]} let(:view) { OpenCensus::Stats::View.new( name: "test.view", @@ -24,12 +26,11 @@ columns: tag_keys ) } - let(:tag_map) { - OpenCensus::Tags::TagMap.new(tag_keys.first => "mobile-ios9.3.5") + let(:tag) { + OpenCensus::Tags::Tag.new tag_key, tag_value } - let(:tags) { - { tag_keys.first => "mobile-ios9.3.5" } + OpenCensus::Tags::TagMap.new [tag] } it "create reacorder with default properties" do @@ -155,22 +156,21 @@ end it "record measurement against tags global context" do - OpenCensus::Tags.tag_map_context = OpenCensus::Tags::TagMap.new( - "frontend" => "android-1.0.1" - ) + tag_ctx = OpenCensus::Tags::TagMap.new + tag_ctx << OpenCensus::Tags::Tag.new(tag_key, tag_value) + OpenCensus::Tags.tag_map_context = tag_ctx recorder = OpenCensus::Stats::Recorder.new recorder.register_view view measurement = OpenCensus::Stats.create_measurement( name: measure_name, - value: 1, - tags: OpenCensus::Tags.tag_map_context + value: 1 ) recorder.record measurement view_data = recorder.view_data view.name view_data.data.length.must_equal 1 - view_data.data.key?(["android-1.0.1"]).must_equal true + view_data.data.key?([tag_value]).must_equal true end end @@ -179,7 +179,7 @@ recorder = OpenCensus::Stats::Recorder.new recorder.register_view view - recorder.record measure.create_measurement(value: 10, tags: tag_map) + recorder.record measure.create_measurement(value: 10, tags: tags) view_data = recorder.view_data view.name view_data.data.length.must_equal 1 diff --git a/test/stats_test.rb b/test/stats_test.rb index 48983398..5cb2159f 100644 --- a/test/stats_test.rb +++ b/test/stats_test.rb @@ -2,7 +2,10 @@ describe OpenCensus::Stats do let(:tags) { - { "tag1" => "value1", "tag2" => "value2"} + [ + OpenCensus::Tags::Tag.new("tag1", "value1"), + OpenCensus::Tags::Tag.new("tag2", "value2") + ] } describe "stats context" do diff --git a/test/tags/formatters/binary_test.rb b/test/tags/formatters/binary_test.rb index c1e83e17..6b772dd8 100644 --- a/test/tags/formatters/binary_test.rb +++ b/test/tags/formatters/binary_test.rb @@ -5,12 +5,11 @@ describe "serialize" do it "return serialize tag map binary data" do - tag_map = OpenCensus::Tags::TagMap.new({ - "key1" => "val1", - "key2" => "val2", - "key3" => "val3", - "key4" => "val4", - }) + tag_map = OpenCensus::Tags::TagMap.new + + 4.times do |i| + tag_map << OpenCensus::Tags::Tag.new("key#{i+1}", "val#{i+1}") + end binary = formatter.serialize(tag_map) expected_binary = "\x00\x00\x04key1\x04val1\x00\x04key2\x04val2\x00\x04key3\x04val3\x00\x04key4\x04val4" @@ -20,13 +19,26 @@ it "return nil if tag map serialize data size more then 8192 bytes" do tag_map = OpenCensus::Tags::TagMap.new - 500.times do |i| - tag_map["key#{i}"] = "value#{i}" + 600.times do |i| + tag_map << OpenCensus::Tags::Tag.new("key#{i+1}", "val#{i+1}") end binary = formatter.serialize(tag_map) binary.must_be_nil end + + it "remove tag with ttl set to no propagation" do + tag_map = OpenCensus::Tags::TagMap.new + tag_map << OpenCensus::Tags::Tag.new("key1", "val1") + tag_map << OpenCensus::Tags::Tag.new("key2", "val2", ttl: 0) + tag_map << OpenCensus::Tags::Tag.new("key3", "val3") + tag_map << OpenCensus::Tags::Tag.new("key4", "val4") + + + binary = formatter.serialize(tag_map) + expected_binary = "\x00\x00\x04key1\x04val1\x00\x04key3\x04val3\x00\x04key4\x04val4" + binary.must_equal expected_binary + end end describe "deserialize" do @@ -36,13 +48,9 @@ tag_map = formatter.deserialize binary tag_map.length.must_equal 4 - expected_tag_map_values = { - "key1" => "val1", - "key2" => "val2", - "key3" => "val3", - "key4" => "val4", - } - tag_map.to_h.must_equal expected_tag_map_values + 4.times do |i| + tag_map["key#{i+1}"].value.must_equal "val#{i+1}" + end end it "returns empty tag map for empty string" do @@ -68,7 +76,8 @@ binary = "\x00\x00\x04key1\x04val1\x01\x04key2\x04val2\x00\x04key3\x04val3" tag_map = formatter.deserialize binary - tag_map.to_h.must_equal({ "key1" => "val1" }) + tag_map.length.must_equal 1 + tag_map["key1"].value.must_equal "val1" end end end diff --git a/test/tags/tag_map_test.rb b/test/tags/tag_map_test.rb index fed7fda7..1e01b961 100644 --- a/test/tags/tag_map_test.rb +++ b/test/tags/tag_map_test.rb @@ -1,6 +1,13 @@ require "test_helper" describe OpenCensus::Tags::TagMap do + let(:tag_key) { "frontend" } + let(:tag_value){ "mobile-1.0.0" } + let(:tag) { + OpenCensus::Tags::Tag.new tag_key, tag_value + } + let(:tags){ [tag] } + describe "create" do it "create tags map with defaults" do tag_map = OpenCensus::Tags::TagMap.new @@ -8,153 +15,41 @@ end it "create tags map with tags key, values" do - tag_map = OpenCensus::Tags::TagMap.new({ "frontend" => "mobile-1.0"}) + tag_map = OpenCensus::Tags::TagMap.new(tags) tag_map.length.must_equal 1 - tag_map["frontend"].must_equal "mobile-1.0" - end - - it "create tag map with empty value" do - tag_map = OpenCensus::Tags::TagMap.new({ "frontend" => ""}) - tag_map["frontend"].must_equal "" - end - - describe "tag key validation" do - it "raise error for empty key" do - expect { - raise OpenCensus::Tags::TagMap.new({ "" => "mobile-1.0"}) - }.must_raise OpenCensus::Tags::TagMap::InvalidTagError - end - - it "raise error if key length more then 255 chars" do - key = "k" * 256 - expect { - raise OpenCensus::Tags::TagMap.new({ key => "mobile-1.0"}) - }.must_raise OpenCensus::Tags::TagMap::InvalidTagError - end - - it "raise error if key contains non printable chars less then 32 ascii code" do - key = "key#{[31].pack('c')}-test" - expect { - raise OpenCensus::Tags::TagMap.new({ key => "mobile-1.0"}) - }.must_raise OpenCensus::Tags::TagMap::InvalidTagError - end - - it "raise error if key contains non printable chars greater then 126 ascii code" do - key = "key#{[127].pack('c')}-test" - expect { - raise OpenCensus::Tags::TagMap.new({ key => "mobile-1.0"}) - }.must_raise OpenCensus::Tags::TagMap::InvalidTagError - end - end - - describe "tag value validation" do - it "raise error if value length more then 255 chars" do - value = "v" * 256 - expect { - OpenCensus::Tags::TagMap.new({ "frontend" => value }) - }.must_raise OpenCensus::Tags::TagMap::InvalidTagError - end - - it "raise error if value contains non printable chars less then 32 ascii code" do - value = "value#{[31].pack('c')}-test" - expect { - OpenCensus::Tags::TagMap.new({ "frontend" => value}) - }.must_raise OpenCensus::Tags::TagMap::InvalidTagError - end - - it "raise error if value contains non printable chars greater then 126 ascii code" do - value = "value#{[127].pack('c')}-test" - expect { - OpenCensus::Tags::TagMap.new({ "frontend" => value }) - }.must_raise OpenCensus::Tags::TagMap::InvalidTagError - end + tag_map[tag_key].value.must_equal tag_value end end describe "add tag to tag map" do it "set tag key value" do tag_map = OpenCensus::Tags::TagMap.new - tag_map["frontend"] = "mobile-1.0" - tag_map["frontend"].must_equal "mobile-1.0" + tag_map << OpenCensus::Tags::Tag.new(tag_key, tag_value) + tag_map.length.must_equal 1 + tag_map[tag_key].value.must_equal tag_value end it "allow empty tag value" do tag_map = OpenCensus::Tags::TagMap.new - tag_map["frontend"] = "" - tag_map["frontend"].must_equal "" - end - - describe "tag key validation" do - let(:tag_map) { OpenCensus::Tags::TagMap.new } - - it "raise error for empty key" do - expect { - tag_map[""] = "mobile-1.0" - }.must_raise OpenCensus::Tags::TagMap::InvalidTagError - end - - it "raise error if key length more then 255 chars" do - key = "k" * 256 - expect { - tag_map[key] = "mobile-1.0" - }.must_raise OpenCensus::Tags::TagMap::InvalidTagError - end - - it "raise error if key contains non printable chars less then 32 ascii code" do - key = "key#{[31].pack('c')}-test" - expect { - tag_map[key] = "mobile-1.0" - }.must_raise OpenCensus::Tags::TagMap::InvalidTagError - end - - it "raise error if key contains non printable chars greater then 126 ascii code" do - key = "key#{[127].pack('c')}-test" - expect { - tag_map[key] = "mobile-1.0" - }.must_raise OpenCensus::Tags::TagMap::InvalidTagError - end - end - - describe "tag value validation" do - let(:tag_map) { OpenCensus::Tags::TagMap.new } - - it "raise error if value length more then 255 chars" do - value = "v" * 256 - expect { - tag_map["frontend"] = value - }.must_raise OpenCensus::Tags::TagMap::InvalidTagError - end - - it "raise error if value contains non printable chars less then 32 ascii code" do - value = "value#{[31].pack('c')}-test" - expect { - tag_map["frontend"] = value - }.must_raise OpenCensus::Tags::TagMap::InvalidTagError - end - - it "raise error if value contains non printable chars greater then 126 ascii code" do - value = "value#{[127].pack('c')}-test" - expect { - tag_map["frontend"] = value - }.must_raise OpenCensus::Tags::TagMap::InvalidTagError - end + tag_map << OpenCensus::Tags::Tag.new(tag_key, "") + tag_map.length.must_equal 1 + tag_map[tag_key].value.must_equal "" end end - it "delete" do - tag_map = OpenCensus::Tags::TagMap.new({ "frontend" => "mobile-1.0"}) + it "delete tag" do + tag_map = OpenCensus::Tags::TagMap.new [tag] - tag_map.delete "frontend" - tag_map["frontend"].must_be_nil + tag_map.delete tag_key + tag_map[tag_key].must_be_nil tag_map.length.must_equal 0 end describe "binary formatter" do it "serialize tag map to binary format" do - tag_map = OpenCensus::Tags::TagMap.new({ - "key1" => "val1", - "key2" => "val2" - }) + tag_map = OpenCensus::Tags::TagMap.new + tag_map << OpenCensus::Tags::Tag.new("key1", "val1") + tag_map << OpenCensus::Tags::Tag.new("key2", "val2") expected_binary = "\x00\x00\x04key1\x04val1\x00\x04key2\x04val2" tag_map.to_binary.must_equal expected_binary @@ -164,11 +59,9 @@ binary = "\x00\x00\x04key1\x04val1\x00\x04key2\x04val2" tag_map = OpenCensus::Tags::TagMap.from_binary binary - expected_value = { - "key1" => "val1", - "key2" => "val2" - } - tag_map.to_h.must_equal expected_value + tag_map.length.must_equal 2 + tag_map["key1"].value.must_equal "val1" + tag_map["key2"].value.must_equal "val2" end end end diff --git a/test/tags/tag_test.rb b/test/tags/tag_test.rb new file mode 100644 index 00000000..1ebac2b1 --- /dev/null +++ b/test/tags/tag_test.rb @@ -0,0 +1,87 @@ +require "test_helper" + +describe OpenCensus::Tags::Tag do + describe "create" do + it "create tag" do + tag = OpenCensus::Tags::Tag.new "key", "value" + tag.key.must_equal "key" + tag.value.must_equal "value" + tag.ttl.must_equal(-1) + tag.propagate?.must_equal true + end + + it "create tag with ttl no propogation" do + tag = OpenCensus::Tags::Tag.new "key", "value", ttl: 0 + tag.key.must_equal "key" + tag.value.must_equal "value" + tag.ttl.must_equal 0 + tag.propagate?.wont_equal true + end + + describe "tag key validation" do + it "raise error for empty key" do + expect { + raise OpenCensus::Tags::Tag.new("", "mobile-1.0") + }.must_raise OpenCensus::Tags::Tag::InvalidTagError + end + + it "raise error if key length more then 255 chars" do + key = "k" * 256 + expect { + raise OpenCensus::Tags::Tag.new(key, "mobile-1.0") + }.must_raise OpenCensus::Tags::Tag::InvalidTagError + end + + it "raise error if key contains non printable chars less then 32 ascii code" do + key = "key#{[31].pack('c')}-test" + expect { + raise OpenCensus::Tags::Tag.new(key, "mobile-1.0") + }.must_raise OpenCensus::Tags::Tag::InvalidTagError + end + + it "raise error if key contains non printable chars greater then 126 ascii code" do + key = "key#{[127].pack('c')}-test" + expect { + raise OpenCensus::Tags::Tag.new(key, "mobile-1.0") + }.must_raise OpenCensus::Tags::Tag::InvalidTagError + end + end + + describe "tag value validation" do + it "raise error if value length more then 255 chars" do + value = "v" * 256 + expect { + OpenCensus::Tags::Tag.new "frontend", value + }.must_raise OpenCensus::Tags::Tag::InvalidTagError + end + + it "raise error if value contains non printable chars less then 32 ascii code" do + value = "value#{[31].pack('c')}-test" + expect { + OpenCensus::Tags::Tag.new "frontend", value + }.must_raise OpenCensus::Tags::Tag::InvalidTagError + end + + it "raise error if value contains non printable chars greater then 126 ascii code" do + value = "value#{[127].pack('c')}-test" + expect { + OpenCensus::Tags::Tag.new "frontend", value + }.must_raise OpenCensus::Tags::Tag::InvalidTagError + end + end + end + + describe "tag ttl" do + it "set no propagation" do + tag = OpenCensus::Tags::Tag.new "key", "val" + tag.set_no_propagation + tag.ttl.must_equal 0 + end + + it "set unlimited propagation" do + tag = OpenCensus::Tags::Tag.new "key", "val" + tag.set_unlimited_propagation + tag.ttl.must_equal(-1) + end + end +end diff --git a/test/tags_test.rb b/test/tags_test.rb index f8123cfb..d74f1eaa 100644 --- a/test/tags_test.rb +++ b/test/tags_test.rb @@ -6,7 +6,8 @@ } it "can be set and unset tag map context" do - tag_map = OpenCensus::Tags::TagMap.new({ "frontend" => "mobile-1.0"}) + tag_map = OpenCensus::Tags::TagMap.new + tag_map << OpenCensus::Tags::Tag.new("frontend", "mobile-1.0") OpenCensus::Tags.tag_map_context.must_be_nil OpenCensus::Tags.tag_map_context = tag_map OpenCensus::Tags.tag_map_context.must_equal tag_map From 3fbd526a1aa6317dfa7bee16ce74402ada7ce386 Mon Sep 17 00:00:00 2001 From: Jiren Patel Date: Mon, 1 Apr 2019 18:47:06 +0530 Subject: [PATCH 2/4] Fixed doc and rubocop warning --- lib/opencensus/stats.rb | 7 ++----- lib/opencensus/stats/exemplar.rb | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/opencensus/stats.rb b/lib/opencensus/stats.rb index 0d182f08..6c298f5f 100644 --- a/lib/opencensus/stats.rb +++ b/lib/opencensus/stats.rb @@ -116,12 +116,9 @@ def registered_measures # if given tags are nil and tags global context is nil. def create_measurement name:, value:, tags: nil measure = MeasureRegistry.get name + raise ArgumentError, "#{name} measure is not registered" unless measure - unless measure - raise ArgumentError, "#{name} measure is not registered" - end - - tags = OpenCensus::Tags.tag_map_context unless tags + tags ||= OpenCensus::Tags.tag_map_context raise ArgumentError, "pass tags or set tags global context" unless tags measure.create_measurement value: value, tags: tags diff --git a/lib/opencensus/stats/exemplar.rb b/lib/opencensus/stats/exemplar.rb index 77a4263b..e1c05455 100644 --- a/lib/opencensus/stats/exemplar.rb +++ b/lib/opencensus/stats/exemplar.rb @@ -25,7 +25,7 @@ class Exemplar # Create instance of the exemplar # @param [Integer,Float] value # @param [Time] time - # @param [Hash] attachments. Attachments are key-value + # @param [Hash] attachments Attachments are key-value # pairs that describe the context in which the exemplar was recored. def initialize value:, time:, attachments: @value = value From 2a9d699f3d63ae90aa83b1c4b0b4674b0450dbcc Mon Sep 17 00:00:00 2001 From: Jiren Patel Date: Wed, 24 Apr 2019 20:18:09 +0530 Subject: [PATCH 3/4] TagMap creation using hash and tags list - updated test cases --- lib/opencensus/stats/measure.rb | 5 ++- lib/opencensus/stats/measurement.rb | 5 ++- lib/opencensus/tags/formatters/binary.rb | 12 ++++-- lib/opencensus/tags/tag.rb | 45 ++++++++++++++++++---- lib/opencensus/tags/tag_map.rb | 28 +++++++++++--- test/stats/measure_test.rb | 2 +- test/stats/measurement_test.rb | 48 ++++++++++++++++++++++++ test/tags/tag_map_test.rb | 8 +++- test/tags/tag_test.rb | 18 +++++---- 9 files changed, 141 insertions(+), 30 deletions(-) create mode 100644 test/stats/measurement_test.rb diff --git a/lib/opencensus/stats/measure.rb b/lib/opencensus/stats/measure.rb index 33e4ebe3..5b25fe89 100644 --- a/lib/opencensus/stats/measure.rb +++ b/lib/opencensus/stats/measure.rb @@ -78,7 +78,8 @@ def initialize name:, unit:, type:, description: nil # Create new measurement # # @param [Integer, Float] value - # @param [Tags::TagMap] tags Tags to which the value is recorded + # @param [Hash, Tags::TagMap] tags Tags to which the value + # is recorded # @return [Measurement] def create_measurement value:, tags: Measurement.new measure: self, value: value, tags: tags @@ -90,7 +91,7 @@ def int64? type == INT64_TYPE end - # Is float data type + # Is double data type # @return [Boolean] def double? type == DOUBLE_TYPE diff --git a/lib/opencensus/stats/measurement.rb b/lib/opencensus/stats/measurement.rb index ee788380..eb4d6ea0 100644 --- a/lib/opencensus/stats/measurement.rb +++ b/lib/opencensus/stats/measurement.rb @@ -24,11 +24,12 @@ class Measurement # # @param [Measure] measure A measure to which the value is applied. # @param [Integer, Float] value Measurement value. - # @param [Tags::TagMap, nil] tags The tags to which the value is applied + # @param [Hash,Tags::TagMap, nil] tags The tags to which + # the value is applied def initialize measure:, value:, tags: nil @measure = measure @value = value - @tags = tags || Tags::TagMap.new + @tags = tags.instance_of?(Tags::TagMap) ? tags : Tags::TagMap.new(tags) @time = Time.now.utc end end diff --git a/lib/opencensus/tags/formatters/binary.rb b/lib/opencensus/tags/formatters/binary.rb index 460ea113..f131c45f 100644 --- a/lib/opencensus/tags/formatters/binary.rb +++ b/lib/opencensus/tags/formatters/binary.rb @@ -59,6 +59,7 @@ class BinaryFormatterError < StandardError; end # Serialize TagMap object # # @param [TagMap] tags_context + # @return [String, nil] # def serialize tags_context binary = [int_to_varint(VERSION_ID)] @@ -68,9 +69,14 @@ def serialize tags_context binary << int_to_varint(TAG_FIELD_ID) binary << int_to_varint(tag.key.length) - binary << tag.key.encode(Encoding::UTF_8) - binary << int_to_varint(tag.value ? tag.value.length : 0) - binary << tag.value.to_s.encode(Encoding::UTF_8) + binary << tag.key + + if tag.value + binary << int_to_varint(tag.value.length) + binary << tag.value + else + binary << int_to_varint(0) + end end binary = binary.join diff --git a/lib/opencensus/tags/tag.rb b/lib/opencensus/tags/tag.rb index 3ed7e7a2..9c9c5fab 100644 --- a/lib/opencensus/tags/tag.rb +++ b/lib/opencensus/tags/tag.rb @@ -76,21 +76,50 @@ def initialize key, value, ttl: nil # Check tag can propagate # @return [Boolean] def propagate? - @ttl == -1 || @ttl > 0 + @ttl == UNLIMITED_PROPAGATION || @ttl > NO_PROPAGATION end - # Set no propagation A tag with no propagation is considered to have + # Create non propagative tag. + # + # A tag with no propagation is considered to have # local scope and is used within the process where it's created. - def set_no_propagation - @ttl = NO_PROPAGATION + # + # @param [String] key Tag key + # Maximum allowed length of the key is {MAX_LENGTH} and must contains + # only printable characters. + # @param [String] value Tag value + # Maximum allowed length of the key is {MAX_LENGTH}. Value can be a + # nil value but if it present it must contains only printable + # characters. + # @raise [InvalidTagError] If invalid tag key or value. + # key: If key is empty, length grater then {MAX_LENGTH} characters or + # contains non printable characters + # value: If value length grater then 255 characters + # or contains non printable characters + # @return [Tag] + def self.create_non_propagative_tag key, value + new key, value, ttl: NO_PROPAGATION end - # Set unlimited propagation - # A tag unlimited propagation can propagate unlimited hops. + # Create tag with unlimited propagation + # # It is typical used to track a request, which may be processed across # multiple entities. - def set_unlimited_propagation - @ttl = UNLIMITED_PROPAGATION + # @param [String] key Tag key + # Maximum allowed length of the key is {MAX_LENGTH} and must contains + # only printable characters. + # @param [String] value Tag value + # Maximum allowed length of the key is {MAX_LENGTH}. Value can be a + # nil value but if it present it must contains only printable + # characters. + # @raise [InvalidTagError] If invalid tag key or value. + # key: If key is empty, length grater then {MAX_LENGTH} characters or + # contains non printable characters + # value: If value length grater then 255 characters + # or contains non printable characters + # @return [Tag] + def self.create_unlimited_propagative_tag key, value + new key, value end private diff --git a/lib/opencensus/tags/tag_map.rb b/lib/opencensus/tags/tag_map.rb index 396cfb53..7b12fdf4 100644 --- a/lib/opencensus/tags/tag_map.rb +++ b/lib/opencensus/tags/tag_map.rb @@ -55,6 +55,13 @@ module Tags # # Length # tag_map.length # 1 # + # @example Create tag map from hash + # + # tag_map = OpenCensus::Tags::OpenCensus.new({ + # "key1" => "val1", + # "key2" => "val2" + # }) + # # @example Create tag map with list of tags. # # tag_map = OpenCensus::Tags::OpenCensus.new([ @@ -67,11 +74,20 @@ class TagMap # Create a tag map. # - # @param [Array] tags Tags list with string key and value and - # metadata. - # - def initialize tags = [] - @tags = tags.each_with_object({}) { |tag, r| r[tag.key] = tag } + # @param [Hash, Array, nil] tags Tags list with + # string key and value and metadata. + def initialize tags = nil + @tags = case tags + when Hash + tags.each_with_object({}) do |(key, value), r| + tag = Tag.new key, value + r[tag.key] = tag + end + when Array + tags.each_with_object({}) { |tag, r| r[tag.key] = tag } + else + {} + end end # Insert tag. @@ -83,7 +99,7 @@ def << tag # Get all tags # - # @returns [Array] + # @return [Array] def tags @tags.values end diff --git a/test/stats/measure_test.rb b/test/stats/measure_test.rb index 1b018fe4..5aa1630f 100644 --- a/test/stats/measure_test.rb +++ b/test/stats/measure_test.rb @@ -1,6 +1,6 @@ require "test_helper" -describe OpenCensus::Stats::Measurement do +describe OpenCensus::Stats::Measure do let(:tag_key) { "key1" } let(:tag_value) { "val1" } let(:tag){ diff --git a/test/stats/measurement_test.rb b/test/stats/measurement_test.rb new file mode 100644 index 00000000..c63fdae6 --- /dev/null +++ b/test/stats/measurement_test.rb @@ -0,0 +1,48 @@ +require "test_helper" + +describe OpenCensus::Stats::Measurement do + let(:tag_key) { "key1" } + let(:tag_value) { "val1" } + let(:tag){ + OpenCensus::Tags::Tag.new tag_key, tag_value + } + let(:measure){ + OpenCensus::Stats::Measure.new( + name: "storage", + unit: "kb", + type: OpenCensus::Stats::Measure::DOUBLE_TYPE, + description: "storage desc" + ) + } + + describe "create" do + it "create using tag map instance" do + tags = OpenCensus::Tags::TagMap.new [tag] + + measurement = OpenCensus::Stats::Measurement.new( + measure: measure, + value: 10.10, + tags: tags + ) + measurement.measure.must_equal measure + measurement.value.must_equal 10.10 + measurement.tags.must_be_kind_of OpenCensus::Tags::TagMap + measurement.tags[tag_key].value.must_equal tag_value + measurement.time.must_be_kind_of Time + end + + it "create using hash" do + tags = { tag_key => tag_value } + measurement = OpenCensus::Stats::Measurement.new( + measure: measure, + value: 10.10, + tags: tags + ) + measurement.measure.must_equal measure + measurement.value.must_equal 10.10 + measurement.tags.must_be_kind_of OpenCensus::Tags::TagMap + measurement.tags[tag_key].value.must_equal tag_value + measurement.time.must_be_kind_of Time + end + end +end diff --git a/test/tags/tag_map_test.rb b/test/tags/tag_map_test.rb index 1e01b961..125ba6f3 100644 --- a/test/tags/tag_map_test.rb +++ b/test/tags/tag_map_test.rb @@ -14,7 +14,13 @@ tag_map.length.must_equal 0 end - it "create tags map with tags key, values" do + it "create tags map from tags key, values" do + tag_map = OpenCensus::Tags::TagMap.new({ tag_key => tag_value }) + tag_map.length.must_equal 1 + tag_map[tag_key].value.must_equal tag_value + end + + it "create tag map from tags array" do tag_map = OpenCensus::Tags::TagMap.new(tags) tag_map.length.must_equal 1 tag_map[tag_key].value.must_equal tag_value diff --git a/test/tags/tag_test.rb b/test/tags/tag_test.rb index 1ebac2b1..f84b23a3 100644 --- a/test/tags/tag_test.rb +++ b/test/tags/tag_test.rb @@ -71,17 +71,21 @@ end end - describe "tag ttl" do - it "set no propagation" do - tag = OpenCensus::Tags::Tag.new "key", "val" - tag.set_no_propagation + describe "create tag with ttl" do + it "no propagation" do + tag = OpenCensus::Tags::Tag.create_non_propagative_tag "key", "val" + tag.key.must_equal "key" + tag.value.must_equal "val" tag.ttl.must_equal 0 + tag.propagate?.must_equal false end - it "set unlimited propagation" do - tag = OpenCensus::Tags::Tag.new "key", "val" - tag.set_unlimited_propagation + it "unlimited propagation" do + tag = OpenCensus::Tags::Tag.create_unlimited_propagative_tag "key", "val" + tag.key.must_equal "key" + tag.value.must_equal "val" tag.ttl.must_equal(-1) + tag.propagate?.must_equal true end end end From 15d913e54150073f23d91746773d4a5d19b04390 Mon Sep 17 00:00:00 2001 From: Jiren Patel Date: Sat, 27 Apr 2019 20:28:36 +0530 Subject: [PATCH 4/4] Updated tagmap delegated methods and binary formatter --- lib/opencensus/tags/formatters/binary.rb | 2 +- lib/opencensus/tags/tag_map.rb | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/opencensus/tags/formatters/binary.rb b/lib/opencensus/tags/formatters/binary.rb index f131c45f..c0696059 100644 --- a/lib/opencensus/tags/formatters/binary.rb +++ b/lib/opencensus/tags/formatters/binary.rb @@ -64,7 +64,7 @@ class BinaryFormatterError < StandardError; end def serialize tags_context binary = [int_to_varint(VERSION_ID)] - tags_context.each do |tag| + tags_context.each do |_, tag| next unless tag.propagate? binary << int_to_varint(TAG_FIELD_ID) diff --git a/lib/opencensus/tags/tag_map.rb b/lib/opencensus/tags/tag_map.rb index 7b12fdf4..298fe7f1 100644 --- a/lib/opencensus/tags/tag_map.rb +++ b/lib/opencensus/tags/tag_map.rb @@ -118,12 +118,12 @@ def delete key end # @!method each - # @see Array#each + # @see Hash#each # @!method length - # @see Array#length + # @see Hash#length # @!method empty? - # @see Array#empty? - def_delegators :tags, :each, :length, :empty? + # @see Hash#empty? + def_delegators :@tags, :each, :length, :empty? # Convert tag map to binary string format. #