-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add log target and l2met transform #21
Changes from all commits
e0ae457
41a7813
a2eb978
de55a37
6f2ceaf
00e94ef
fcc2f48
d904b84
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -41,22 +41,61 @@ Puma::Plugin::Telemetry.configure do |config| | |
end | ||
``` | ||
|
||
### Basic | ||
### Basic IO Target | ||
|
||
Output telemetry as JSON to `STDOUT` | ||
A basic I/O target will emit telemetry data to `STDOUT`, formatted in JSON. | ||
|
||
```ruby | ||
config.add_target :io | ||
config.add_target :io | ||
``` | ||
|
||
#### Options | ||
|
||
This target has configurable `formatter:` and `transform:` options. | ||
The `formatter:` options are | ||
|
||
* `:json` _(default)_ - Print the logs in JSON. | ||
* `:logfmt` - Print the logs in key/value pairs, as per `logfmt`. | ||
* `:noop` - A NOOP formatter which returns the telemetry `Hash` unaltered, passing it directly to the `io:` instance. | ||
|
||
The `transform:` options are | ||
|
||
* `:cloud_watch` _(default)_ - Transforms telemetry keys, replacing dots with dashes to support AWS CloudWatch Log Metrics filters. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This does keep things backward compatible, though I wonder if it's a bit surprising and changing the default, as part of a breaking-change 2.0 release, might be the way to go? I could see the |
||
* `:logfmt` - Transforms telemetry keys, prepending `sample#` for [L2Met][l2met] consumption. | ||
* `:noop` - A NOOP transform which returns the telemetry `Hash` unaltered. | ||
|
||
### Log target | ||
|
||
Emitting to `STDOUT` via the basic `IOTarget` can work for getting telemetry into logs, we also provide an explicit `LogTarget`. | ||
This target will defaults to emitting telemetry at the `INFO` log level via a [standard library `::Logger`][logger] instance. | ||
That default logger will print to `STDOUT` in [the `logfmt` format][logfmt]. | ||
|
||
```ruby | ||
config.add_target :log | ||
``` | ||
|
||
You can pass an explicit `logger:` option if you wanted to, for example, use the same logger as Rails. | ||
|
||
```ruby | ||
config.add_target :log, logger: Rails.logger | ||
``` | ||
|
||
This target also has configurable `formatter:` and `transform:` options. | ||
The [possible options are the same as for the `IOTarget`](#options), but the defaults are different. | ||
The `LogTarget` defaults to `formatter: :logfmt`, and `transform: :noop` | ||
|
||
[l2met]: https://github.com/ryandotsmith/l2met?tab=readme-ov-file#l2met "l2met - Logs to Metrics" | ||
[logfmt]: https://brandur.org/logfmt "logfmt - Structured log format" | ||
[logger]: https://rubyapi.org/o/logger "Ruby's Logger, from the stdlib" | ||
|
||
### Datadog StatsD target | ||
|
||
Given gem provides built in target for Datadog StatsD client, that uses batch operation to publish metrics. | ||
A target for the Datadog StatsD client, that uses batch operation to publish metrics. | ||
|
||
**NOTE** Be sure to have `dogstatsd` gem installed. | ||
**NOTE** Be sure to have the `dogstatsd` gem installed. | ||
|
||
```ruby | ||
config.add_target :dogstatsd, client: Datadog::Statsd.new | ||
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. | ||
|
@@ -75,6 +114,7 @@ Puma::Plugin::Telemetry.configure do |config| | |
config.socket_parser = :inspect | ||
config.add_target :io, formatter: :json, io: StringIO.new | ||
config.add_target :dogstatsd, client: Datadog::Statsd.new(tags: { env: ENV["RAILS_ENV"] }) | ||
config.add_target :log, logger: Rails.logger, formatter: :logfmt, transform: :l2met) | ||
end | ||
``` | ||
|
||
|
@@ -85,8 +125,8 @@ Target is a simple object that implements `call` methods that accepts `telemetry | |
Just be mindful that if the API takes long to call, it will slow down frequency with which telemetry will get reported. | ||
|
||
```ruby | ||
# Example logfmt to stdout target | ||
config.add_target proc { |telemetry| puts telemetry.map { |k, v| "#{k}=#{v.inspect}" }.join(" ") } | ||
# Example key/value log to `STDOUT` target | ||
config.add_target ->(telemetry) { puts telemetry.map { |k, v| "#{k}=#{v.inspect}" }.join(" ") } | ||
``` | ||
|
||
## Extra middleware | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
# frozen_string_literal: true | ||
|
||
require 'json' | ||
|
||
module Puma | ||
class Plugin | ||
module Telemetry | ||
module Formatters | ||
# JSON formatter, expects `call` method accepting telemetry hash | ||
# | ||
class JSONFormatter | ||
def self.call(telemetry) | ||
::JSON.dump(telemetry) | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
# frozen_string_literal: true | ||
|
||
module Puma | ||
class Plugin | ||
module Telemetry | ||
module Formatters | ||
# Logfmt formatter, expects `call` method accepting telemetry hash | ||
# | ||
class LogfmtFormatter | ||
def self.call(telemetry) | ||
telemetry.map { |k, v| "#{String(k)}=#{v.inspect}" }.join(' ') | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
# frozen_string_literal: true | ||
|
||
module Puma | ||
class Plugin | ||
module Telemetry | ||
module Formatters | ||
# A NOOP formatter - it returns the telemetry Hash it was given | ||
class NoopFormatter | ||
def self.call(telemetry) | ||
telemetry | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
# frozen_string_literal: true | ||
|
||
require_relative '../formatters/json_formatter' | ||
require_relative '../formatters/logfmt_formatter' | ||
require_relative '../formatters/noop_formatter' | ||
require_relative '../transforms/cloud_watch_transform' | ||
require_relative '../transforms/l2met_transform' | ||
require_relative '../transforms/noop_transform' | ||
|
||
module Puma | ||
class Plugin | ||
module Telemetry | ||
module Targets | ||
# A base class for other Targets concerned with formatting telemetry | ||
# | ||
class BaseFormattingTarget | ||
def initialize(formatter: :json, transform: :cloud_watch) | ||
@formatter = FORMATTERS.fetch(formatter) { formatter } | ||
@transform = TRANSFORMS.fetch(transform) { transform } | ||
end | ||
|
||
def call(_telemetry) | ||
raise "#{__method__} must be implemented by #{self.class.name}" | ||
end | ||
|
||
private | ||
|
||
attr_reader :formatter, :transform | ||
|
||
FORMATTERS = { | ||
json: Formatters::JSONFormatter, | ||
logfmt: Formatters::LogfmtFormatter, | ||
noop: Formatters::NoopFormatter | ||
}.freeze | ||
private_constant :FORMATTERS | ||
|
||
TRANSFORMS = { | ||
cloud_watch: Transforms::CloudWatchTranform, | ||
l2met: Transforms::L2metTransform, | ||
noop: Transforms::NoopTransform | ||
}.freeze | ||
private_constant :TRANSFORMS | ||
end | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
# frozen_string_literal: true | ||
|
||
require 'logger' | ||
require_relative 'base_formatting_target' | ||
|
||
module Puma | ||
class Plugin | ||
module Telemetry | ||
module Targets | ||
# Simple Log Target, publishing metrics to a Ruby ::Logger at stdout | ||
# at the INFO log level | ||
# | ||
class LogTarget < BaseFormattingTarget | ||
def initialize(logger: ::Logger.new($stdout), formatter: :logfmt, transform: :noop) | ||
super(formatter: formatter, transform: transform) | ||
@logger = logger | ||
end | ||
|
||
def call(telemetry) | ||
logger.info(formatter.call(transform.call(telemetry))) | ||
end | ||
|
||
private | ||
|
||
attr_reader :logger | ||
end | ||
end | ||
end | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I fully like the name noop, I think passthrough might be a better option for naming, but I'm really open minded about this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't really love it either.
:passthrough
seems a reasonable alternative to me. Anyone have other alternatives or thoughts?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any further thoughts? I'm happy to rename to
:passthrough
; it is a bit more explicit of how it's not doing anything. 😄