🪟 Windows TippsModify Windows Support Phone Number with PowerShell(03.09.2026 um 00:00 Uhr)
🔧 AI Nachrichten Podcast: ChatGPT schwatzt Nutzern in Deutschland jetzt Werbung auf(28.08.2026 um 08:46 Uhr)
🪟 Windows TippsMicrosoft bringt Emoji 17.0 auf Windows 11(31.08.2026 um 08:16 Uhr)
🪟 Windows TippsModify Windows Support Phone Number with PowerShell(03.09.2026 um 00:00 Uhr)
🔧 AI Nachrichten Podcast: ChatGPT schwatzt Nutzern in Deutschland jetzt Werbung auf(28.08.2026 um 08:46 Uhr)
🪟 Windows TippsMicrosoft bringt Emoji 17.0 auf Windows 11(31.08.2026 um 08:16 Uhr)

🔧 Programmierung 🕛 vor 8 Monaten 10 Min Lesezeit
0

Switching APM providers without the headache

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht




The context



Last week a friend called me asking which APM/error tracking tool I use. He had his current provider scattered all over the project and wanted to switch to something different.



I've been using AppSignal or New Relic since 2019 and really like them. In his case, I recommended AppSignal for reasons we can discuss in another post to keep this one short.



But the conversation went further: "What if a year from now I want to switch again? Will I have to find & replace across the entire project?"



I suggested he create an adapter. He said he'd never implemented one in practice.



Well, we built it together that afternoon. And that's exactly what I'm going to share here.









Before: provider coupled throughout the code



His project looked like this:




CODE
# app/controllers/orders_controller.rb
class OrdersController < ApplicationController
def create
@order = Order.new(order_params)

if @order.save
redirect_to @order
else
ProviderX.capture_message("Order creation failed", extra: { errors: @order.errors.full_messages })
render :new
end
end
end

# app/services/payment_service.rb
class PaymentService
def process(order)
gateway.charge(order.total)
rescue PaymentError => e
ProviderX.capture_exception(e, extra: { order_id: order.id })
raise
end
end

# app/jobs/sync_inventory_job.rb
class SyncInventoryJob < ApplicationJob
def perform
InventorySync.run!
rescue => e
ProviderX.capture_exception(e, tags: { job: "inventory_sync" })
raise
end
end






The problem is clear: ProviderX scattered across dozens of files. To switch tools, he would have to:




  1. Find all occurrences

  2. Understand the context of each one

  3. Adapt to the new API (which is different)

  4. Hope he didn't miss any



I've been there. It's not fun.









About over-engineering



Before showing the solution, I need to make something clear: I'm not a fan of implementing design patterns for everything. I like to keep things simple.



But this is one of those cases where it makes sense:




  • It's an external service that can change

  • The call is spread across dozens of files

  • Each provider's API is different

  • The cost of creating the abstraction is low



When these factors align, it's worth the investment.









The solution: Adapter Pattern



The idea is to create a layer between the application and the tracking tool. The application only knows this layer. The layer knows the specific tool.






Structure






CODE
app/
└── services/
└── error_tracking/
├── base_adapter.rb
├── appsignal_adapter.rb
├── null_adapter.rb
└── tracker.rb









1. Base contract



First, we define the contract that all adapters must follow:




CODE
# app/services/error_tracking/base_adapter.rb
# frozen_string_literal: true

module ErrorTracking
# Abstract base class for error tracking adapters
# All adapters must implement these methods
class BaseAdapter
# Capture and report an exception
# @param exception [Exception] the exception to capture
# @param extra [Hash] additional context data
# @param level [Symbol] severity level (:error, :warning, :info)
# @param tags [Hash] tags for categorization
def capture_exception(exception, extra: {}, level: :error, tags: {})
raise NotImplementedError, "#{self.class} must implement #capture_exception"
end

# Capture and report a message
# @param message [String] the message to capture
# @param extra [Hash] additional context data
# @param level [Symbol] severity level (:error, :warning, :info)
# @param tags [Hash] tags for categorization
def capture_message(message, extra: {}, level: :info, tags: {})
raise NotImplementedError, "#{self.class} must implement #capture_message"
end

# Set user context for subsequent captures
# @param user_data [Hash] user information (id, email, username, type, etc.)
def set_user(user_data)
raise NotImplementedError, "#{self.class} must implement #set_user"
end

# Set request context for subsequent captures
# @param request_data [Hash] request information (method, path, url, ip, user_agent, params, headers)
def set_request(request_data)
raise NotImplementedError, "#{self.class} must implement #set_request"
end

# Set custom context data
# @param context_data [Hash] custom context information
def set_context(context_data)
raise NotImplementedError, "#{self.class} must implement #set_context"
end

# Add custom context/scope
# @yield [scope] block to configure scope
def configure_scope(&)
raise NotImplementedError, "#{self.class} must implement #configure_scope"
end

