diff --git a/lib/inspec.rb b/lib/inspec.rb index cae64b608..722b3bcd8 100644 --- a/lib/inspec.rb +++ b/lib/inspec.rb @@ -20,6 +20,7 @@ require 'inspec/input_registry' require 'inspec/rspec_extensions' require 'inspec/globals' require 'inspec/impact' +require 'inspec/utils/telemetry' require 'inspec/plugin/v2' require 'inspec/plugin/v1' diff --git a/lib/inspec/utils/telemetry.rb b/lib/inspec/utils/telemetry.rb new file mode 100644 index 000000000..e97bc8ff7 --- /dev/null +++ b/lib/inspec/utils/telemetry.rb @@ -0,0 +1,3 @@ +require 'inspec/utils/telemetry/collector' +require 'inspec/utils/telemetry/data_series' +require 'inspec/utils/telemetry/global_methods' diff --git a/lib/inspec/utils/telemetry/collector.rb b/lib/inspec/utils/telemetry/collector.rb new file mode 100644 index 000000000..fbde84f17 --- /dev/null +++ b/lib/inspec/utils/telemetry/collector.rb @@ -0,0 +1,66 @@ +require 'inspec/utils/telemetry/data_series' +require 'singleton' + +module Inspec::Telemetry + # A Singleton collection of data series objects. + class Collector + include Singleton + + def initialize + @data_series = [] + @enabled = true + end + + # Add a data series to the collection. + # @return [True] + def add_data_series(data_series) + @data_series << data_series + end + + # Is the Telemetry system enabled or disabled? + # Always true until we add configuration parsing. + # @return [True, False] + def telemetry_enabled? + @enabled + end + + # A way to disable the telemetry system. + # @return [True] + def disable_telemetry + @enabled = false + end + + # The entire data series collection. + # @return [Array] + def list_data_series + @data_series + end + + # Finds the data series object with the specified name and returns it. + # If it does not exist then creates a new data series with that name + # and returns it. + # @return [Inspec::Telemetry::DataSeries] + def find_or_create_data_series(name) + ds = @data_series.select { |data_series| data_series.name.eql?(name) } + if ds.empty? + new_data_series = Inspec::Telemetry::DataSeries.new(name) + @data_series << new_data_series + new_data_series + else + ds.first + end + end + + # Lists all the data series objects that match the specified name. + # @return [Array] + def list_data_series_by_name(name) + @data_series.select { |ds| ds.name.eql?(name) } + end + + # Blanks the contents of the data series collection. + # @return [True] + def reset + @data_series = [] + end + end +end diff --git a/lib/inspec/utils/telemetry/data_series.rb b/lib/inspec/utils/telemetry/data_series.rb new file mode 100644 index 000000000..cadc2aa55 --- /dev/null +++ b/lib/inspec/utils/telemetry/data_series.rb @@ -0,0 +1,45 @@ +require 'json' + +# A minimal Dataseries Object +# Stores the name of the data series and an array of data. +# Stored data should be a object that supports #to_s +module Inspec::Telemetry + class DataSeries + def initialize(name) + @name = name + @enabled = true + end + + attr_reader :name + + def data + @data ||= [] + end + + # This needs to also be set by configuration. + def enabled? + @enabled + end + + def disable + @enabled = false + end + + def <<(appending_data) + data << appending_data + end + + alias push << + + def to_h + { + name: @name, + data: @data, + } + end + + def to_json + to_h.to_json + end + end +end diff --git a/lib/inspec/utils/telemetry/global_methods.rb b/lib/inspec/utils/telemetry/global_methods.rb new file mode 100644 index 000000000..7aa5a424e --- /dev/null +++ b/lib/inspec/utils/telemetry/global_methods.rb @@ -0,0 +1,22 @@ +require 'inspec/utils/telemetry' + +module Inspec + # A Global method to add a data series object to the Telemetry Collection. + # `data_series_name`s are unique, so `:dependency_group` will always return + # the same object. + # `data_point` is optional, you may also supply a block with several data points. + # All data points should allow #to_s + # @return [True] + def self.record_telemetry_data(data_series_name, data_point = nil) + coll = Inspec::Telemetry::Collector.instance + return unless coll.telemetry_enabled? + ds = coll.find_or_create_data_series(data_series_name) + return unless ds.enabled? + + if block_given? + ds << yield + else + ds << data_point + end + end +end diff --git a/test/unit/utils/telemetry/collector_test.rb b/test/unit/utils/telemetry/collector_test.rb new file mode 100644 index 000000000..ad8d09147 --- /dev/null +++ b/test/unit/utils/telemetry/collector_test.rb @@ -0,0 +1,46 @@ +require 'inspec/utils/telemetry' +require_relative '../../../helper.rb' + +class TestTelemetryCollector < Minitest::Test + def setup + @collector = Inspec::Telemetry::Collector.instance + @collector.reset + end + + def test_collector_singleton + assert_equal Inspec::Telemetry::Collector.instance, @collector + end + + def test_add_data_series + data_series = Inspec::Telemetry::DataSeries.new('/resource/File') + assert @collector.add_data_series(data_series) + end + + def test_list_data_series + assert @collector.list_data_series + end + + def test_find_or_create_data_series + dg = @collector.find_or_create_data_series(:deprecation_group) + assert_kind_of Inspec::Telemetry::DataSeries, dg + assert_equal :deprecation_group, dg.name + assert_equal @collector.find_or_create_data_series(:deprecation_group), dg + end + + def test_list_data_series_names + data_series = Inspec::Telemetry::DataSeries.new('/resource/File') + data_series2 = Inspec::Telemetry::DataSeries.new('/resource/Dir') + @collector.add_data_series(data_series) + @collector.add_data_series(data_series2) + list = @collector.list_data_series_by_name('/resource/File') + assert_kind_of Array, list + assert_equal 1, list.count + end + + def test_reset_singleton + data_series = Inspec::Telemetry::DataSeries.new('/resource/File') + @collector.add_data_series(data_series) + @collector.reset + assert_equal 0, @collector.list_data_series.count + end +end diff --git a/test/unit/utils/telemetry/data_series_test.rb b/test/unit/utils/telemetry/data_series_test.rb new file mode 100644 index 000000000..cd7b3ba1c --- /dev/null +++ b/test/unit/utils/telemetry/data_series_test.rb @@ -0,0 +1,45 @@ +require 'inspec/utils/telemetry' +require 'json' +require_relative '../../../helper.rb' + +class TestTelemetryDataSeries < Minitest::Test + def test_name + ds = Inspec::Telemetry::DataSeries.new('fizz') + refute_nil ds + assert_equal 'fizz', ds.name + end + + def test_data + ds = Inspec::Telemetry::DataSeries.new('fizz') + refute_nil ds.data + assert_kind_of Array, ds.data + assert_empty ds.data + end + + def test_data_append + ds = Inspec::Telemetry::DataSeries.new('fizz') + assert_empty ds.data + assert ds << 'foo' + assert_equal ['foo'], ds.data + end + + def test_data_push_alias + ds = Inspec::Telemetry::DataSeries.new('fizz') + assert_empty ds.data + assert ds.push 'bar' + assert_equal ['bar'], ds.data + end + + def test_to_h + ds = Inspec::Telemetry::DataSeries.new('fizz') + ds << 'foo' + assert_kind_of Hash, ds.to_h + end + + def test_to_json + ds = Inspec::Telemetry::DataSeries.new('fizz') + ds << 'foo' + assert_kind_of String, ds.to_json + assert JSON.parse(ds.to_json) + end +end diff --git a/test/unit/utils/telemetry/global_methods_test.rb b/test/unit/utils/telemetry/global_methods_test.rb new file mode 100644 index 000000000..bd8efc6a3 --- /dev/null +++ b/test/unit/utils/telemetry/global_methods_test.rb @@ -0,0 +1,27 @@ +require 'inspec/utils/telemetry' +require_relative '../../../helper.rb' + +class TestTelemetryGlobalMethods < Minitest::Test + def setup + @collector = Inspec::Telemetry::Collector.instance + @collector.reset + end + + def test_record_telemetry_data + assert Inspec.record_telemetry_data(:deprecation_group, 'serverspec_compat') + + depgrp = @collector.find_or_create_data_series(:deprecation_group) + assert_equal ['serverspec_compat'], depgrp.data + assert_equal :deprecation_group, depgrp.name + end + + def test_record_telemetry_data_with_block + Inspec.record_telemetry_data(:deprecation_group) do + 'serverspec_compat' + end + + depgrp = @collector.find_or_create_data_series(:deprecation_group) + assert_equal ['serverspec_compat'], depgrp.data + assert_equal :deprecation_group, depgrp.name + end +end