Module: LogBrew::IssueDiagnostics

Defined in:
lib/logbrew/issue_diagnostics.rb

Overview

Privacy-bounded builders and validators for first-class issue evidence.

Generated exception frames contain code identity only. They are newest first, use basename-only filenames, and never include source text, local variables, arguments, raw stack strings, or exception messages.

Constant Summary collapse

MAX_STACK_FRAMES =
32
MAX_BREADCRUMBS =
64
MAX_EXCEPTIONS =
8
MAX_EXCEPTION_TYPE =
256
MAX_EXCEPTION_MESSAGE =
1_024
MAX_EXCEPTION_MODULE =
512
MAX_MECHANISM_TYPE =
64
MAX_FRAME_FILENAME =
2_048
MAX_FRAME_FUNCTION =
256
MAX_FRAME_MODULE =
512
MAX_BREADCRUMB_NAME =
64
MAX_BREADCRUMB_MESSAGE =
512
MAX_BREADCRUMB_DATA_FIELDS =
8
MAX_BREADCRUMB_DATA_STRING =
256
MAX_COORDINATE =
2_147_483_647
MACHINE_NAME =
/\A[A-Za-z][A-Za-z0-9_.:-]{0,63}\z/.freeze
DATA_KEY =
/\A[A-Za-z][A-Za-z0-9_.-]{0,63}\z/.freeze
DEBUG_ID =
/\A[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\z/.freeze
RFC3339 =
/\A[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]+)?(?:Z|[+-][0-9]{2}:[0-9]{2})\z/.freeze
CONTROL_CHARACTERS =
/[\u0000-\u001f\u007f-\u009f]/.freeze
{
  "trace" => "debug",
  "debug" => "debug",
  "log" => "info",
  "info" => "info",
  "warn" => "warning",
  "warning" => "warning",
  "error" => "error",
  "fatal" => "critical",
  "critical" => "critical"
}.freeze

Class Method Summary collapse

Class Method Details

Build one oldest-to-newest breadcrumb with bounded flat primitive data.



158
159
160
161
162
163
164
165
166
167
168
# File 'lib/logbrew/issue_diagnostics.rb', line 158

def breadcrumb(timestamp:, category:, type: nil, level: nil, message: nil, data: nil)
  value = {
    "timestamp" => timestamp,
    "category" => category
  }
  value["type"] = type unless type.nil?
  value["level"] = level unless level.nil?
  value["message"] = message unless message.nil?
  value["data"] = data unless data.nil?
  validate_breadcrumb(value)
end

.exception(type:, mechanism_type: nil, handled: nil) ⇒ Object

Build a typed exception identity and optional observation mechanism.



102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/logbrew/issue_diagnostics.rb', line 102

def exception(type:, mechanism_type: nil, handled: nil)
  value = { "type" => require_text("issue exception type", type, MAX_EXCEPTION_TYPE, true) }
  unless mechanism_type.nil? && handled.nil?
    if mechanism_type.nil? || (handled != true && handled != false)
      raise validation("issue exception mechanism must include type and handled")
    end
    value["mechanism"] = {
      "type" => require_machine_name(
        "issue exception mechanism type",
        mechanism_type,
        MAX_MECHANISM_TYPE,
        true
      ),
      "handled" => handled
    }
  end
  value
end

.from_exception(error, title: nil, level: "error", message: nil, mechanism_type: "ruby.exception", handled: true, metadata: nil, breadcrumbs: nil, breadcrumbs_truncated: false, include_stack_frames: true, context: nil) ⇒ Object

Build a complete issue attribute payload from an exception. Exception text remains application-controlled through the explicit message option.



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/logbrew/issue_diagnostics.rb', line 49

def from_exception(
  error,
  title: nil,
  level: "error",
  message: nil,
  mechanism_type: "ruby.exception",
  handled: true,
  metadata: nil,
  breadcrumbs: nil,
  breadcrumbs_truncated: false,
  include_stack_frames: true,
  context: nil
)
  unless error.is_a?(Exception)
    raise validation("issue error must be an exception")
  end

  exception_type = safe_exception_type(error)
  attributes = {
    "title" => title.nil? ? exception_type : title,
    "level" => level,
    "exception" => exception(
      type: exception_type,
      mechanism_type: mechanism_type,
      handled: handled
    )
  }
  attributes["message"] = message unless message.nil?
  root_stack = include_stack_frames ? stack_evidence_from_exception(error) : nil
  if include_stack_frames
    frames = root_stack.fetch(:frames)
    attributes["stackFrames"] = frames unless frames.empty?
  end
  attributes["exceptionChain"] = exception_chain_from_exception(
    error,
    root_exception: attributes.fetch("exception"),
    root_stack: root_stack,
    include_stack_frames: include_stack_frames
  )
  attributes["breadcrumbs"] = breadcrumbs unless breadcrumbs.nil?
  attributes["breadcrumbsTruncated"] = true if breadcrumbs_truncated
  attributes["metadata"] =  unless .nil?
  validated = validate_issue_attributes(attributes)
  unless context.nil?
    unless context.is_a?(TelemetryContext)
      raise validation("issue context must be a LogBrew::TelemetryContext")
    end
    validated["context"] = context
  end
  validated
