diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1a6cc246c9..e77afb9399 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: flags: --include-simple --include-appraisal --check-rubocop --check-yard - name: JRuby container: circleci/jruby:latest - flags: --include-simple --exclude opentelemetry-resource_detectors + flags: --include-simple --exclude opentelemetry-resource_detectors --exclude opentelemetry-exporter-otlp fail-fast: false name: Test ${{ matrix.name }} (${{ matrix.flags }}) runs-on: ubuntu-latest diff --git a/.toys/.data/releases.yml b/.toys/.data/releases.yml index 926285da50..1e0883c58e 100644 --- a/.toys/.data/releases.yml +++ b/.toys/.data/releases.yml @@ -16,6 +16,10 @@ gems: directory: exporter/jaeger version_constant: [OpenTelemetry, Exporter, Jaeger, VERSION] + - name: opentelemetry-exporter-otlp + directory: exporter/otlp + version_constant: [OpenTelemetry, Exporter, OTLP, VERSION] + - name: opentelemetry-instrumentation-all directory: instrumentation/all version_constant: [OpenTelemetry, Instrumentation, All, VERSION] diff --git a/exporter/otlp/.rubocop.yml b/exporter/otlp/.rubocop.yml new file mode 100644 index 0000000000..0441b05385 --- /dev/null +++ b/exporter/otlp/.rubocop.yml @@ -0,0 +1,21 @@ +AllCops: + TargetRubyVersion: "2.5.0" + Exclude: + - "lib/opentelemetry/proto/**/*" + - "vendor/**/*" + +Lint/UnusedMethodArgument: + Enabled: false +Metrics/AbcSize: + Max: 18 +Metrics/LineLength: + Enabled: false +Metrics/MethodLength: + Max: 21 +Metrics/ParameterLists: + Enabled: false +Naming/FileName: + Exclude: + - "lib/opentelemetry-exporter-otlp.rb" +Style/ModuleFunction: + Enabled: false diff --git a/exporter/otlp/CHANGELOG.md b/exporter/otlp/CHANGELOG.md new file mode 100644 index 0000000000..5768479c8c --- /dev/null +++ b/exporter/otlp/CHANGELOG.md @@ -0,0 +1 @@ +# Release History: opentelemetry-exporter-otlp diff --git a/exporter/otlp/Gemfile b/exporter/otlp/Gemfile new file mode 100644 index 0000000000..39f2c26fc9 --- /dev/null +++ b/exporter/otlp/Gemfile @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +source 'https://rubygems.org' + +gemspec + +# Use the opentelemetry-api gem from source +gem 'opentelemetry-api', path: '../../api' diff --git a/exporter/otlp/LICENSE b/exporter/otlp/LICENSE new file mode 100644 index 0000000000..b7fbe3acd1 --- /dev/null +++ b/exporter/otlp/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2020 OpenTelemetry 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. diff --git a/exporter/otlp/README.md b/exporter/otlp/README.md new file mode 100644 index 0000000000..7ac2ab4108 --- /dev/null +++ b/exporter/otlp/README.md @@ -0,0 +1,87 @@ +# opentelemetry-exporter-otlp + +The `opentelemetry-exporter-otlp` gem provides an [OTLP](https://github.com/open-telemetry/opentelemetry-proto) exporter for OpenTelemetry for Ruby. Using `opentelemetry-exporter-otlp`, an application can configure OpenTelemetry to export collected tracing data to [the OpenTelemetry Collector][opentelemetry-collector-home]. + +## What is OpenTelemetry? + +[OpenTelemetry][opentelemetry-home] is an open source observability framework, providing a general-purpose API, SDK, and related tools required for the instrumentation of cloud-native software, frameworks, and libraries. + +OpenTelemetry provides a single set of APIs, libraries, agents, and collector services to capture distributed traces and metrics from your application. You can analyze them using Prometheus, Jaeger, and other observability tools. + +## How does this gem fit in? + +The `opentelemetry-exporter-otlp` gem is a plugin that provides OTLP export. To export to the OpenTelemetry Collector, an application can include this gem along with `opentelemetry-sdk`, and configure the `SDK` to use the provided OTLP exporter as a span processor. + +Generally, *libraries* that produce telemetry data should avoid depending directly on specific exporter, deferring that choice to the application developer. + +### Supported protocol version + +This gem supports the [v0.4.0 release](https://github.com/open-telemetry/opentelemetry-proto/releases/tag/v0.4.0) of OTLP. + +## How do I get started? + +Install the gem using: + +``` +gem install opentelemetry-sdk +gem install opentelemetry-exporter-otlp +``` + +Or, if you use [bundler][bundler-home], include `opentelemetry-sdk` in your `Gemfile`. + +Then, configure the SDK to use the OTLP exporter as a span processor, and use the OpenTelemetry interfaces to produces traces and other information. Following is a basic example. + +```ruby +require 'opentelemetry/sdk' +require 'opentelemetry/exporter/otlp' + +# Configure the sdk with custom export +OpenTelemetry::SDK.configure do |c| + c.add_span_processor( + OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new( + OpenTelemetry::Exporter::OTLP::Exporter.new( + host: 'localhost', port: 55680 + ) + ) + ) +end + +# To start a trace you need to get a Tracer from the TracerProvider +tracer = OpenTelemetry.tracer_provider.tracer('my_app_or_gem', '0.1.0') + +# create a span +tracer.in_span('foo') do |span| + # set an attribute + span.set_attribute('platform', 'osx') + # add an event + span.add_event(name: 'event in bar') + # create bar as child of foo + tracer.in_span('bar') do |child_span| + # inspect the span + pp child_span + end +end +``` + +For additional examples, see the [examples on github][examples-github]. + +## How can I get involved? + +The `opentelemetry-exporter-otlp` gem source is [on github][repo-github], along with related gems including `opentelemetry-sdk`. + +The OpenTelemetry Ruby gems are maintained by the OpenTelemetry-Ruby special interest group (SIG). You can get involved by joining us on our [gitter channel][ruby-gitter] or attending our weekly meeting. See the [meeting calendar][community-meetings] for dates and times. For more information on this and other language SIGs, see the OpenTelemetry [community page][ruby-sig]. + +## License + +The `opentelemetry-exporter-otlp` gem is distributed under the Apache 2.0 license. See [LICENSE][license-github] for more information. + + +[opentelemetry-collector-home]: https://opentelemetry.io/docs/collector/about/ +[opentelemetry-home]: https://opentelemetry.io +[bundler-home]: https://bundler.io +[repo-github]: https://github.com/open-telemetry/opentelemetry-ruby +[license-github]: https://github.com/open-telemetry/opentelemetry-ruby/blob/master/LICENSE +[examples-github]: https://github.com/open-telemetry/opentelemetry-ruby/tree/master/examples +[ruby-sig]: https://github.com/open-telemetry/community#ruby-sig +[community-meetings]: https://github.com/open-telemetry/community#community-meetings +[ruby-gitter]: https://gitter.im/open-telemetry/opentelemetry-ruby diff --git a/exporter/otlp/Rakefile b/exporter/otlp/Rakefile new file mode 100644 index 0000000000..511f7665d9 --- /dev/null +++ b/exporter/otlp/Rakefile @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'bundler/gem_tasks' +require 'rake/testtask' +require 'yard' + +require 'rubocop/rake_task' +RuboCop::RakeTask.new + +Rake::TestTask.new :test do |t| + t.libs << 'test' + t.libs << 'lib' + t.libs << '../../api/lib' + t.libs << '../../sdk/lib' + t.test_files = FileList['test/**/*_test.rb'] +end + +YARD::Rake::YardocTask.new do |t| + t.stats_options = ['--list-undoc'] +end + +if RUBY_ENGINE == 'truffleruby' + task default: %i[test] +else + task default: %i[test rubocop yard] +end + +PROTOBUF_FILES = [ + 'common/v1/common.proto', + 'resource/v1/resource.proto', + 'trace/v1/trace.proto', + 'collector/trace/v1/trace_service.proto' +].freeze + +task :update_protobuf do + system('git clone https://github.com/open-telemetry/opentelemetry-proto') + PROTOBUF_FILES.each do |file| + system("protoc --ruby_out=lib/ --proto_path=opentelemetry-proto opentelemetry/proto/#{file}") + end + system('rm -rf opentelemetry-proto') +end diff --git a/exporter/otlp/lib/opentelemetry-exporter-otlp.rb b/exporter/otlp/lib/opentelemetry-exporter-otlp.rb new file mode 100644 index 0000000000..5c97153c35 --- /dev/null +++ b/exporter/otlp/lib/opentelemetry-exporter-otlp.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'opentelemetry/exporter/otlp' diff --git a/exporter/otlp/lib/opentelemetry/exporter/otlp.rb b/exporter/otlp/lib/opentelemetry/exporter/otlp.rb new file mode 100644 index 0000000000..58f4864621 --- /dev/null +++ b/exporter/otlp/lib/opentelemetry/exporter/otlp.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'opentelemetry/exporter/otlp/exporter' +require 'opentelemetry/exporter/otlp/version' + +# OpenTelemetry is an open source observability framework, providing a +# general-purpose API, SDK, and related tools required for the instrumentation +# of cloud-native software, frameworks, and libraries. +# +# The OpenTelemetry module provides global accessors for telemetry objects. +# See the documentation for the `opentelemetry-api` gem for details. +module OpenTelemetry +end diff --git a/exporter/otlp/lib/opentelemetry/exporter/otlp/exporter.rb b/exporter/otlp/lib/opentelemetry/exporter/otlp/exporter.rb new file mode 100644 index 0000000000..c1c78b57f8 --- /dev/null +++ b/exporter/otlp/lib/opentelemetry/exporter/otlp/exporter.rb @@ -0,0 +1,292 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'opentelemetry/sdk' +require 'net/http' +require 'csv' + +require 'opentelemetry/proto/common/v1/common_pb' +require 'opentelemetry/proto/resource/v1/resource_pb' +require 'opentelemetry/proto/trace/v1/trace_pb' +require 'opentelemetry/proto/collector/trace/v1/trace_service_pb' + +module OpenTelemetry + module Exporter + module OTLP + # An OpenTelemetry trace exporter that sends spans over HTTP as Protobuf encoded OTLP ExportTraceServiceRequests. + class Exporter # rubocop:disable Metrics/ClassLength + SUCCESS = OpenTelemetry::SDK::Trace::Export::SUCCESS + FAILURE = OpenTelemetry::SDK::Trace::Export::FAILURE + private_constant(:SUCCESS, :FAILURE) + + # Default timeouts in seconds. + KEEP_ALIVE_TIMEOUT = 30 + OPEN_TIMEOUT = 5 + READ_TIMEOUT = 5 + RETRY_COUNT = 5 + private_constant(:KEEP_ALIVE_TIMEOUT, :OPEN_TIMEOUT, :READ_TIMEOUT, :RETRY_COUNT) + + def initialize(endpoint: config_opt('OTEL_EXPORTER_OTLP_SPAN_ENDPOINT', 'OTEL_EXPORTER_OTLP_ENDPOINT', default: 'localhost:55681/v1/trace'), # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity + insecure: config_opt('OTEL_EXPORTER_OTLP_SPAN_INSECURE', 'OTEL_EXPORTER_OTLP_INSECURE', default: false), + certificate_file: config_opt('OTEL_EXPORTER_OTLP_SPAN_CERTIFICATE', 'OTEL_EXPORTER_OTLP_CERTIFICATE'), + headers: config_opt('OTEL_EXPORTER_OTLP_SPAN_HEADERS', 'OTEL_EXPORTER_OTLP_HEADERS'), # TODO: what format is expected here? + compression: config_opt('OTEL_EXPORTER_OTLP_SPAN_COMPRESSION', 'OTEL_EXPORTER_OTLP_COMPRESSION'), + timeout: config_opt('OTEL_EXPORTER_OTLP_SPAN_TIMEOUT', 'OTEL_EXPORTER_OTLP_TIMEOUT', default: 10)) + raise ArgumentError, "invalid url for OTLP::Exporter #{endpoint}" if invalid_url?("http://#{endpoint}") + raise ArgumentError, "unsupported compression key #{compression}" unless compression.nil? + raise ArgumentError, 'headers must be comma-separated k:v pairs or a Hash' unless valid_headers?(headers) + + uri = URI "http://#{endpoint}" + @http = Net::HTTP.new(uri.host, uri.port) + @http.use_ssl = insecure.to_s.downcase == 'false' + @http.ca_file = certificate_file unless certificate_file.nil? + @http.keep_alive_timeout = KEEP_ALIVE_TIMEOUT + @http.open_timeout = OPEN_TIMEOUT + @http.read_timeout = READ_TIMEOUT + + @path = uri.path + @headers = case headers + when String then CSV.parse(headers, col_sep: ':', row_sep: ',').to_h + when Hash then headers + end + @timeout = timeout.to_f # TODO: use this as a default timeout when we implement timeouts in https://github.com/open-telemetry/opentelemetry-ruby/pull/341 + @tracer = OpenTelemetry.tracer_provider.tracer + + @shutdown = false + end + + # Called to export sampled {OpenTelemetry::SDK::Trace::SpanData} structs. + # + # @param [Enumerable] span_data the + # list of recorded {OpenTelemetry::SDK::Trace::SpanData} structs to be + # exported. + # @return [Integer] the result of the export. + def export(span_data) + return FAILURE if @shutdown + + send_bytes(encode(span_data)) + end + + # Called when {OpenTelemetry::SDK::Trace::Tracer#shutdown} is called, if + # this exporter is registered to a {OpenTelemetry::SDK::Trace::Tracer} + # object. + def shutdown + @shutdown = true + @http.finish if @http.started? + end + + private + + def config_opt(*env_vars, default: nil) + env_vars.each do |env_var| + val = ENV[env_var] + return val unless val.nil? + end + default + end + + def valid_headers?(headers) + return true if headers.nil? || headers.is_a?(Hash) + return false unless headers.is_a?(String) + + CSV.parse(headers, col_sep: ':', row_sep: ',').to_h + true + rescue ArgumentError + false + end + + def invalid_url?(url) + return true if url.nil? || url.strip.empty? + + uri = URI(url) + uri.path.nil? || uri.path.empty? + rescue URI::InvalidURIError + true + end + + def send_bytes(bytes) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity + retry_count = 0 + untraced do # rubocop:disable Metrics/BlockLength + request = Net::HTTP::Post.new(@path) + request.body = bytes + request.add_field('Content-Type', 'application/x-protobuf') + @headers&.each { |key, value| request.add_field(key, value) } + # TODO: enable gzip when https://github.com/open-telemetry/opentelemetry-collector/issues/1344 is fixed. + # request.add_field('Content-Encoding', 'gzip') + + @http.start unless @http.started? + response = @http.request(request) + + case response + when Net::HTTPOK + response.body # Read and discard body + SUCCESS + when Net::HTTPServiceUnavailable, Net::HTTPTooManyRequests + response.body # Read and discard body + redo if backoff?(retry_after: response['Retry-After'], retry_count: retry_count += 1) + FAILURE + when Net::HTTPRequestTimeOut, Net::HTTPGatewayTimeOut, Net::HTTPBadGateway + response.body # Read and discard body + redo if backoff?(retry_count: retry_count += 1) + FAILURE + when Net::HTTPBadRequest, Net::HTTPClientError, Net::HTTPServerError + # TODO: decode the body as a google.rpc.Status Protobuf-encoded message when https://github.com/open-telemetry/opentelemetry-collector/issues/1357 is fixed. + response.body # Read and discard body + FAILURE + when Net::HTTPRedirection + @http.finish + handle_redirect(response['location']) + redo if backoff?(retry_after: 0, retry_count: retry_count += 1) + else + @http.finish + FAILURE + end + rescue Net::OpenTimeout, Net::ReadTimeout + retry if backoff?(retry_count: retry_count += 1) + return FAILURE + end + end + + def handle_redirect(location) + # TODO: figure out destination and reinitialize @http and @path + end + + def untraced + @tracer.with_span(OpenTelemetry::Trace::Span.new) { yield } + end + + def backoff?(retry_after: nil, retry_count:, reason:) + return false if retry_count > RETRY_COUNT + + sleep_interval = nil + unless retry_after.nil? + sleep_interval = + begin + Integer(retry_after) + rescue ArgumentError + nil + end + sleep_interval ||= + begin + Time.httpdate(retry_after) - Time.now + rescue # rubocop:disable Style/RescueStandardError + nil + end + sleep_interval = nil unless sleep_interval&.positive? + end + sleep_interval ||= rand(2**retry_count) + + sleep(sleep_interval) + true + end + + def encode(span_data) # rubocop:disable Metrics/MethodLength + Opentelemetry::Proto::Collector::Trace::V1::ExportTraceServiceRequest.encode( + Opentelemetry::Proto::Collector::Trace::V1::ExportTraceServiceRequest.new( + resource_spans: span_data + .group_by(&:resource) + .map do |resource, span_datas| + Opentelemetry::Proto::Trace::V1::ResourceSpans.new( + resource: Opentelemetry::Proto::Resource::V1::Resource.new( + attributes: resource.label_enumerator.map { |key, value| as_otlp_key_value(key, value) } + ), + instrumentation_library_spans: span_datas + .group_by(&:instrumentation_library) + .map do |il, sds| + Opentelemetry::Proto::Trace::V1::InstrumentationLibrarySpans.new( + instrumentation_library: Opentelemetry::Proto::Common::V1::InstrumentationLibrary.new( + name: il.name, + version: il.version + ), + spans: sds.map { |sd| as_otlp_span(sd) } + ) + end + ) + end + ) + ) + end + + def as_otlp_span(span_data) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + Opentelemetry::Proto::Trace::V1::Span.new( + trace_id: span_data.trace_id, + span_id: span_data.span_id, + trace_state: span_data.tracestate, + parent_span_id: span_data.parent_span_id, + name: span_data.name, + kind: as_otlp_span_kind(span_data.kind), + start_time_unix_nano: as_otlp_timestamp(span_data.start_timestamp), + end_time_unix_nano: as_otlp_timestamp(span_data.end_timestamp), + attributes: span_data.attributes&.map { |k, v| as_otlp_key_value(k, v) }, + dropped_attributes_count: span_data.total_recorded_attributes - span_data.attributes&.size.to_i, + events: span_data.events&.map do |event| + Opentelemetry::Proto::Trace::V1::Span::Event.new( + time_unix_nano: as_otlp_timestamp(event.timestamp), + name: event.name, + attributes: event.attributes&.map { |k, v| as_otlp_key_value(k, v) } + # TODO: track dropped_attributes_count in Span#append_event + ) + end, + dropped_events_count: span_data.total_recorded_events - span_data.events&.size.to_i, + links: span_data.links&.map do |link| + Opentelemetry::Proto::Trace::V1::Span::Link.new( + trace_id: link.context.trace_id, + span_id: link.context.span_id, + trace_state: link.context.tracestate, + attributes: link.attributes&.map { |k, v| as_otlp_key_value(k, v) } + # TODO: track dropped_attributes_count in Span#trim_links + ) + end, + dropped_links_count: span_data.total_recorded_links - span_data.links&.size.to_i, + status: span_data.status&.yield_self do |status| + Opentelemetry::Proto::Trace::V1::Status.new( + code: status.canonical_code, + message: status.description + ) + end + ) + end + + def as_otlp_timestamp(timestamp) + (timestamp.to_r * 1_000_000_000).to_i + end + + def as_otlp_span_kind(kind) + case kind + when :internal then Opentelemetry::Proto::Trace::V1::Span::SpanKind::INTERNAL + when :server then Opentelemetry::Proto::Trace::V1::Span::SpanKind::SERVER + when :client then Opentelemetry::Proto::Trace::V1::Span::SpanKind::CLIENT + when :producer then Opentelemetry::Proto::Trace::V1::Span::SpanKind::PRODUCER + when :consumer then Opentelemetry::Proto::Trace::V1::Span::SpanKind::CONSUMER + else Opentelemetry::Proto::Trace::V1::Span::SpanKind::SPAN_KIND_UNSPECIFIED + end + end + + def as_otlp_key_value(key, value) + Opentelemetry::Proto::Common::V1::KeyValue.new(key: key, value: as_otlp_any_value(value)) + end + + def as_otlp_any_value(value) + result = Opentelemetry::Proto::Common::V1::AnyValue.new + case value + when String + result.string_value = value + when Integer + result.int_value = value + when Float + result.double_value = value + when true, false + result.bool_value = value + when Array + values = value.map { |element| as_otlp_any_value(element) } + result.array_value = Opentelemetry::Proto::Common::V1::ArrayValue.new(values: values) + end + result + end + end + end + end +end diff --git a/exporter/otlp/lib/opentelemetry/exporter/otlp/version.rb b/exporter/otlp/lib/opentelemetry/exporter/otlp/version.rb new file mode 100644 index 0000000000..286f6e2c40 --- /dev/null +++ b/exporter/otlp/lib/opentelemetry/exporter/otlp/version.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +# Copyright 2019 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Exporter + module OTLP + ## Current OpenTelemetry OTLP exporter version + VERSION = '0.5.0' + end + end +end diff --git a/exporter/otlp/lib/opentelemetry/proto/collector/trace/v1/trace_service_pb.rb b/exporter/otlp/lib/opentelemetry/proto/collector/trace/v1/trace_service_pb.rb new file mode 100644 index 0000000000..8cfd05470b --- /dev/null +++ b/exporter/otlp/lib/opentelemetry/proto/collector/trace/v1/trace_service_pb.rb @@ -0,0 +1,28 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: opentelemetry/proto/collector/trace/v1/trace_service.proto + +require 'google/protobuf' + +require 'opentelemetry/proto/trace/v1/trace_pb' +Google::Protobuf::DescriptorPool.generated_pool.build do + add_file("opentelemetry/proto/collector/trace/v1/trace_service.proto", :syntax => :proto3) do + add_message "opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest" do + repeated :resource_spans, :message, 1, "opentelemetry.proto.trace.v1.ResourceSpans" + end + add_message "opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse" do + end + end +end + +module Opentelemetry + module Proto + module Collector + module Trace + module V1 + ExportTraceServiceRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest").msgclass + ExportTraceServiceResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse").msgclass + end + end + end + end +end diff --git a/exporter/otlp/lib/opentelemetry/proto/common/v1/common_pb.rb b/exporter/otlp/lib/opentelemetry/proto/common/v1/common_pb.rb new file mode 100644 index 0000000000..3a132e3c4d --- /dev/null +++ b/exporter/otlp/lib/opentelemetry/proto/common/v1/common_pb.rb @@ -0,0 +1,52 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: opentelemetry/proto/common/v1/common.proto + +require 'google/protobuf' + +Google::Protobuf::DescriptorPool.generated_pool.build do + add_file("opentelemetry/proto/common/v1/common.proto", :syntax => :proto3) do + add_message "opentelemetry.proto.common.v1.AnyValue" do + oneof :value do + optional :string_value, :string, 1 + optional :bool_value, :bool, 2 + optional :int_value, :int64, 3 + optional :double_value, :double, 4 + optional :array_value, :message, 5, "opentelemetry.proto.common.v1.ArrayValue" + optional :kvlist_value, :message, 6, "opentelemetry.proto.common.v1.KeyValueList" + end + end + add_message "opentelemetry.proto.common.v1.ArrayValue" do + repeated :values, :message, 1, "opentelemetry.proto.common.v1.AnyValue" + end + add_message "opentelemetry.proto.common.v1.KeyValueList" do + repeated :values, :message, 1, "opentelemetry.proto.common.v1.KeyValue" + end + add_message "opentelemetry.proto.common.v1.KeyValue" do + optional :key, :string, 1 + optional :value, :message, 2, "opentelemetry.proto.common.v1.AnyValue" + end + add_message "opentelemetry.proto.common.v1.StringKeyValue" do + optional :key, :string, 1 + optional :value, :string, 2 + end + add_message "opentelemetry.proto.common.v1.InstrumentationLibrary" do + optional :name, :string, 1 + optional :version, :string, 2 + end + end +end + +module Opentelemetry + module Proto + module Common + module V1 + AnyValue = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.common.v1.AnyValue").msgclass + ArrayValue = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.common.v1.ArrayValue").msgclass + KeyValueList = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.common.v1.KeyValueList").msgclass + KeyValue = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.common.v1.KeyValue").msgclass + StringKeyValue = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.common.v1.StringKeyValue").msgclass + InstrumentationLibrary = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.common.v1.InstrumentationLibrary").msgclass + end + end + end +end diff --git a/exporter/otlp/lib/opentelemetry/proto/resource/v1/resource_pb.rb b/exporter/otlp/lib/opentelemetry/proto/resource/v1/resource_pb.rb new file mode 100644 index 0000000000..7c0d53e6ee --- /dev/null +++ b/exporter/otlp/lib/opentelemetry/proto/resource/v1/resource_pb.rb @@ -0,0 +1,24 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: opentelemetry/proto/resource/v1/resource.proto + +require 'google/protobuf' + +require 'opentelemetry/proto/common/v1/common_pb' +Google::Protobuf::DescriptorPool.generated_pool.build do + add_file("opentelemetry/proto/resource/v1/resource.proto", :syntax => :proto3) do + add_message "opentelemetry.proto.resource.v1.Resource" do + repeated :attributes, :message, 1, "opentelemetry.proto.common.v1.KeyValue" + optional :dropped_attributes_count, :uint32, 2 + end + end +end + +module Opentelemetry + module Proto + module Resource + module V1 + Resource = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.resource.v1.Resource").msgclass + end + end + end +end diff --git a/exporter/otlp/lib/opentelemetry/proto/trace/v1/trace_pb.rb b/exporter/otlp/lib/opentelemetry/proto/trace/v1/trace_pb.rb new file mode 100644 index 0000000000..e9e6c93044 --- /dev/null +++ b/exporter/otlp/lib/opentelemetry/proto/trace/v1/trace_pb.rb @@ -0,0 +1,97 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: opentelemetry/proto/trace/v1/trace.proto + +require 'google/protobuf' + +require 'opentelemetry/proto/common/v1/common_pb' +require 'opentelemetry/proto/resource/v1/resource_pb' +Google::Protobuf::DescriptorPool.generated_pool.build do + add_file("opentelemetry/proto/trace/v1/trace.proto", :syntax => :proto3) do + add_message "opentelemetry.proto.trace.v1.ResourceSpans" do + optional :resource, :message, 1, "opentelemetry.proto.resource.v1.Resource" + repeated :instrumentation_library_spans, :message, 2, "opentelemetry.proto.trace.v1.InstrumentationLibrarySpans" + end + add_message "opentelemetry.proto.trace.v1.InstrumentationLibrarySpans" do + optional :instrumentation_library, :message, 1, "opentelemetry.proto.common.v1.InstrumentationLibrary" + repeated :spans, :message, 2, "opentelemetry.proto.trace.v1.Span" + end + add_message "opentelemetry.proto.trace.v1.Span" do + optional :trace_id, :bytes, 1 + optional :span_id, :bytes, 2 + optional :trace_state, :string, 3 + optional :parent_span_id, :bytes, 4 + optional :name, :string, 5 + optional :kind, :enum, 6, "opentelemetry.proto.trace.v1.Span.SpanKind" + optional :start_time_unix_nano, :fixed64, 7 + optional :end_time_unix_nano, :fixed64, 8 + repeated :attributes, :message, 9, "opentelemetry.proto.common.v1.KeyValue" + optional :dropped_attributes_count, :uint32, 10 + repeated :events, :message, 11, "opentelemetry.proto.trace.v1.Span.Event" + optional :dropped_events_count, :uint32, 12 + repeated :links, :message, 13, "opentelemetry.proto.trace.v1.Span.Link" + optional :dropped_links_count, :uint32, 14 + optional :status, :message, 15, "opentelemetry.proto.trace.v1.Status" + end + add_message "opentelemetry.proto.trace.v1.Span.Event" do + optional :time_unix_nano, :fixed64, 1 + optional :name, :string, 2 + repeated :attributes, :message, 3, "opentelemetry.proto.common.v1.KeyValue" + optional :dropped_attributes_count, :uint32, 4 + end + add_message "opentelemetry.proto.trace.v1.Span.Link" do + optional :trace_id, :bytes, 1 + optional :span_id, :bytes, 2 + optional :trace_state, :string, 3 + repeated :attributes, :message, 4, "opentelemetry.proto.common.v1.KeyValue" + optional :dropped_attributes_count, :uint32, 5 + end + add_enum "opentelemetry.proto.trace.v1.Span.SpanKind" do + value :SPAN_KIND_UNSPECIFIED, 0 + value :INTERNAL, 1 + value :SERVER, 2 + value :CLIENT, 3 + value :PRODUCER, 4 + value :CONSUMER, 5 + end + add_message "opentelemetry.proto.trace.v1.Status" do + optional :code, :enum, 1, "opentelemetry.proto.trace.v1.Status.StatusCode" + optional :message, :string, 2 + end + add_enum "opentelemetry.proto.trace.v1.Status.StatusCode" do + value :Ok, 0 + value :Cancelled, 1 + value :UnknownError, 2 + value :InvalidArgument, 3 + value :DeadlineExceeded, 4 + value :NotFound, 5 + value :AlreadyExists, 6 + value :PermissionDenied, 7 + value :ResourceExhausted, 8 + value :FailedPrecondition, 9 + value :Aborted, 10 + value :OutOfRange, 11 + value :Unimplemented, 12 + value :InternalError, 13 + value :Unavailable, 14 + value :DataLoss, 15 + value :Unauthenticated, 16 + end + end +end + +module Opentelemetry + module Proto + module Trace + module V1 + ResourceSpans = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.trace.v1.ResourceSpans").msgclass + InstrumentationLibrarySpans = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.trace.v1.InstrumentationLibrarySpans").msgclass + Span = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.trace.v1.Span").msgclass + Span::Event = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.trace.v1.Span.Event").msgclass + Span::Link = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.trace.v1.Span.Link").msgclass + Span::SpanKind = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.trace.v1.Span.SpanKind").enummodule + Status = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.trace.v1.Status").msgclass + Status::StatusCode = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.trace.v1.Status.StatusCode").enummodule + end + end + end +end diff --git a/exporter/otlp/opentelemetry-exporter-otlp.gemspec b/exporter/otlp/opentelemetry-exporter-otlp.gemspec new file mode 100644 index 0000000000..b0a26fa98c --- /dev/null +++ b/exporter/otlp/opentelemetry-exporter-otlp.gemspec @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +lib = File.expand_path('lib', __dir__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require 'opentelemetry/exporter/otlp/version' + +Gem::Specification.new do |spec| + spec.name = 'opentelemetry-exporter-otlp' + spec.version = OpenTelemetry::Exporter::OTLP::VERSION + spec.authors = ['OpenTelemetry Authors'] + spec.email = ['cncf-opentelemetry-contributors@lists.cncf.io'] + + spec.summary = 'OTLP exporter for the OpenTelemetry framework' + spec.description = 'OTLP exporter for the OpenTelemetry framework' + spec.homepage = 'https://github.com/open-telemetry/opentelemetry-ruby' + spec.license = 'Apache-2.0' + + spec.files = ::Dir.glob('lib/**/*.rb') + + ::Dir.glob('*.md') + + ['LICENSE', '.yardopts'] + spec.require_paths = ['lib'] + spec.required_ruby_version = '>= 2.5.0' + + spec.add_dependency 'google-protobuf', '>= 3.4.1.1', '< 4' + spec.add_dependency 'opentelemetry-api', '~> 0.5.0' + + spec.add_development_dependency 'bundler', '>= 1.17' + spec.add_development_dependency 'faraday', '~> 0.13' + spec.add_development_dependency 'minitest', '~> 5.0' + spec.add_development_dependency 'rake', '~> 12.0' + spec.add_development_dependency 'rubocop', '~> 0.73.0' + spec.add_development_dependency 'simplecov', '~> 0.17' + spec.add_development_dependency 'webmock', '~> 3.7.6' + spec.add_development_dependency 'yard', '~> 0.9' + spec.add_development_dependency 'yard-doctest', '~> 0.1.6' +end diff --git a/exporter/otlp/test/.rubocop.yml b/exporter/otlp/test/.rubocop.yml new file mode 100644 index 0000000000..4c8c0d91ed --- /dev/null +++ b/exporter/otlp/test/.rubocop.yml @@ -0,0 +1,6 @@ +inherit_from: ../.rubocop.yml + +Metrics/BlockLength: + Enabled: false +Metrics/LineLength: + Enabled: false diff --git a/exporter/otlp/test/opentelemetry/exporter/otlp/exporter_test.rb b/exporter/otlp/test/opentelemetry/exporter/otlp/exporter_test.rb new file mode 100644 index 0000000000..38c4124370 --- /dev/null +++ b/exporter/otlp/test/opentelemetry/exporter/otlp/exporter_test.rb @@ -0,0 +1,312 @@ +# frozen_string_literal: true + +# Copyright 2019 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 +require 'test_helper' + +describe OpenTelemetry::Exporter::OTLP::Exporter do + SUCCESS = OpenTelemetry::SDK::Trace::Export::SUCCESS + FAILURE = OpenTelemetry::SDK::Trace::Export::FAILURE + + describe '#initialize' do + it 'initializes with defaults' do + exp = OpenTelemetry::Exporter::OTLP::Exporter.new + _(exp).wont_be_nil + _(exp.instance_variable_get(:@headers)).must_be_nil + _(exp.instance_variable_get(:@timeout)).must_equal 10.0 + _(exp.instance_variable_get(:@path)).must_equal '/v1/trace' + http = exp.instance_variable_get(:@http) + _(http.ca_file).must_be_nil + _(http.use_ssl?).must_equal true + _(http.address).must_equal 'localhost' + _(http.port).must_equal 55_681 + end + + it 'refuses invalid headers' do + assert_raises ArgumentError do + OpenTelemetry::Exporter::OTLP::Exporter.new(headers: 'a:b,c') + end + end + + it 'refuses invalid endpoint' do + assert_raises ArgumentError do + OpenTelemetry::Exporter::OTLP::Exporter.new(endpoint: 'not a url') + end + end + + it 'refuses compression' do + assert_raises ArgumentError do + OpenTelemetry::Exporter::OTLP::Exporter.new(compression: 'gzip') + end + end + + it 'sets parameters from the environment' do + exp = with_env('OTEL_EXPORTER_OTLP_ENDPOINT' => 'localhost:1234/v2/trace', + 'OTEL_EXPORTER_OTLP_INSECURE' => 'true', + 'OTEL_EXPORTER_OTLP_CERTIFICATE' => '/foo/bar', + 'OTEL_EXPORTER_OTLP_HEADERS' => 'a:b,c:d', + 'OTEL_EXPORTER_OTLP_TIMEOUT' => '11') do + OpenTelemetry::Exporter::OTLP::Exporter.new + end + _(exp.instance_variable_get(:@headers)).must_equal('a' => 'b', 'c' => 'd') + _(exp.instance_variable_get(:@timeout)).must_equal 11.0 + _(exp.instance_variable_get(:@path)).must_equal '/v2/trace' + http = exp.instance_variable_get(:@http) + _(http.ca_file).must_equal '/foo/bar' + _(http.use_ssl?).must_equal false + _(http.address).must_equal 'localhost' + _(http.port).must_equal 1234 + end + + it 'prefers explicit parameters rather than the environment' do + exp = with_env('OTEL_EXPORTER_OTLP_ENDPOINT' => 'localhost:1234/v2/trace', + 'OTEL_EXPORTER_OTLP_INSECURE' => 'true', + 'OTEL_EXPORTER_OTLP_CERTIFICATE' => '/foo/bar', + 'OTEL_EXPORTER_OTLP_HEADERS' => 'a:b,c:d', + 'OTEL_EXPORTER_OTLP_TIMEOUT' => '11') do + OpenTelemetry::Exporter::OTLP::Exporter.new(endpoint: 'localhost:4321/v3/trace', + insecure: 'false', + certificate_file: '/baz', + headers: { 'x' => 'y' }, + timeout: 12) + end + _(exp.instance_variable_get(:@headers)).must_equal('x' => 'y') + _(exp.instance_variable_get(:@timeout)).must_equal 12.0 + _(exp.instance_variable_get(:@path)).must_equal '/v3/trace' + http = exp.instance_variable_get(:@http) + _(http.ca_file).must_equal '/baz' + _(http.use_ssl?).must_equal true + _(http.address).must_equal 'localhost' + _(http.port).must_equal 4321 + end + end + + describe '#export' do + let(:exporter) { OpenTelemetry::Exporter::OTLP::Exporter.new } + + before do + OpenTelemetry.tracer_provider = OpenTelemetry::SDK::Trace::TracerProvider.new(OpenTelemetry::SDK::Resources::Resource.telemetry_sdk) + end + + it 'integrates with collector' do + skip unless ENV['TRACING_INTEGRATION_TEST'] + WebMock.disable_net_connect!(allow: 'localhost') + span_data = create_span_data + exporter = OpenTelemetry::Exporter::OTLP::Exporter.new(insecure: true) + result = exporter.export([span_data]) + _(result).must_equal(SUCCESS) + end + + it 'returns FAILURE when shutdown' do + exporter.shutdown + result = exporter.export(nil) + _(result).must_equal(FAILURE) + end + + it 'exports a span_data' do + stub_request(:post, 'https://localhost:55681/v1/trace').to_return(status: 200) + span_data = create_span_data + result = exporter.export([span_data]) + _(result).must_equal(SUCCESS) + end + + it 'exports a span from a tracer' do + stub_post = stub_request(:post, 'https://localhost:55681/v1/trace').to_return(status: 200) + processor = OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(exporter: exporter, max_queue_size: 1, max_export_batch_size: 1) + OpenTelemetry.tracer_provider.add_span_processor(processor) + OpenTelemetry.tracer_provider.tracer.start_root_span('foo').finish + OpenTelemetry.tracer_provider.shutdown + assert_requested(stub_post) + end + + it 'batches per resource' do + etsr = nil + stub_post = stub_request(:post, 'https://localhost:55681/v1/trace').to_return do |request| + etsr = Opentelemetry::Proto::Collector::Trace::V1::ExportTraceServiceRequest.decode(request.body) + { status: 200 } + end + + span_data1 = create_span_data(resource: OpenTelemetry::SDK::Resources::Resource.create('k1' => 'v1')) + span_data2 = create_span_data(resource: OpenTelemetry::SDK::Resources::Resource.create('k2' => 'v2')) + + result = exporter.export([span_data1, span_data2]) + + _(result).must_equal(SUCCESS) + assert_requested(stub_post) + _(etsr.resource_spans.length).must_equal(2) + end + + it 'translates all the things' do + stub_request(:post, 'https://localhost:55681/v1/trace').to_return(status: 200) + processor = OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(exporter: exporter) + tracer = OpenTelemetry.tracer_provider.tracer('tracer', 'v0.0.1') + other_tracer = OpenTelemetry.tracer_provider.tracer('other_tracer') + + trace_id = OpenTelemetry::Trace.generate_trace_id + root_span_id = OpenTelemetry::Trace.generate_span_id + child_span_id = OpenTelemetry::Trace.generate_span_id + client_span_id = OpenTelemetry::Trace.generate_span_id + server_span_id = OpenTelemetry::Trace.generate_span_id + consumer_span_id = OpenTelemetry::Trace.generate_span_id + start_timestamp = Time.now + end_timestamp = start_timestamp + 6 + + OpenTelemetry.tracer_provider.add_span_processor(processor) + root = with_ids(trace_id, root_span_id) { tracer.start_root_span('root', kind: :internal, start_timestamp: start_timestamp).finish(end_timestamp: end_timestamp) } + span = with_ids(trace_id, child_span_id) { tracer.start_span('child', with_parent: root, kind: :producer, start_timestamp: start_timestamp + 1, links: [OpenTelemetry::Trace::Link.new(root.context, 'attr' => 4)]) } + span['b'] = true + span['f'] = 1.1 + span['i'] = 2 + span['s'] = 'val' + span['a'] = [3, 4] + span.status = OpenTelemetry::Trace::Status.new(OpenTelemetry::Trace::Status::UNKNOWN_ERROR) + client = with_ids(trace_id, client_span_id) { tracer.start_span('client', with_parent: span, kind: :client, start_timestamp: start_timestamp + 2).finish(end_timestamp: end_timestamp) } + with_ids(trace_id, server_span_id) { other_tracer.start_span('server', with_parent: client, kind: :server, start_timestamp: start_timestamp + 3).finish(end_timestamp: end_timestamp) } + span.add_event('event', attributes: { 'attr' => 42 }, timestamp: start_timestamp + 4) + with_ids(trace_id, consumer_span_id) { tracer.start_span('consumer', with_parent: span, kind: :consumer, start_timestamp: start_timestamp + 5).finish(end_timestamp: end_timestamp) } + span.finish(end_timestamp: end_timestamp) + OpenTelemetry.tracer_provider.shutdown + + encoded_etsr = Opentelemetry::Proto::Collector::Trace::V1::ExportTraceServiceRequest.encode( + Opentelemetry::Proto::Collector::Trace::V1::ExportTraceServiceRequest.new( + resource_spans: [ + Opentelemetry::Proto::Trace::V1::ResourceSpans.new( + resource: Opentelemetry::Proto::Resource::V1::Resource.new( + attributes: [ + Opentelemetry::Proto::Common::V1::KeyValue.new(key: 'telemetry.sdk.name', value: Opentelemetry::Proto::Common::V1::AnyValue.new(string_value: 'opentelemetry')), + Opentelemetry::Proto::Common::V1::KeyValue.new(key: 'telemetry.sdk.language', value: Opentelemetry::Proto::Common::V1::AnyValue.new(string_value: 'ruby')), + Opentelemetry::Proto::Common::V1::KeyValue.new(key: 'telemetry.sdk.version', value: Opentelemetry::Proto::Common::V1::AnyValue.new(string_value: OpenTelemetry::SDK::VERSION)) + ] + ), + instrumentation_library_spans: [ + Opentelemetry::Proto::Trace::V1::InstrumentationLibrarySpans.new( + instrumentation_library: Opentelemetry::Proto::Common::V1::InstrumentationLibrary.new( + name: 'tracer', + version: 'v0.0.1' + ), + spans: [ + Opentelemetry::Proto::Trace::V1::Span.new( + trace_id: trace_id, + span_id: root_span_id, + parent_span_id: OpenTelemetry::Trace::INVALID_SPAN_ID, + name: 'root', + kind: Opentelemetry::Proto::Trace::V1::Span::SpanKind::INTERNAL, + start_time_unix_nano: (start_timestamp.to_r * 1_000_000_000).to_i, + end_time_unix_nano: (end_timestamp.to_r * 1_000_000_000).to_i + ), + Opentelemetry::Proto::Trace::V1::Span.new( + trace_id: trace_id, + span_id: client_span_id, + parent_span_id: child_span_id, + name: 'client', + kind: Opentelemetry::Proto::Trace::V1::Span::SpanKind::CLIENT, + start_time_unix_nano: ((start_timestamp + 2).to_r * 1_000_000_000).to_i, + end_time_unix_nano: (end_timestamp.to_r * 1_000_000_000).to_i + ), + Opentelemetry::Proto::Trace::V1::Span.new( + trace_id: trace_id, + span_id: consumer_span_id, + parent_span_id: child_span_id, + name: 'consumer', + kind: Opentelemetry::Proto::Trace::V1::Span::SpanKind::CONSUMER, + start_time_unix_nano: ((start_timestamp + 5).to_r * 1_000_000_000).to_i, + end_time_unix_nano: (end_timestamp.to_r * 1_000_000_000).to_i + ), + Opentelemetry::Proto::Trace::V1::Span.new( + trace_id: trace_id, + span_id: child_span_id, + parent_span_id: root_span_id, + name: 'child', + kind: Opentelemetry::Proto::Trace::V1::Span::SpanKind::PRODUCER, + start_time_unix_nano: ((start_timestamp + 1).to_r * 1_000_000_000).to_i, + end_time_unix_nano: (end_timestamp.to_r * 1_000_000_000).to_i, + attributes: [ + Opentelemetry::Proto::Common::V1::KeyValue.new(key: 'b', value: Opentelemetry::Proto::Common::V1::AnyValue.new(bool_value: true)), + Opentelemetry::Proto::Common::V1::KeyValue.new(key: 'f', value: Opentelemetry::Proto::Common::V1::AnyValue.new(double_value: 1.1)), + Opentelemetry::Proto::Common::V1::KeyValue.new(key: 'i', value: Opentelemetry::Proto::Common::V1::AnyValue.new(int_value: 2)), + Opentelemetry::Proto::Common::V1::KeyValue.new(key: 's', value: Opentelemetry::Proto::Common::V1::AnyValue.new(string_value: 'val')), + Opentelemetry::Proto::Common::V1::KeyValue.new( + key: 'a', + value: Opentelemetry::Proto::Common::V1::AnyValue.new( + array_value: Opentelemetry::Proto::Common::V1::ArrayValue.new( + values: [ + Opentelemetry::Proto::Common::V1::AnyValue.new(int_value: 3), + Opentelemetry::Proto::Common::V1::AnyValue.new(int_value: 4) + ] + ) + ) + ) + ], + events: [ + Opentelemetry::Proto::Trace::V1::Span::Event.new( + time_unix_nano: ((start_timestamp + 4).to_r * 1_000_000_000).to_i, + name: 'event', + attributes: [ + Opentelemetry::Proto::Common::V1::KeyValue.new(key: 'attr', value: Opentelemetry::Proto::Common::V1::AnyValue.new(int_value: 42)) + ] + ) + ], + links: [ + Opentelemetry::Proto::Trace::V1::Span::Link.new( + trace_id: trace_id, + span_id: root_span_id, + attributes: [ + Opentelemetry::Proto::Common::V1::KeyValue.new(key: 'attr', value: Opentelemetry::Proto::Common::V1::AnyValue.new(int_value: 4)) + ] + ) + ], + status: Opentelemetry::Proto::Trace::V1::Status.new( + code: Opentelemetry::Proto::Trace::V1::Status::StatusCode::UnknownError + ) + ) + ] + ), + Opentelemetry::Proto::Trace::V1::InstrumentationLibrarySpans.new( + instrumentation_library: Opentelemetry::Proto::Common::V1::InstrumentationLibrary.new( + name: 'other_tracer' + ), + spans: [ + Opentelemetry::Proto::Trace::V1::Span.new( + trace_id: trace_id, + span_id: server_span_id, + parent_span_id: client_span_id, + name: 'server', + kind: Opentelemetry::Proto::Trace::V1::Span::SpanKind::SERVER, + start_time_unix_nano: ((start_timestamp + 3).to_r * 1_000_000_000).to_i, + end_time_unix_nano: (end_timestamp.to_r * 1_000_000_000).to_i + ) + ] + ) + ] + ) + ] + ) + ) + + assert_requested(:post, 'https://localhost:55681/v1/trace') do |req| + req.body == encoded_etsr + end + end + end + + def with_ids(trace_id, span_id) + OpenTelemetry::Trace.stub(:generate_trace_id, trace_id) do + OpenTelemetry::Trace.stub(:generate_span_id, span_id) do + yield + end + end + end + + def create_span_data(name: '', kind: nil, status: nil, parent_span_id: OpenTelemetry::Trace::INVALID_SPAN_ID, child_count: 0, + total_recorded_attributes: 0, total_recorded_events: 0, total_recorded_links: 0, start_timestamp: Time.now, + end_timestamp: Time.now, attributes: nil, links: nil, events: nil, resource: nil, + instrumentation_library: OpenTelemetry::SDK::InstrumentationLibrary.new('', 'v0.0.1'), + span_id: OpenTelemetry::Trace.generate_span_id, trace_id: OpenTelemetry::Trace.generate_trace_id, + trace_flags: OpenTelemetry::Trace::TraceFlags::DEFAULT, tracestate: nil) + resource ||= OpenTelemetry::SDK::Resources::Resource.telemetry_sdk + OpenTelemetry::SDK::Trace::SpanData.new(name, kind, status, parent_span_id, child_count, total_recorded_attributes, + total_recorded_events, total_recorded_links, start_timestamp, end_timestamp, + attributes, links, events, resource, instrumentation_library, span_id, trace_id, trace_flags, tracestate) + end +end diff --git a/exporter/otlp/test/test_helper.rb b/exporter/otlp/test/test_helper.rb new file mode 100644 index 0000000000..bab57ae734 --- /dev/null +++ b/exporter/otlp/test/test_helper.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +# Copyright 2019 OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'simplecov' +SimpleCov.start + +require 'opentelemetry/exporter/otlp' +require 'minitest/autorun' +require 'webmock/minitest' + +OpenTelemetry.logger = Logger.new('/dev/null') + +def with_env(new_env) + env_to_reset = ENV.select { |k, _| new_env.key?(k) } + keys_to_delete = new_env.keys - ENV.keys + new_env.each_pair { |k, v| ENV[k] = v } + yield +ensure + env_to_reset.each_pair { |k, v| ENV[k] = v } + keys_to_delete.each { |k| ENV.delete(k) } +end diff --git a/sdk/lib/opentelemetry/sdk/trace/span.rb b/sdk/lib/opentelemetry/sdk/trace/span.rb index 7ceba60766..c5afd0040d 100644 --- a/sdk/lib/opentelemetry/sdk/trace/span.rb +++ b/sdk/lib/opentelemetry/sdk/trace/span.rb @@ -21,7 +21,7 @@ class Span < OpenTelemetry::Trace::Span attr_reader :name, :status, :kind, :parent_span_id, :start_timestamp, :end_timestamp, :links, :resource, :instrumentation_library # Return a frozen copy of the current attributes. This is intended for - # use of SpanProcesses and should not be considered part of the public + # use of SpanProcessors and should not be considered part of the public # interface for instrumentation. # # @return [Hash{String => String, Numeric, Boolean, Array}] may be nil. @@ -76,6 +76,7 @@ def set_attribute(key, value) end self end + alias []= set_attribute # Add an Event to a {Span}. #