Skip to content

Commit

Permalink
feat: Telemetry service and middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
jrgns committed Sep 17, 2023
1 parent 98d13da commit c12cc23
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 5 deletions.
5 changes: 5 additions & 0 deletions lib/ditty/components/ditty.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ def self.load
controllers = File.expand_path('../controllers', __dir__)
Dir.glob("#{controllers}/*.rb").sort.each { |f| require f }

services = File.expand_path('../services', __dir__)
Dir.glob("#{services}/*.rb").sort.each { |f| require f }

require 'ditty/models/user'
require 'ditty/models/role'
require 'ditty/models/identity'
Expand All @@ -19,6 +22,8 @@ def self.load
def self.configure(_container)
require 'ditty/db' unless defined? ::DB
require 'ditty/listener'

self.load
end

def self.migrations
Expand Down
23 changes: 19 additions & 4 deletions lib/ditty/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
require 'ditty/helpers/pundit'
require 'ditty/helpers/authentication'
require 'ditty/services/logger'
require 'ditty/middleware/telemetry'
require 'active_support'
require 'active_support/inflector'
require 'rack/contrib'
Expand All @@ -36,6 +37,7 @@ class ApplicationController < Sinatra::Base

register Sinatra::Flash, Sinatra::RespondWith

use Middleware::Telemetry
use Rack::JSONBodyParser
use Rack::MethodOverride
use Rack::NestedParams
Expand Down Expand Up @@ -65,6 +67,15 @@ def config(name, default = '')
end
end

# Fine for certain things, but using middleware might be a better idea
before do
broadcast(:before_route, target: self)
end

after do
broadcast(:after_route, target: self)
end

def view_folders
folders = ['./views']
folders << settings.view_folder if settings.view_folder
Expand Down Expand Up @@ -98,6 +109,7 @@ def find_template(views, name, engine, &block)
end

not_found do
broadcast(:application_error, env['sinatra.error'], error_code: 404, target: self)
respond_to do |format|
status 404
format.html do
Expand All @@ -114,7 +126,7 @@ def find_template(views, name, engine, &block)
end

error Helpers::NotAuthenticated, ::Pundit::NotAuthorizedError do
# TODO: Check if this is logged / tracked
broadcast(:application_error, env['sinatra.error'], error_code: 403, target: self)
if authenticated?
respond_to do |format|
status 403
Expand All @@ -141,6 +153,7 @@ def find_template(views, name, engine, &block)
end

error ::Sinatra::Param::InvalidParameterError do
broadcast(:application_error, env['sinatra.error'], error_code: 400, target: self)
respond_to do |format|
status 400
format.html do
Expand All @@ -155,6 +168,7 @@ def find_template(views, name, engine, &block)
end

error ::Sequel::NoMatchingRow do
broadcast(:application_error, env['sinatra.error'], error_code: 404, target: self)
respond_to do |format|
status 404
format.html do
Expand All @@ -171,6 +185,7 @@ def find_template(views, name, engine, &block)
end

error ::Sequel::ValidationFailed do
broadcast(:application_error, env['sinatra.error'], error_code: 400, target: self)
respond_to do |format|
entity = env['sinatra.error'].model
errors = env['sinatra.error'].errors
Expand All @@ -187,7 +202,7 @@ def find_template(views, name, engine, &block)

error ::Sequel::ForeignKeyConstraintViolation do
error = env['sinatra.error']
broadcast(:application_error, error)
broadcast(:application_error, error, error_code: 400, target: self)
logger.error error
respond_to do |format|
status 400
Expand All @@ -203,7 +218,7 @@ def find_template(views, name, engine, &block)
error ::Ditty::TemplateNotFoundError do
# TODO: Display a better error message
error = env['sinatra.error']
broadcast(:application_error, error)
broadcast(:application_error, error, error_code: 500, target: self)
logger.error error
respond_to do |format|
status 500
Expand All @@ -215,7 +230,7 @@ def find_template(views, name, engine, &block)

error do
error = env['sinatra.error']
broadcast(:application_error, error)
broadcast(:application_error, error, error_code: 500, target: self)
logger.error error
respond_to do |format|
status 500
Expand Down
46 changes: 46 additions & 0 deletions lib/ditty/middleware/telemetry.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# frozen_string_literal: true

require 'ditty/services/open_telemetry'

module Ditty
module Middleware
class Telemetry
attr_reader :env, :telemetry

def initialize(app, telemetry = nil)
@app = app
@telemetry = telemetry || Services::OpenTelemetry.new
end

def call(env)
@env = env
request = Rack::Request.new(env)
attribs = {
::OpenTelemetry::SemanticConventions::Trace::HTTP_URL => request.url,
::OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => request.request_method,
}

telemetry.instrumented("#{request.request_method} #{request.path}", attribs) do |span|
# Not sure if we should try and catch exceptions?
result = @app.call(env)
span.add_attributes(
::OpenTelemetry::SemanticConventions::Trace::HTTP_STATUS_CODE => result[0]
)
result
end
end

class << self
def method_missing(method, *args, &block)
instance.send(method, *args, &block)
end

def respond_to_missing?(method, _include_private)
return true if instance.respond_to?(method)

super
end
end
end
end
end
2 changes: 1 addition & 1 deletion lib/ditty/services/logger.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def initialize
end
end

# TODO: REfac this so that you can log something like ES to a separate logger
# TODO: Refac this so that you can log something like ES to a separate logger

def method_missing(method, *args, &block)
loggers.each { |logger| logger.send(method, *args, &block) }
Expand Down
54 changes: 54 additions & 0 deletions lib/ditty/services/open_telemetry.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# frozen_string_literal: true

module Ditty
module Services
class OpenTelemetry
def tracer
# ::OpenTelemetry::Instrumentation::Sinatra::Instrumentation.instance.tracer
@tracer ||= ::OpenTelemetry.tracer_provider.tracer('Ditty', ::Ditty::VERSION)
end

def instrumented?
(ENV['DITTY_TELEMETRY_ENABLED'] || 0).to_i == 1
end

def instrumented(span_name, attribs = {})
return yield unless instrumented?

tracer.in_span(span_name, attributes: base_attributes.merge(attribs)) do |span|
yield(span)
end
end

def application_error(error, opts = {})
error ||= env['sinatra.error']
return unless error.is_a? StandardError
return unless @request_span

request = opts[:target].request
@request_span&.record_exception(
error,
attributes: base_attributes.merge(
::OpenTelemetry::SemanticConventions::Trace::HTTP_URL => request.url,
::OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => request.request_method,
::OpenTelemetry::SemanticConventions::Trace::HTTP_STATUS_CODE => opts[:error_code] || 500
)
)
if (opts[:error_code] || 500) >= 500
@request_span&.status = ::OpenTelemetry::Trace::Status.error("Application Error: #{error.class}")
end
@request_span&.finish
end

private

def base_attributes
{
'ditty.version' => ::Ditty::VERSION,
}
end
end
end
end

Wisper.subscribe(::Ditty::Services::OpenTelemetry.new, on: %i[application_error]) unless ENV['RACK_ENV'] == 'test'

0 comments on commit c12cc23

Please sign in to comment.