Skip to content

Commit

Permalink
Merge pull request #17 from Sija/develop
Browse files Browse the repository at this point in the history
v0.8
  • Loading branch information
Sija authored May 3, 2017
2 parents 396e2c8 + e4ee03b commit e022525
Show file tree
Hide file tree
Showing 13 changed files with 360 additions and 54 deletions.
48 changes: 47 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ yet there are no tests written, so use it at your own risk! - or kindly send a P
- [x] Integrations ([Kemal](https://github.com/kemalcr/kemal), [Sidekiq.cr](https://github.com/mperham/sidekiq.cr))
- [x] Async support
- [x] User Feedback (`Raven.send_feedback` + Kemal handler)
- [x] Crash handler

### TODO

- [ ] Tests
- [ ] Exponential backoff in case of connection error
- [ ] Caching unsent events for later send
- [ ] Catching app crashes, kind of a bin wrapper perhaps?

## Installation

Expand Down Expand Up @@ -177,6 +177,52 @@ Raven.extra_context happiness: "very"

For more information, see [Context](https://docs.sentry.io/clients/ruby/context/).

## Crash Handler

Since Crystal doesn't provide native handlers for unhandled exceptions
and sigfaults, *raven.cr* introduces its own crash handler compiled as
external binary.

### Setup

Easiest way of using it is adding appropriate entry to project's `shard.yml`:

```yaml
targets:
# other target definitions if any...

sentry.crash_handler:
main: lib/raven/src/crash_handler.cr
```
With above entry defined in `targets`, running `shards build` should result in
binary built in `bin/sentry.crash_handler`.

__NOTE__: While building you might specify `SENTRY_DSN` env variable, which will be
compiled into the binary (as plain-text) and used by the handler as
*Sentry* endpoint.

```bash
SENTRY_DSN=<private_dsn> shards build sentry.crash_handler
```

Pass `--release` flag to disable debug messages.

### Usage

You need to run your app with previously built `bin/sentry.crash_handler` in
front.

```bash
bin/sentry.crash_handler bin/your_app --some arguments --passed to your program
```

As one would expect, `STDIN` is passed to the original process, while
`STDOUT` and `STDERR` are piped back from it.

__NOTE__: You can always pass `SENTRY_DSN` env variable during execution
in case you didn't do it while building the wrapper.

## More Information

* [Documentation](https://docs.sentry.io/clients/ruby)
Expand Down
8 changes: 6 additions & 2 deletions shard.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: raven
version: 0.7.0
version: 0.8.0

authors:
- Sijawusz Pur Rahnama <[email protected]>
Expand All @@ -8,6 +8,10 @@ dependencies:
any_hash:
github: sija/any_hash.cr

crystal: 0.21.1
targets:
crash_handler:
main: src/crash_handler.cr

crystal: 0.22.0

license: MIT
205 changes: 205 additions & 0 deletions src/crash_handler.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
require "./raven"

module Raven
class CrashHandler
# Example:
#
# ```
# Invalid memory access (signal 11) at address 0x20
# [0x1057a9fab] *CallStack::print_backtrace:Int32 +107
# [0x105798aac] __crystal_sigfault_handler +60
# [0x7fff9ca0652a] _sigtramp +26
# [0x105cb35a1] GC_realloc +50
# [0x1057870bb] __crystal_realloc +11
# [0x1057d3ecc] *Pointer(UInt8)@Pointer(T)#realloc<Int32>:Pointer(UInt8) +28
# [0x10578706c] __crystal_main +2940
# [0x105798128] main +40
# ```
CRYSTAL_CRASH_PATTERN = /([^\n]+)\n(\[#{Backtrace::Line::ADDR_FORMAT}\] .*)$/m

# Example:
#
# ```
# execvp: No such file or directory (Errno)
# 0x108a4ab85: *CallStack::unwind:Array(Pointer(Void)) at ??
# 0x108a4ab21: *CallStack#initialize:Array(Pointer(Void)) at ??
# 0x108a4aaf8: *CallStack::new:CallStack at ??
# 0x108a391d1: *raise<Errno>:NoReturn at ??
# 0x108a9fdf5: *Process::exec_internal<String, Array(Pointer(UInt8)), Nil, Bool, IO::FileDescriptor, IO::FileDescriptor, (IO::FileDescriptor | IO::MultiWriter), Nil>:Nil at ??
# 0x108a9f2de: *Process#initialize<String, Nil, Nil, Bool, Bool, IO::FileDescriptor, IO::FileDescriptor, IO::MultiWriter, Nil>:Nil at ??
# 0x108a9ed3b: *Process::new<String, Nil, Nil, Bool, Bool, IO::FileDescriptor, IO::FileDescriptor, IO::MultiWriter, Nil>:Process at ??
# 0x108a9ec65: *Process::run:input:output:error<String, IO::FileDescriptor, IO::FileDescriptor, IO::MultiWriter>:Process::Status at ??
# 0x108a2d948: __crystal_main at ??
# 0x108a3f6a8: main at ??
# ```
CRYSTAL_EXCEPTION_PATTERN = /([^\n]+) \(([A-Z]\w+)\)\n(#{Backtrace::Line::ADDR_FORMAT}: .*)$/m

# Default event options.
DEFAULT_OPTS = {
logger: "raven.crash_handler",
fingerprint: ["{{ default }}", "process.crash"],
}

# Process executable path.
property name : String
# An `Array` of arguments passed to process.
property args : Array(String)?

# FIXME: doesn't work yet due to usage of global Raven within `Backtrace::Line`.
#
# ```
# getter raven : Instance { Instance.new }
# ```
getter raven : Instance { Raven.instance }

delegate :context, :configuration, :configure, :capture,
to: raven

property logger : ::Logger {
Logger.new({{ flag?(:release) ? nil : "STDOUT".id }}).tap do |logger|
logger.level = {{ flag?(:debug) ? "Logger::DEBUG".id : "Logger::ERROR".id }}

"#{logger.progname}.crash_handler".tap do |progname|
logger.progname = progname
configuration.exclude_loggers << progname
end
end
}

def initialize(@name, @args)
context.extra.merge!({
process: {name: @name, args: @args},
})
end

private def configure!
configure do |config|
config.logger = logger
config.send_modules = false
config.processors = [
Processor::UTF8Conversion,
Processor::SanitizeData,
Processor::Compact,
] of Processor.class
end
end

private def capture_with_options(*args, **options)
capture(*args) do |event|
event.initialize_with **DEFAULT_OPTS
event.initialize_with **options
yield event
end
end

private def capture_with_options(*args, **options)
capture_with_options(*args, **options) { }
end

private def capture_with_options(**options)
yield
rescue e : Raven::Error
raise e # Don't capture Raven errors
rescue e : Exception
capture_with_options(e, **options) { }
raise e
end

private def capture_crystal_exception(klass, msg, backtrace)
capture_with_options klass, msg, backtrace
end

private def capture_crystal_crash(msg, backtrace)
capture_with_options msg do |event|
event.level = :fatal
event.backtrace = backtrace
# we need to overwrite the fingerprint due to varied
# pointer addresses in crash messages, otherwise resulting
# in new event per crash
event.fingerprint.tap do |fingerprint|
fingerprint.delete "{{ default }}"
fingerprint << "process: #{name}"
if culprit = event.culprit
fingerprint << "culprit: #{culprit}"
end
end
end
end

private def capture_process_failure(exit_code, output, error)
msg = "Process #{name} exited with code #{exit_code}"
cmd = (args = @args) ? "#{name} #{args.join ' '}" : name

capture_with_options msg do |event|
event.culprit = cmd
event.extra.merge!({
output: output,
error: error,
})
end
end

getter! started_at : Time
getter! process_status : Process::Status

delegate :exit_code, :success?,
to: process_status

private def run_process(output : IO = IO::Memory.new, error : IO = IO::Memory.new)
@process_status = Process.run command: name, args: args,
input: STDIN,
output: IO::MultiWriter.new(STDOUT, output),
error: IO::MultiWriter.new(STDERR, error)
{output.to_s.chomp, error.to_s.chomp}
end

def run : Void
configure!
@started_at = Time.now

capture_with_options do
output, error = run_process
running_for = Time.now - started_at

context.tags.merge!({
exit_code: exit_code,
})
context.extra.merge!({
running_for: running_for.to_s,
started_at: started_at,
})

unless success?
# TODO: pluggable detectors
case error
when CRYSTAL_CRASH_PATTERN
_, msg, backtrace = $~
capture_crystal_crash(msg, backtrace)
when CRYSTAL_EXCEPTION_PATTERN
_, msg, klass, backtrace = $~
capture_crystal_exception(klass, msg, backtrace)
else
capture_process_failure(exit_code, output, error)
end
end

exit(exit_code)
end
end
end
end

if ARGV.empty?
puts "Usage: #{PROGRAM_NAME} <CMD> [OPTION]..."
exit(1)
end

name, args = ARGV[0], ARGV.size > 1 ? ARGV[1..-1] : nil
handler = Raven::CrashHandler.new(name, args)
handler.raven.tap do |raven|
raven.configuration.src_path = Dir.current
raven.user_context({
username: `whoami`.chomp,
})
end
handler.run
8 changes: 3 additions & 5 deletions src/raven/backtrace.cr
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module Raven
class Backtrace
IGNORED_LINES_PATTERN = /CallStack|caller:|raise<(.+?)>:NoReturn/
IGNORED_LINES_PATTERN = /_sigtramp|__crystal_(sigfault_handler|raise)|CallStack|caller:|raise<(.+?)>:NoReturn/

class_getter default_filters = [
->(line : String) { line.match(IGNORED_LINES_PATTERN) ? nil : line },
Expand All @@ -9,10 +9,8 @@ module Raven
getter lines : Array(Line)

def self.parse(backtrace : Array(String), **options)
filters = default_filters
if f = options[:filters]?
filters.concat(f)
end
filters = default_filters.dup
options[:filters]?.try { |f| filters.concat(f) }

filtered_lines = backtrace.map do |line|
filters.reduce(line) do |nested_line, proc|
Expand Down
Loading

0 comments on commit e022525

Please sign in to comment.