Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
9 changes: 8 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@ source 'https://rubygems.org'
# Specify your gem's dependencies in puma-plugin-telemetry.gemspec
gemspec

gem 'dogstatsd-ruby'
group :development, :test do
gem 'dogstatsd-ruby'
gem 'opentelemetry-exporter-otlp'
gem 'opentelemetry-exporter-otlp-metrics', github: 'joshwestbrook/opentelemetry-ruby', branch: 'gauge-encoding', glob: 'exporter/otlp-metrics/*.gemspec' # TODO: Once gauge exporting is released, we can switch back to released version: https://github.com/open-telemetry/opentelemetry-ruby/pull/1780
gem 'opentelemetry-metrics-api', github: 'open-telemetry/opentelemetry-ruby', glob: 'metrics_api/*.gemspec' # TODO: Once gauges are released, we can switch back to released version: https://github.com/open-telemetry/opentelemetry-ruby/commit/bb5159598850b42e9da54608a8af2fbe422193b7
gem 'opentelemetry-metrics-sdk', github: 'open-telemetry/opentelemetry-ruby', glob: 'metrics_sdk/*.gemspec' # TODO: Once gauges are released, we can switch back to released version: https://github.com/open-telemetry/opentelemetry-ruby/commit/bb5159598850b42e9da54608a8af2fbe422193b7
gem 'opentelemetry-sdk'
end
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

groups don't really matter in gems Gemfile, as it's used exclusively in development, only gems from .gemspec matter for end users

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point! Got those removed in b7aa0f0


gem 'rack'
gem 'rake', '~> 13.0'
Expand Down
70 changes: 70 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,3 +1,37 @@
GIT
remote: https://github.com/joshwestbrook/opentelemetry-ruby.git
revision: c32f2c0d490189f3bc39322c617cd137d6be8128
branch: gauge-encoding
glob: exporter/otlp-metrics/*.gemspec
specs:
opentelemetry-exporter-otlp-metrics (0.2.1)
google-protobuf (>= 3.18, < 5.0)
googleapis-common-protos-types (~> 1.3)
opentelemetry-api (~> 1.1)
opentelemetry-common (~> 0.20)
opentelemetry-metrics-api (~> 0.1)
opentelemetry-metrics-sdk (~> 0.2)
opentelemetry-sdk (~> 1.2)
opentelemetry-semantic_conventions

GIT
remote: https://github.com/open-telemetry/opentelemetry-ruby.git
revision: 035c32ad9791f6200733e087f2ee49e0a615879a
glob: metrics_api/*.gemspec
specs:
opentelemetry-metrics-api (0.1.1)
opentelemetry-api (~> 1.0)

GIT
remote: https://github.com/open-telemetry/opentelemetry-ruby.git
revision: 035c32ad9791f6200733e087f2ee49e0a615879a
glob: metrics_sdk/*.gemspec
specs:
opentelemetry-metrics-sdk (0.4.1)
opentelemetry-api (~> 1.1)
opentelemetry-metrics-api (~> 0.1.1)
opentelemetry-sdk (~> 1.2)

PATH
remote: .
specs:
Expand All @@ -8,10 +42,41 @@ 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-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 +129,11 @@ PLATFORMS

DEPENDENCIES
dogstatsd-ruby
opentelemetry-exporter-otlp
opentelemetry-exporter-otlp-metrics!
opentelemetry-metrics-api!
opentelemetry-metrics-sdk!
opentelemetry-sdk
puma-plugin-telemetry!
rack
rake (~> 13.0)
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,23 @@ Given gem provides built in target for Datadog StatsD client, that uses batch op

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.

**NOTE** Be sure to have `opentelemetry-metrics-sdk` gem installed.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would maybe update this not to include the gemfile snippet, i.e.

gem "opentelemetry-metrics-sdk"

and update the same note for dogstatsd

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure thing! b7aa0f0


```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
12 changes: 12 additions & 0 deletions lib/puma/plugin/telemetry/targets/datadog_statsd_target.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# frozen_string_literal: true

begin
require 'datadog/statsd'
rescue LoadError
# Gracefully handle the case when Datadog::Statsd is not installed
end
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's no need for this, as:

  • it worked so far without explicit require already as it was optional from the start :)
  • it would crash on the configuration with explicit message as well, as it's required to provide initialized client, like config.add_target :dogstatsd, client: Datadog::Statsd.new same for the open telemetry

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes it way simpler 😄 b7aa0f0


module Puma
class Plugin
module Telemetry
Expand All @@ -22,7 +28,13 @@ module Targets
# DatadogStatsdTarget.new(client: client)
#
class DatadogStatsdTarget
def self.available?
!defined?(Datadog::Statsd).nil?
end

def initialize(client:)
raise ArgumentError, ':dogstatsd target can only be used when `datadog/statsd` gem is installed' unless self.class.available?

@client = client
end

Expand Down
62 changes: 62 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,62 @@
# frozen_string_literal: true

begin
require 'opentelemetry-metrics-sdk'
rescue LoadError
# Gracefully handle the case when OpenTelemetry Metrics SDK is not installed
end

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 self.available?
!defined?(OpenTelemetry::SDK::Metrics).nil?
end

def initialize(meter_provider:, prefix: nil, suffix: nil, attributes: {})
raise ArgumentError, ':open_telemetry target can only be used when the `opentelemetry-metrics-sdk` and `opentelemetry-exporter-otlp-metrics` gems are installed' unless self.class.available?

@meter_provider = meter_provider
@meter = meter_provider.meter('puma.telemetry')
@prefix = prefix
@suffix = suffix
@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 are explicitly calling force_flush here, in order to persist
# all metrics, and not only the most recent ones.
#
def call(telemetry)
telemetry.each do |metric, value|
instrument(metric).record(value, attributes: @attributes)
end

@meter_provider.try(: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