# Add breadcrumb for debugging trail
# @param message [String] breadcrumb message
# @param category [String] category for grouping
# @param data [Hash] additional data
def add_breadcrumb(message, category: "custom", data: {})
raise NotImplementedError, "#{self.class} must implement #add_breadcrumb"
end

# Get trace propagation meta for distributed tracing (HTML views)
# @return [String] HTML meta tags for tracing
def get_trace_propagation_meta
raise NotImplementedError, "#{self.class} must implement #get_trace_propagation_meta"
end

# Check if the adapter is enabled/configured
# @return [Boolean]
def enabled?
raise NotImplementedError, "#{self.class} must implement #enabled?"
end
end
end









2. AppSignal Adapter



The implementation I use in production:




CODE
# app/services/error_tracking/appsignal_adapter.rb
# frozen_string_literal: true

module ErrorTracking
class AppsignalAdapter < BaseAdapter
def capture_exception(exception, extra: {}, level: :error, tags: {})
return unless enabled?

Appsignal.send_error(exception) do |transaction|
transaction.set_namespace(namespace_from_level(level))
transaction.set_tags(tags) if tags.any?
transaction.set_params(extra) if extra.any?
end
end

def capture_message(message, extra: {}, level: :info, tags: {})
return unless enabled?

Appsignal.send_error(StandardError.new(message)) do |transaction|
transaction.set_namespace(namespace_from_level(level))
transaction.set_tags(tags.merge(type: "message"))
transaction.set_params(extra) if extra.any?
end
end

def set_user(user_data)
return unless enabled?

Appsignal.add_tags(
user_id: user_data[:id]&.to_s,
user_email: user_data[:email],
username: user_data[:username],
user_type: user_data[:type]
)
end

def set_request(request_data)
return unless enabled?

Appsignal.add_tags(
request_method: request_data[:method],
request_path: request_data[:path],
request_url: truncate_value(request_data[:url]),
request_ip: request_data[:ip],
request_user_agent: truncate_value(request_data[:user_agent])
)

Appsignal.add_params(request_data[:params]) if request_data[:params].present?
Appsignal.add_headers(sanitize_headers(request_data[:headers])) if request_data[:headers].present?
end

def set_context(context_data)
return unless enabled?

Appsignal.add_custom_data(context_data)
end

def configure_scope
return unless enabled?

yield(self) if block_given?
end

def add_breadcrumb(message, category: "custom", data: {})
return unless enabled?

Appsignal.add_breadcrumb(category, message, "", data)
end

def get_trace_propagation_meta
""
end

def enabled?
defined?(Appsignal) && Appsignal.config&.active?
end

private

def namespace_from_level(level)
case level
when :error then "error"
when :warning then "warning"
else "info"
end
end

def truncate_value(value, max_length: 255)
return nil if value.nil?

value.to_s[0, max_length]
end

def sanitize_headers(headers)
return {} if headers.nil?

sensitive_keys = %w[authorization cookie set-cookie x-api-key api-key token]
headers.transform_values.with_index do |(key, value), _|
sensitive_keys.any? { |k| key.to_s.downcase.include?(k) } ? "[FILTERED]" : value
end
end
end
end









3. NullAdapter



This is a detail that makes a difference day to day. In development and tests, we don't want to send errors to the real provider. The NullAdapter solves this:




CODE
# frozen_string_literal: true

module ErrorTracking
# Null Object pattern - does nothing, useful for development/test
class NullAdapter < BaseAdapter
def capture_exception(exception, extra: {}, level: :error, tags: {}) # rubocop:disable Lint/UnusedMethodArgument
log_in_development("Exception: #{exception.class} - #{exception.message}", extra, level)
nil
end

def capture_message(message, extra: {}, level: :info, tags: {}) # rubocop:disable Lint/UnusedMethodArgument
log_in_development("Message: #{message}", extra, level)
nil
end

def set_user(_user_data)
nil
end

def set_request(_request_data)
nil
end

def set_context(_context_data)
nil
end

def configure_scope
nil
end

def add_breadcrumb(_message, _category: "custom", _data: {})
nil
end

def get_trace_propagation_meta
""
end

def enabled?
false
end

private

def log_in_development(message, extra, level)
return unless Rails.env.development?

Rails.logger.tagged("ErrorTracking") do
Rails.logger.send(level == :error ? :error : :info, "#{message} | extra: #{extra.inspect}")
end
end
end
end










4. Tracker (facade)



The Tracker centralizes the API and adds some utility methods I use a lot:




CODE
# app/services/error_tracking/tracker.rb
# frozen_string_literal: true