end

.safe_exception_type(error) ⇒ Object



496
497
498
499
500
501
502
# File 'lib/logbrew/issue_diagnostics.rb', line 496

def safe_exception_type(error)
  candidate = error.class.name
  candidate = "anonymous_exception" if candidate.nil? || candidate.to_s.strip.empty?
  safe_generated_text(candidate, MAX_EXCEPTION_TYPE, true, "Exception")
rescue StandardError
  "Exception"
end

.stack_frame(filename:, line:, column: 1, function: nil, module_name: nil, in_app: nil, debug_id: nil) ⇒ Object

Build one explicit structured frame. Absolute filenames are reduced to their basename and URL query or fragment text is removed.



123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/logbrew/issue_diagnostics.rb', line 123

def stack_frame(
  filename:,
  line:,
  column: 1,
  function: nil,
  module_name: nil,
  in_app: nil,
  debug_id: nil
)
  frame = {
    "filename" => filename,
    "line" => line,
    "column" => column
  }
  frame["function"] = function unless function.nil?
  frame["module"] = module_name unless module_name.nil?
  frame["inApp"] = in_app unless in_app.nil?
  frame["debugId"] = debug_id unless debug_id.nil?
  validate_stack_frame(frame)
end

.stack_frames_from_exception(error) ⇒ Object

Project an exception backtrace into at most 32 newest-first code frames.



145
146
147
148
149
150
151
152
153
154
155
# File 'lib/logbrew/issue_diagnostics.rb', line 145

def stack_frames_from_exception(error)
  unless error.is_a?(Exception)
    raise validation("issue error must be an exception")
  end

  locations = safe_backtrace_locations(error)
  frames = locations.first(MAX_STACK_FRAMES).map { |location| frame_from_location(location) }.compact
  return frames unless frames.empty?

  safe_backtrace(error).first(MAX_STACK_FRAMES).map { |line| frame_from_backtrace_line(line) }.compact
end

.validate_issue_attributes(attributes) ⇒ Object

Validate and detach a complete issue attribute payload.



171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/logbrew/issue_diagnostics.rb', line 171

def validate_issue_attributes(attributes)
  unless attributes.is_a?(Hash)
    raise validation("issue attributes must be an object")
  end

  title = read_required(attributes, "title", "issue title")
  Validation.require_non_empty("issue title", title)
  level = read_required(attributes, "level", "issue level")
  Validation.require_allowed_value("issue level", level, SEVERITY_VALUES)
  payload = {
    "title" => copy_string(title),
    "level" => SEVERITY_ALIASES.fetch(level)
  }

  if has_key?(attributes, "message")
    message = read_value(attributes, "message")
    unless message.is_a?(String) && message.valid_encoding?
      raise validation("issue message must be a string")
    end
    payload["message"] = message.dup
  end
  payload["exception"] = validate_exception(read_value(attributes, "exception")) if has_key?(attributes, "exception")
  if has_key?(attributes, "exceptionChain")
    payload["exceptionChain"] = validate_exception_chain(
      read_value(attributes, "exceptionChain"),
      payload["exception"],
      has_key?(attributes, "stackFrames") ? validate_stack_frames(read_value(attributes, "stackFrames")) : nil
    )
  end
  payload["stackFrames"] = validate_stack_frames(read_value(attributes, "stackFrames")) if has_key?(attributes, "stackFrames")
  payload["breadcrumbs"] = validate_breadcrumbs(read_value(attributes, "breadcrumbs")) if has_key?(attributes, "breadcrumbs")
  if has_key?(attributes, "breadcrumbsTruncated")
    truncated = read_value(attributes, "breadcrumbsTruncated")
    unless truncated == true || truncated == false
      raise validation("issue breadcrumbsTruncated must be a boolean")
    end
    payload["breadcrumbsTruncated"] = true if truncated
  end

  if has_key?(attributes, "metadata")
     = Validation.(read_value(attributes, "metadata"))
    payload["metadata"] = () unless .nil?
  end
  payload
end