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
- BREADCRUMB_LEVELS =
{ "trace" => "debug", "debug" => "debug", "log" => "info", "info" => "info", "warn" => "warning", "warning" => "warning", "error" => "error", "fatal" => "critical", "critical" => "critical" }.freeze
Class Method Summary collapse
-
.breadcrumb(timestamp:, category:, type: nil, level: nil, message: nil, data: nil) ⇒ Object
Build one oldest-to-newest breadcrumb with bounded flat primitive data.
-
.exception(type:, mechanism_type: nil, handled: nil) ⇒ Object
Build a typed exception identity and optional observation mechanism.
-
.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.
- .safe_exception_type(error) ⇒ Object
-
.stack_frame(filename:, line:, column: 1, function: nil, module_name: nil, in_app: nil, debug_id: nil) ⇒ Object
Build one explicit structured frame.
-
.stack_frames_from_exception(error) ⇒ Object
Project an exception backtrace into at most 32 newest-first code frames.
-
.validate_issue_attributes(attributes) ⇒ Object
Validate and detach a complete issue attribute payload.
Class Method Details
.breadcrumb(timestamp:, category:, type: nil, level: nil, message: nil, data: nil) ⇒ Object
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 (timestamp:, category:, type: nil, level: nil, message: nil, data: nil) value = { "timestamp" => , "category" => category } value["type"] = type unless type.nil? value["level"] = level unless level.nil? value["message"] = unless .nil? value["data"] = data unless data.nil? (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"] = unless .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"] = unless .nil? attributes["breadcrumbsTruncated"] = true if 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") = read_value(attributes, "message") unless .is_a?(String) && .valid_encoding? raise validation("issue message must be a string") end payload["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"] = (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 |