module ErrorTracking
# Main facade for error tracking
# Usage:
# ErrorTracking.capture_exception(e, extra: { user_id: 123 })
# ErrorTracking.capture_message("Something happened", level: :warning)
#
# Configure in initializer:
# ErrorTracking.configure(adapter: ErrorTracking::AppsignalAdapter.new)
class Tracker
class << self
attr_writer :adapter

def adapter
@adapter ||= NullAdapter.new
end

# Delegate all methods to the adapter
delegate :capture_exception,
:capture_message,
:set_user,
:set_request,
:set_context,
:configure_scope,
:add_breadcrumb,
:get_trace_propagation_meta,
:enabled?,
to: :adapter

# Convenience method to capture exception with context and re-raise
def capture_and_reraise(exception, extra: {}, level: :error, tags: {})
capture_exception(exception, extra: extra, level: level, tags: tags)
raise exception
end

# Safe capture that never raises (for use in rescue blocks)
def safe_capture_exception(exception, extra: {}, level: :error, tags: {})
capture_exception(exception, extra: extra, level: level, tags: tags)
rescue StandardError => e
Rails.logger.error("ErrorTracking failed to capture: #{e.message}")
nil
end

# Safe message capture that never raises
def safe_capture_message(message, extra: {}, level: :info, tags: {})
capture_message(message, extra: extra, level: level, tags: tags)
rescue StandardError => e
Rails.logger.error("ErrorTracking failed to capture message: #{e.message}")
nil
end
end
end
end









5. Main module






CODE
# app/services/error_tracking.rb
# frozen_string_literal: true

module ErrorTracking
class << self
delegate :capture_exception,
:capture_message,
:set_user,
:set_request,
:set_context,
:configure_scope,
:add_breadcrumb,
:get_trace_propagation_meta,
:enabled?,
:safe_capture_exception,
:safe_capture_message,
:capture_and_reraise,
to: Tracker

def configure(adapter:)
Tracker.adapter = adapter
end
end
end









6. Configuration



For those using AppSignal, add the gem:




CODE
# Gemfile
gem "appsignal"









CODE
# config/initializers/error_tracking.rb
# frozen_string_literal: true

Rails.application.config.after_initialize do
adapter = if Rails.env.in?(%w[development test])
ErrorTracking::NullAdapter.new
else
ErrorTracking::AppsignalAdapter.new
end

ErrorTracking.configure(adapter: adapter)
end






In the future, if you want to migrate to New Relic, Datadog, Bugsnag or any other service, just create a new adapter and change this line.









After: decoupled code






CODE
# app/controllers/orders_controller.rb
class OrdersController < ApplicationController
def create
@order = Order.new(order_params)

if @order.save
redirect_to @order
else
ErrorTracking.capture_message(
"Order creation failed",
extra: { errors: @order.errors.full_messages },
level: :warning
)
render :new
end
end
end

# app/services/payment_service.rb
class PaymentService
def process(order)
ErrorTracking.add_breadcrumb("Starting payment", category: "payment")

gateway.charge(order.total)
rescue PaymentError => e
ErrorTracking.capture_and_reraise(e, extra: { order_id: order.id })
end
end

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
before_action :set_error_tracking_context

private

def set_error_tracking_context
ErrorTracking.set_request(
method: request.method,
path: request.path,
url: request.url,
ip: request.remote_ip,
user_agent: request.user_agent
)

return unless current_user

ErrorTracking.set_user(
id: current_user.id,
email: current_user.email,
username: current_user.name,
type: current_user.class.name
)
end
end












What we gained




























Before After
Switching tools = changing dozens of files Switching tools = create adapter + change 1 line
Tests depend on SDK Tests use NullAdapter
No logs in development NullAdapter logs locally
Different API for each tool Single consistent API








When to use this pattern?



I apply it when:




  • It's an external service that can change

  • The call is spread across multiple files

  • The service API is complex or different between providers



I don't apply it when:




  • It's used in a single place

  • The service will probably never change

  • The complexity doesn't justify it









Conclusion



By the end of that afternoon, my friend's project was ready for the future. If a year from now he wants to go back to the previous provider, switch to Datadog, or anything else, he just needs to create the adapter and change one line.



The code I shared here is a draft of the real code, but the adapters work perfectly in production. The controllers and usage examples are fictional to maintain project integrity.

Vollständiger Original-Bericht
Ausführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 0%
🟡 In Evaluierung 0%
🟢 Keine Auswirkung 0%
Spannende Innovation 0%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
1 Quelle
Modify Windows Support Phone Number with PowerShell
1 Quelle
Die Zukunft des Einkaufens: Warum wir ein neues Kapitel aufschlagen (und wie du es mitschreiben kannst)
1 Quelle
ZDE Podcast 251: Wie sieht digitales Instore Marketing 2026 aus, Amit Chatterjee?
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Switching APM providers without the headache

Thematisch verwandte Begriffe: Switching, providers, without, headache · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...