diff --git a/CHANGELOG.md b/CHANGELOG.md
index df8598e..180d77c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
-## [Unreleased]
+## [Unreleased] TBD
+
+### Added
+- Introduce new `:open_telemetry` target
+
+### Changed
+- Removed dependency for `dogstatsd-ruby` gem. Added `dogstatsd-ruby` and `opentelemetry-*` gems to development/test dependencies for integration tests.
+ - Consumers are expected to explicitly define these dependencies in their projects.
## [1.1.4]
diff --git a/Gemfile b/Gemfile
index 0f12708..0808e90 100644
--- a/Gemfile
+++ b/Gemfile
@@ -6,7 +6,10 @@ source 'https://rubygems.org'
gemspec
gem 'dogstatsd-ruby'
-
+gem 'opentelemetry-exporter-otlp'
+gem 'opentelemetry-exporter-otlp-metrics', '~> 0.3'
+gem 'opentelemetry-metrics-sdk', '~> 0.5'
+gem 'opentelemetry-sdk'
gem 'rack'
gem 'rake', '~> 13.0'
gem 'rspec', '~> 3.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index e0256f4..5274d7c 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -8,10 +8,56 @@ GEM
remote: https://rubygems.org/
specs:
ast (2.4.2)
+ bigdecimal (3.1.8)
diff-lcs (1.5.1)
dogstatsd-ruby (5.6.1)
+ google-protobuf (4.29.1)
+ bigdecimal
+ rake (>= 13)
+ google-protobuf (4.29.1-arm64-darwin)
+ bigdecimal
+ rake (>= 13)
+ google-protobuf (4.29.1-x86_64-linux)
+ bigdecimal
+ rake (>= 13)
+ googleapis-common-protos-types (1.16.0)
+ google-protobuf (>= 3.18, < 5.a)
json (2.7.2)
nio4r (2.7.1)
+ opentelemetry-api (1.4.0)
+ opentelemetry-common (0.21.0)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-exporter-otlp (0.29.1)
+ google-protobuf (>= 3.18)
+ googleapis-common-protos-types (~> 1.3)
+ opentelemetry-api (~> 1.1)
+ opentelemetry-common (~> 0.20)
+ opentelemetry-sdk (~> 1.2)
+ opentelemetry-semantic_conventions
+ opentelemetry-exporter-otlp-metrics (0.3.0)
+ google-protobuf (>= 3.18, < 5.0)
+ googleapis-common-protos-types (~> 1.3)
+ opentelemetry-api (~> 1.1)
+ opentelemetry-common (~> 0.20)
+ opentelemetry-metrics-api (~> 0.2)
+ opentelemetry-metrics-sdk (~> 0.5)
+ opentelemetry-sdk (~> 1.2)
+ opentelemetry-semantic_conventions
+ opentelemetry-metrics-api (0.2.0)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-metrics-sdk (0.5.0)
+ opentelemetry-api (~> 1.1)
+ opentelemetry-metrics-api (~> 0.2)
+ opentelemetry-sdk (~> 1.2)
+ opentelemetry-registry (0.3.1)
+ opentelemetry-api (~> 1.1)
+ opentelemetry-sdk (1.6.0)
+ opentelemetry-api (~> 1.1)
+ opentelemetry-common (~> 0.20)
+ opentelemetry-registry (~> 0.2)
+ opentelemetry-semantic_conventions
+ opentelemetry-semantic_conventions (1.10.1)
+ opentelemetry-api (~> 1.0)
parallel (1.24.0)
parser (3.3.1.0)
ast (~> 2.4.1)
@@ -64,6 +110,10 @@ PLATFORMS
DEPENDENCIES
dogstatsd-ruby
+ opentelemetry-exporter-otlp
+ opentelemetry-exporter-otlp-metrics (~> 0.3)
+ opentelemetry-metrics-sdk (~> 0.5)
+ opentelemetry-sdk
puma-plugin-telemetry!
rack
rake (~> 13.0)
diff --git a/README.md b/README.md
index d0b6f66..8ddbfdc 100644
--- a/README.md
+++ b/README.md
@@ -53,14 +53,27 @@ Output telemetry as JSON to `STDOUT`
Given gem provides built in target for Datadog StatsD client, that uses batch operation to publish metrics.
-**NOTE** Be sure to have `dogstatsd` gem installed.
-
```ruby
config.add_target :dogstatsd, client: Datadog::Statsd.new
```
You can provide all the tags, namespaces, and other configuration options as always to `Datadog::Statsd.new` method.
+### OpenTelemetry target
+
+Given gem provides built in target for OpenTelemetry Metrics SDK, that uses batch operations to publish metrics.
+
+```ruby
+ config.add_target :open_telemetry, meter_provider: OpenTelemetry.meter_provider
+```
+
+This target supports the following options:
+| Option | Description | Default | Required | |
+|------------|-----------------------------------------------------------------|---------|----------|---|
+| prefix | Metric name prefix.
ex) prefix: 'puma' => 'puma.workers.booted' | nil | No | |
+| suffix | Metric name suffix.
ex) suffix: 'v1' => 'workers.booted.v1' | nil | No | |
+| attributes | Attributes to be included with the metric | {} | No | |
+
### All available options
For detailed documentation checkout [`Puma::Plugin::Telemetry::Config`](./lib/puma/plugin/telemetry/config.rb) class.
diff --git a/lib/puma/plugin/telemetry.rb b/lib/puma/plugin/telemetry.rb
index 235ecb2..4e0d764 100644
--- a/lib/puma/plugin/telemetry.rb
+++ b/lib/puma/plugin/telemetry.rb
@@ -7,6 +7,7 @@
require 'puma/plugin/telemetry/data'
require 'puma/plugin/telemetry/targets/datadog_statsd_target'
require 'puma/plugin/telemetry/targets/io_target'
+require 'puma/plugin/telemetry/targets/open_telemetry_target'
require 'puma/plugin/telemetry/config'
module Puma
diff --git a/lib/puma/plugin/telemetry/config.rb b/lib/puma/plugin/telemetry/config.rb
index 1f26718..5efcbdf 100644
--- a/lib/puma/plugin/telemetry/config.rb
+++ b/lib/puma/plugin/telemetry/config.rb
@@ -32,7 +32,8 @@ class Config
TARGETS = {
dogstatsd: Telemetry::Targets::DatadogStatsdTarget,
- io: Telemetry::Targets::IOTarget
+ io: Telemetry::Targets::IOTarget,
+ open_telemetry: Telemetry::Targets::OpenTelemetryTarget
}.freeze
# Whenever telemetry should run with puma
diff --git a/lib/puma/plugin/telemetry/targets/open_telemetry_target.rb b/lib/puma/plugin/telemetry/targets/open_telemetry_target.rb
new file mode 100644
index 0000000..cdea1ae
--- /dev/null
+++ b/lib/puma/plugin/telemetry/targets/open_telemetry_target.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module Puma
+ class Plugin
+ module Telemetry
+ module Targets
+ # Target wrapping OpenTelemetry Metrics client.
+ #
+ # ## Example
+ #
+ # require 'opentelemetry-metrics-sdk'
+ #
+ # OpenTelemetryTarget.new(meter_provider: OpenTelemetry.meter_provider, prefix: 'puma')
+ #
+ class OpenTelemetryTarget
+ def initialize(meter_provider:, prefix: nil, suffix: nil, force_flush: false, attributes: {})
+ @meter_provider = meter_provider
+ @meter = meter_provider.meter('puma.telemetry')
+ @prefix = prefix
+ @suffix = suffix
+ @force_flush = force_flush
+ @attributes = attributes
+ @instruments = {}
+ end
+
+ # We are using `gauge` metric type, which means that only the last value will get exported
+ # since the OpenTelemetry exporter aggregates metrics before sending them.
+ #
+ # This means that we could publish metrics from here several times
+ # before they get flushed from the aggregation thread, and when they
+ # do, only the last values will get sent.
+ #
+ # That's why we provide the option to explicitly call force_flush here, in order to persist
+ # all metrics, and not only the most recent ones.
+ #
+ # Note: Force flushing metrics every time can significantly impact performance
+ #
+ def call(telemetry)
+ telemetry.each do |metric, value|
+ instrument(metric).record(value, attributes: @attributes)
+ end
+
+ @meter_provider.force_flush if @force_flush
+ end
+
+ def instrument(metric)
+ @instruments[metric] ||= @meter.create_gauge([@prefix, metric, @suffix].compact.join('.'))
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/fixtures/open_telemetry.rb b/spec/fixtures/open_telemetry.rb
new file mode 100644
index 0000000..f276244
--- /dev/null
+++ b/spec/fixtures/open_telemetry.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'opentelemetry/sdk'
+require 'opentelemetry-metrics-sdk'
+
+# Custom console exporter to support flushing metrics. Console _pull_ exporter doesn't have a force_flush method like the typical exporters
+class ConsoleMetricExporter < OpenTelemetry::SDK::Metrics::Export::ConsoleMetricPullExporter
+ def force_flush(timeout: nil)
+ pull
+ end
+end
+
+OpenTelemetry::SDK.configure
+
+console_metric_exporter = ConsoleMetricExporter.new
+OpenTelemetry.meter_provider.add_metric_reader(console_metric_exporter)
+
+app { |_env| [200, {}, ['embedded app']] }
+lowlevel_error_handler { |_err| [500, {}, ['error page']] }
+
+threads 1, 1
+
+bind "unix://#{ENV.fetch('BIND_PATH', nil)}"
+
+plugin 'telemetry'
+
+Puma::Plugin::Telemetry.configure do |config|
+ config.add_target :open_telemetry, meter_provider: OpenTelemetry.meter_provider
+ config.frequency = 0.2
+ config.enabled = true
+ config.initial_delay = 2
+end
diff --git a/spec/integration/plugin_spec.rb b/spec/integration/plugin_spec.rb
index 81628c3..d70a058 100644
--- a/spec/integration/plugin_spec.rb
+++ b/spec/integration/plugin_spec.rb
@@ -95,6 +95,40 @@ class Plugin
end
end
+ context 'when open_telemetry target' do
+ let(:config) { 'open_telemetry' }
+ let(:expected_telemetry) do
+ {
+ 'workers.booted' => 1,
+ 'workers.total' => 1,
+ 'workers.spawned_threads' => 1,
+ 'workers.max_threads' => 1,
+ 'workers.requests_count' => 0,
+ 'queue.backlog' => 0,
+ 'queue.capacity' => 1,
+ }
+ end
+
+ it "doesn't crash" do
+ total_metrics = 0
+ matched_telemetry = {}
+
+ while (line = @server.next_line) do
+ if line.include?('OpenTelemetry::SDK::Metrics::State::MetricData')
+ name = @server.next_line.match(/name="(.*)"/)[1]
+
+ true until (line = @server.next_line).include?('value=')
+ value = line.match(/value=(.*)/)[1].to_i
+
+ matched_telemetry[name] = value
+
+ break if matched_telemetry.keys.size == expected_telemetry.keys.size
+ end
+ end
+ expect(matched_telemetry).to eq(expected_telemetry)
+ end
+ end
+
context 'when sockets telemetry' do
let(:config) { 'sockets' }
diff --git a/spec/puma/plugin/telemetry/config_spec.rb b/spec/puma/plugin/telemetry/config_spec.rb
index 5c5a50d..06aaf91 100644
--- a/spec/puma/plugin/telemetry/config_spec.rb
+++ b/spec/puma/plugin/telemetry/config_spec.rb
@@ -45,6 +45,21 @@ module Telemetry
end
end
+ context 'when built in: Open Telemetry' do
+ let(:meter_provider) { double('otel meter provider', meter: double('otel meter')) }
+
+ it 'adds new target' do
+ expect do
+ config.add_target(:open_telemetry, meter_provider: meter_provider)
+ end.to change(config.targets, :size).by(1)
+ end
+
+ it 'adds new Open Telemetry Target' do
+ config.add_target(:open_telemetry, meter_provider: meter_provider)
+ expect(config.targets.first).to be_a(Telemetry::Targets::OpenTelemetryTarget)
+ end
+ end
+
context 'when custom' do
let(:target) { proc { |telemetry| puts telemetry.inspect } }