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_EXCEPTION_TYPE =
256
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.



148
149
150
151
152
153
154
155
156
157
158
# File 'lib/logbrew/issue_diagnostics.rb', line 148

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.



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/logbrew/issue_diagnostics.rb', line 92

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.



46
47
48
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
# File 'lib/logbrew/issue_diagnostics.rb', line 46

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?
  if include_stack_frames
    frames = stack_frames_from_exception(error)
    attributes["stackFrames"] = frames unless frames.empty?
  end
  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



375
376
377
378
379
380
381
# File 'lib/logbrew/issue_diagnostics.rb', line 375

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.



113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/logbrew/issue_diagnostics.rb', line 113

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.



135
136
137
138
139
140
141
142
143
144
145
# File 'lib/logbrew/issue_diagnostics.rb', line 135

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.



161
162
163
164
165
166
167
168
169
170
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
# File 'lib/logbrew/issue_diagnostics.rb', line 161

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")
  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