Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down
5 changes: 4 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
50 changes: 50 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. <br> ex) prefix: 'puma' => 'puma.workers.booted' | nil | No | |
| suffix | Metric name suffix. <br> 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.
Expand Down
1 change: 1 addition & 0 deletions lib/puma/plugin/telemetry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion lib/puma/plugin/telemetry/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
53 changes: 53 additions & 0 deletions lib/puma/plugin/telemetry/targets/open_telemetry_target.rb
Original file line number Diff line number Diff line change
@@ -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
32 changes: 32 additions & 0 deletions spec/fixtures/open_telemetry.rb
Original file line number Diff line number Diff line change
@@ -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
34 changes: 34 additions & 0 deletions spec/integration/plugin_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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' }

Expand Down
15 changes: 15 additions & 0 deletions spec/puma/plugin/telemetry/config_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 } }

Expand Down