Class: RailsSemanticLogging::Formatters::Datadog

Inherits:
SemanticLogger::Formatters::Raw
  • Object
show all
Defined in:
lib/rails_semantic_logging/formatters/datadog.rb

Overview

Datadog-optimized JSON formatter that maps log fields to Datadog Standard Attributes (docs.datadoghq.com/standard-attributes/).

Key mappings:

name              -> logger.name
level             -> status
duration          -> duration (nanoseconds) + duration_human (Rails format)
exception         -> error: { kind, message, stack }
payload           -> http: { status_code, method, url, ... } (controller requests)
named_tags.dd     -> dd (top-level, for trace linking)
named_tags.user_* -> usr.{id, email, name, role}

Constant Summary collapse

NANOSECONDS_PER_MILLISECOND =
1_000_000
HTTP_PAYLOAD_MAP =

Mapping of Rails payload keys to Datadog http standard attribute names

{
  status: :status_code,
  method: :method,
  host: :host,
  user_agent: :useragent,
  referer: :referer
}.freeze
USER_NAMED_TAGS_MAP =

Mapping of user-related named_tags to usr.* standard attributes

{
  user_id: :id,
  user_email: :email,
  user_name: :name,
  user_role: :role
}.freeze
ROUTING_ERROR_MESSAGE =

Matches ActionController::RoutingError messages like:

No route matches [GET] "/some/path"

Used to extract http.method and http.url_details.path from the exception.

/\ANo route matches \[(?<method>\w+)\]\s+"(?<path>[^"]+)"/

Instance Method Summary collapse

Constructor Details

#initialize(time_format: :iso_8601, time_key: :timestamp, **args) ⇒ Datadog

rubocop:disable Naming/VariableNumber



41
42
43
# File 'lib/rails_semantic_logging/formatters/datadog.rb', line 41

def initialize(time_format: :iso_8601, time_key: :timestamp, **args) # rubocop:disable Naming/VariableNumber
  super(time_format:, time_key:, log_application: false, log_host: true, log_environment: false, **args)
end

Instance Method Details

#call(log, logger) ⇒ Object



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/rails_semantic_logging/formatters/datadog.rb', line 45

def call(log, logger)
  super
  # SemanticLogger::Formatters::Raw assigns log.payload and log.named_tags
  # to hash[:payload] / hash[:named_tags] BY REFERENCE. We mutate those
  # below (remap_http_payload deletes :host/:method/etc., remap_named_tags
  # deletes :request_id/:client_ip/:dd/etc., deep_compact_blank! drops
  # every blank value). Without these dups the formatter strips keys off
  # the underlying log event, which breaks any consumer that reads
  # log.payload / log.named_tags after formatting (other appenders,
  # RSpec matchers that re-inspect the captured event).
  hash[:payload] = hash[:payload].dup if hash[:payload].is_a?(Hash)
  hash[:named_tags] = hash[:named_tags].dup if hash[:named_tags].is_a?(Hash)
  remap_named_tags
  remap_http_payload
  parse_url_details
  apache_message
  deep_compact_blank!(hash)
  hash.to_json
end

#durationObject



87
88
89
90
91
92
93
94
95
96
# File 'lib/rails_semantic_logging/formatters/datadog.rb', line 87

def duration
  # Propagate duration from payload if not set on log
  log.duration = log.payload[:duration] if log.duration.nil? && log.payload&.dig(:duration)
  return unless log.duration

  # Datadog standard: duration in nanoseconds
  hash[:duration] = (log.duration * NANOSECONDS_PER_MILLISECOND).to_i
  # Human-readable duration for readability (Rails format)
  hash[:duration_human] = "#{log.duration.round(2)}ms"
end

#exceptionObject



98
99
100
101
102
103
104
105
106
107
108
# File 'lib/rails_semantic_logging/formatters/datadog.rb', line 98

def exception
  return unless log.exception

  hash[:error] = {
    kind: log.exception.class.name,
    message: log.exception.message,
    stack: log.exception.backtrace&.join("\n")
  }

  parse_routing_error
end

#levelObject



73
74
75
# File 'lib/rails_semantic_logging/formatters/datadog.rb', line 73

def level
  hash[:status] = log.level
end

#messageObject

Fall back to the exception message when the log carries no message of its own. rails_semantic_logger logs unmatched routes (and other rescued exceptions) via ActionDispatch::DebugExceptions by passing only the exception object, so without this the Datadog ‘message` field would be empty for those events.



82
83
84
85
# File 'lib/rails_semantic_logging/formatters/datadog.rb', line 82

def message
  super
  hash[:message] ||= log.exception&.message
end

#metricObject

SemanticLogger’s :dimensions log attribute carries metric tags (e.g. ‘logger.info(’Processed feed’, metric: ‘feed_size’, metric_amount: 12, dimensions: { feed: ‘amazon’ })‘). The Raw formatter sets :metric and :metric_amount but not :dimensions — surface it at the top level so the Datadog UI can filter on those tags directly without diving into named_tags.



115
116
117
118
# File 'lib/rails_semantic_logging/formatters/datadog.rb', line 115

def metric
  super
  hash[:dimensions] = log.dimensions if log.dimensions.respond_to?(:any?) && log.dimensions.any?
end

#nameObject



69
70
71
# File 'lib/rails_semantic_logging/formatters/datadog.rb', line 69

def name
  hash[:'logger.name'] = log.name if log.name
end

#thread_nameObject



65
66
67
# File 'lib/rails_semantic_logging/formatters/datadog.rb', line 65

def thread_name
  # Exclude thread_name from output
end