diff --git a/lib/elastic_apm/span.rb b/lib/elastic_apm/span.rb index 70a50eb64..dfd0a1cf0 100644 --- a/lib/elastic_apm/span.rb +++ b/lib/elastic_apm/span.rb @@ -144,6 +144,8 @@ def inspect "' end diff --git a/lib/elastic_apm/subscriber.rb b/lib/elastic_apm/subscriber.rb index 8aa54db81..9537764bd 100644 --- a/lib/elastic_apm/subscriber.rb +++ b/lib/elastic_apm/subscriber.rb @@ -45,6 +45,7 @@ def unregister! # AS::Notifications API Notification = Struct.new(:id, :span) + def start(name, id, payload) return unless (transaction = @agent.current_transaction) diff --git a/spec/entrypoint.sh b/spec/entrypoint.sh index 73894ac89..d40f60b4c 100755 --- a/spec/entrypoint.sh +++ b/spec/entrypoint.sh @@ -1,7 +1,7 @@ #!/bin/bash set -x -bundle check || (rm Gemfile.lock && bundle) +bundle check || (rm -f Gemfile.lock && bundle) # If first arg is a spec path, run spec(s) if [[ $1 == spec/* ]]; then diff --git a/spec/fixtures/span_types.json b/spec/fixtures/span_types.json new file mode 100644 index 000000000..af54b9a84 --- /dev/null +++ b/spec/fixtures/span_types.json @@ -0,0 +1,279 @@ +{ + "__description": { + "": "root element for type identified by ''", + ".__description": "description for '' (optional)", + ".__used_by": "list of agents that use '' to help document alignment (optional)", + ".allow_null_subtype": "true to allow null subtype, false by default if omitted", + ".allow_unlisted_subtype": "true to allow unlisted subtypes, false by default if omitted", + ".subtypes": "root element for sub-types of type '', if omitted or empty subtype must be null, unless 'allow_unlisted_subtype' is set to true", + ".subtypes.": "sub-type element for ", + ".subtypes..__description": "description of subtype (optional)", + ".subtypes..__used_by": "list of agents that use to help document alignment (optional)" + }, + "app": { + "allow_null_subtype": true, + "subtypes": { + "inferred": { + "__description": "Sampling profiler inferred spans", + "__used_by": [ + "java" + ] + }, + "controller": { + "__description": "Application controller actions", + "__used_by": [ + "ruby" + ] + }, + "graphql": { + "__description": "Incoming GraphQL requests", + "__used_by": [ + "ruby" + ] + }, + "mailer": { + "__description": "Application mailer actions", + "__used_by": [ + "ruby" + ] + }, + "resource": { + "__description": "Application resource actions", + "__used_by": [ + "ruby" + ] + } + } + }, + "custom": { + "__description": "API custom instrumentation", + "__used_by": [ + "java", + "ruby" + ], + "allow_null_subtype": true + }, + "db": { + "__description": "database span", + "subtypes": { + "cassandra": { + "__description": "Cassandra", + "__used_by": [ + "java" + ] + }, + "cosmosdb": { + "__description": "Azure CosmosDB" + }, + "db2": { + "__description": "IBM DB2", + "__used_by": [ + "java" + ] + }, + "derby": { + "__description": "Apache Derby", + "__used_by": [ + "java" + ] + }, + "dynamodb": { + "__description": "AWS DynamoDB", + "__used_by": [ + "ruby" + ] + }, + "elasticsearch": { + "__description": "Elasticsearch", + "__used_by": [ + "java", + "ruby" + ] + }, + "h2": { + "__description": "H2", + "__used_by": [ + "java" + ] + }, + "hsqldb": { + "__description": "HSQLDB", + "__used_by": [ + "java" + ] + }, + "ingres": { + "__description": "Ingres" + }, + "mariadb": { + "__description": "MariaDB", + "__used_by": [ + "java", + "ruby" + ] + }, + "mongodb": { + "__description": "MongoDB", + "__used_by": [ + "java", + "ruby" + ] + }, + "mysql": { + "__description": "MySQL", + "__used_by": [ + "java", + "ruby" + ] + }, + "oracle": { + "__description": "Oracle Database", + "__used_by": [ + "java" + ] + }, + "postgresql": { + "__description": "PostgreSQL", + "__used_by": [ + "ruby" + ] + }, + "redis": { + "__description": "Redis", + "__used_by": [ + "java", + "ruby" + ] + }, + "sqlite": { + "__description": "SQLite", + "__used_by": [ + "ruby" + ] + }, + "sqlite3": { + "__description": "SQLite 3", + "__used_by": [ + "ruby" + ] + }, + "sqlserver": { + "__description": "Microsoft SQL Server", + "__used_by": [ + "java" + ] + }, + "unknown": { + "__description": "Unknown database", + "__used_by": [ + "java", + "ruby" + ] + } + } + }, + "external": { + "subtypes": { + "dubbo": { + "__description": "Apache Dubbo" + }, + "grpc": { + "__description": "gRPC", + "__used_by": [ + "ruby" + ] + }, + "http": { + "__description": "HTTP client", + "__used_by": [ + "ruby" + ] + } + } + }, + "json": { + "__description": "JSON parsing and generation", + "subtypes": { + "parse": { + "__description": "JSON parsing" + }, + "generate": { + "__description": "JSON generation" + } + }, + "__used_by": [ + "ruby" + ] + }, + "messaging": { + "__description": "Messaging", + "subtypes": { + "azurequeue": { + "__description": "Azure Queue" + }, + "azureservicebus": { + "__description": "Azure Service Bus" + }, + "jms": { + "__description": "Java Messaging Service", + "__used_by": [ + "java" + ] + }, + "kafka": { + "__description": "Apache Kafka", + "__used_by": [ + "java" + ] + }, + "rabbitmq": { + "__description": "RabbitMQ", + "__used_by": [ + "java" + ] + }, + "sns": { + "__description": "AWS Simple Notification Service", + "__used_by": [ + "ruby" + ] + }, + "sqs": { + "__description": "AWS Simple Queue Service", + "__used_by": [ + "ruby" + ] + } + } + }, + "process": { + "__description": "External process", + "__used_by": [ + "java" + ] + }, + "storage": { + "subtypes": { + "azurefile": { + "__description": "Azure Files" + }, + "azuresblob": { + "__description": "Azure Blob Storage" + }, + "s3": { + "__description": "AWS S3", + "__used_by": [ + "ruby" + ] + } + } + }, + "template": { + "__description": "Template engines (no sub-type for now as really platform-specific)", + "__used_by": [ + "java", + "ruby" + ], + "allow_unlisted_subtype": true + } +} diff --git a/spec/support/intercept.rb b/spec/support/intercept.rb index ce1a6f10d..b7b2232d4 100644 --- a/spec/support/intercept.rb +++ b/spec/support/intercept.rb @@ -24,6 +24,8 @@ def initialize @spans = [] @errors = [] @metricsets = [] + + @span_types = JSON.parse(File.read('./spec/fixtures/span_types.json')) end attr_reader :transactions, :spans, :errors, :metricsets @@ -33,6 +35,7 @@ def submit(obj) when ElasticAPM::Transaction transactions << obj when ElasticAPM::Span + validate_span!(obj) spans << obj when ElasticAPM::Error errors << obj @@ -46,6 +49,26 @@ def submit(obj) def start; end def stop; end + + def validate_span!(span) + type, subtype = [span.type, span.subtype] + + begin + info = @span_types.fetch(type) + rescue KeyError + raise "Unknown span.type `#{type}'\nPossible types: #{@span_types.keys.join(', ')}" + end + + return unless (allowed_subtypes = info['subtypes']) + + if !info['allow_null_subtype'] && !subtype + raise "span.subtype missing when required,\nPossible subtypes: #{allowed_subtypes}" + end + + allowed_subtypes.fetch(subtype) unless info['allow_unlisted_subtype'] + rescue KeyError + raise "Unknown span.subtype `#{span.type}'\nPossible subtypes: #{allowed_subtypes}" + end end module Methods diff --git a/spec/support/mock_intake.rb b/spec/support/mock_intake.rb index 173a99d91..d46bee1d4 100644 --- a/spec/support/mock_intake.rb +++ b/spec/support/mock_intake.rb @@ -24,6 +24,8 @@ class MockIntake def initialize clear! + + @span_types = JSON.parse(File.read('./spec/fixtures/span_types.json')) end attr_reader( @@ -136,10 +138,42 @@ def gunzip(string) def catalog(obj) case obj.keys.first when 'transaction' then transactions << obj.values.first - when 'span' then spans << obj.values.first when 'error' then errors << obj.values.first when 'metricset' then metricsets << obj.values.first + when 'span' + validate_span!(obj.values.first) + spans << obj.values.first + end + end + + def validate_span!(span) + type, subtype, _action = span['type'].split('.') + + begin + info = @span_types.fetch(type) + rescue KeyError + puts "Unknown span.type `#{type}'\nPossible types: #{@span_types.keys.join(', ')}" + pp span + raise end + + return unless (allowed_subtypes = info['subtypes']) + + if !info['optional_subtype'] && !subtype + msg = "span.subtype missing when required for type `#{type}',\n" \ + "Possible subtypes: #{allowed_subtypes}" + puts msg # print because errors are swallowed + pp span + raise msg + end + + allowed_subtypes.fetch(subtype) + rescue KeyError => e + puts "Unknown span.subtype `#{subtype.inspect}'\n" \ + "Possible subtypes: #{allowed_subtypes}" + pp span + puts e # print because errors are swallowed + raise end module WaitFor