Module: Logtide::StructuredException

Defined in:
lib/logtide/structured_exception.rb

Overview

Serialises a Ruby exception into the StructuredException wire shape (spec 003 section 4): type, message, language, stacktrace (outermost first), the cause chain (capped at 10, cycle-safe) and an optional raw trace.

Constant Summary collapse

LANGUAGE =
"ruby"
MAX_CAUSE_DEPTH =
10
MAX_FRAMES =
100

Class Method Summary collapse

Class Method Details

.build(exception, depth, seen, include_stacktrace) ⇒ Object



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# File 'lib/logtide/structured_exception.rb', line 20

def build(exception, depth, seen, include_stacktrace)
  seen[exception] = true
  result = {
    "type" => type_of(exception),
    "message" => message_of(exception),
    "language" => LANGUAGE
  }

  if include_stacktrace
    frames = stacktrace(exception)
    result["stacktrace"] = frames unless frames.empty?
    raw = raw_text(exception)
    result["raw"] = raw if raw
  end

  cause = safe_cause(exception)
  result["cause"] = build(cause, depth + 1, seen, include_stacktrace) if follow_cause?(cause, depth, seen)

  result
end

.follow_cause?(cause, depth, seen) ⇒ Boolean

Returns:

  • (Boolean)


83
84
85
# File 'lib/logtide/structured_exception.rb', line 83

def follow_cause?(cause, depth, seen)
  cause && depth < MAX_CAUSE_DEPTH && !seen.key?(cause)
end

.frame(location) ⇒ Object



60
61
62
63
64
65
66
67
68
# File 'lib/logtide/structured_exception.rb', line 60

def frame(location)
  path = location.absolute_path || location.path
  {
    "file" => path,
    "function" => location.label,
    "line" => location.lineno,
    "metadata" => { "in_app" => in_app?(path) }
  }
end

.in_app?(path) ⇒ Boolean

Returns:

  • (Boolean)


70
71
72
73
74
# File 'lib/logtide/structured_exception.rb', line 70

def in_app?(path)
  return false unless path

  !(path.include?("/gems/") || path.start_with?(RbConfig::CONFIG["rubylibdir"].to_s))
end

.message_of(exception) ⇒ Object



46
47
48
49
# File 'lib/logtide/structured_exception.rb', line 46

def message_of(exception)
  message = exception.message.to_s
  message.empty? ? type_of(exception) : message
end

.raw_text(exception) ⇒ Object



76
77
78
79
80
81
# File 'lib/logtide/structured_exception.rb', line 76

def raw_text(exception)
  backtrace = exception.backtrace
  return nil unless backtrace

  (["#{type_of(exception)}: #{message_of(exception)}"] + backtrace).join("\n")
end

.safe_cause(exception) ⇒ Object



87
88
89
90
91
92
# File 'lib/logtide/structured_exception.rb', line 87

def safe_cause(exception)
  cause = exception.cause
  cause if cause.is_a?(Exception)
rescue StandardError
  nil
end

.serialize(exception, include_stacktrace: true) ⇒ Object



16
17
18
# File 'lib/logtide/structured_exception.rb', line 16

def serialize(exception, include_stacktrace: true)
  build(exception, 0, {}.compare_by_identity, include_stacktrace)
end

.stacktrace(exception) ⇒ Object



51
52
53
54
55
56
57
58
# File 'lib/logtide/structured_exception.rb', line 51

def stacktrace(exception)
  locations = exception.backtrace_locations
  return [] unless locations

  # Ruby backtraces are innermost-first; the wire format wants outermost
  # first, and truncation keeps the outermost frames (003 section 4).
  locations.reverse.first(MAX_FRAMES).map { |location| frame(location) }
end

.type_of(exception) ⇒ Object



41
42
43
44
# File 'lib/logtide/structured_exception.rb', line 41

def type_of(exception)
  name = exception.class.name
  name.nil? || name.empty? ? "Exception" : name
end