Skip to content

Commit

Permalink
Merge pull request #1 from Sija/develop
Browse files Browse the repository at this point in the history
v0.1
  • Loading branch information
Sija authored Feb 28, 2017
2 parents 60f5785 + c8fe045 commit ea6d148
Show file tree
Hide file tree
Showing 27 changed files with 1,691 additions and 11 deletions.
132 changes: 123 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# raven.cr
# raven.cr [![Build Status](https://travis-ci.org/Sija/raven.cr.svg?branch=master)](https://travis-ci.org/Sija/raven.cr)

TODO: Write a description here
A client and integration layer for the [Sentry](https://github.com/getsentry/sentry) error reporting API.

## Installation

Expand All @@ -18,19 +18,133 @@ dependencies:
require "raven"
```

TODO: Write usage instructions here
### Raven only runs when SENTRY_DSN is set

Raven will capture and send exceptions to the Sentry server whenever its DSN is set. This makes environment-based configuration easy - if you don't want to send errors in a certain environment, just don't set the DSN in that environment!

```bash
# Set your SENTRY_DSN environment variable.
export SENTRY_DSN=http://public:[email protected]/project-id
```

```crystal
# Or you can configure the client in the code (not recommended - keep your DSN secret!)
Raven.configure do |config|
config.server = "http://public:[email protected]/project-id"
end
```

### Raven doesn't report some kinds of data by default.

Raven ignores some exceptions by default - most of these are related to 404s or controller actions not being found. [For a complete list, see the `IGNORE_DEFAULT` constant](https://github.com/sija/raven.cr/blob/master/src/raven/configuration.cr).

Raven doesn't report POST data or cookies by default. In addition, it will attempt to remove any obviously sensitive data, such as credit card or Social Security numbers. For more information about how Sentry processes your data, [check out the documentation on the `processors` config setting.](https://docs.sentry.io/clients/ruby/config/)

### Call

Raven supports two methods of capturing exceptions:

```crystal
Raven.capture do
# capture any exceptions which happen during execution of this block
1 / 0
end
begin
1 / 0
rescue exception : DivisionByZero
Raven.capture(exception)
end
```

### More configuration

You're all set - but there's a few more settings you may want to know about too!

#### DSN

While we advise that you set your Sentry DSN through the `SENTRY_DSN` environment
variable, there are two other configuration settings for controlling Raven:

```crystal
# DSN can be configured as a config setting instead.
# Place in config/initializers or similar.
Raven.configure do |config|
config.server = "your_dsn"
end
```

And, while not necessary if using `SENTRY_DSN`, you can also provide an `environments`
setting. Raven will only capture events when `KEMAL_ENV` matches an environment in the list.

```crystal
Raven.configure do |config|
config.environments = %w[staging production]
end
```

#### transport_failure_callback

If Raven fails to send an event to Sentry for any reason (either the Sentry server has returned a 4XX or 5XX response), this Proc will be called.

```crystal
config.transport_failure_callback = ->(event : Raven::Event) {
AdminMailer.email_admins("Oh god, it's on fire!", event.to_hash).deliver_later
}
```

#### Context

Much of the usefulness of Sentry comes from additional context data with the events. Raven makes this very convenient by providing methods to set thread local context data that is then submitted automatically with all events.

There are three primary methods for providing request context:

```crystal
# bind the logged in user
Raven.user_context email: "[email protected]"
# tag the request with something interesting
Raven.tags_context interesting: "yes"
# provide a bit of additional context
Raven.extra_context happiness: "very"
```

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

## TODO

- [x] Configuration
- [x] Connection to Sentry server
- [ ] Exponential backoff in case of connection error
- [x] Interfaces
- [x] Connection transports
- [x] Processors
- [x] Breadcrumbs
- [ ] Integrations (Kemal, Sidekiq)
- [ ] Async

## Development

TODO: Write development instructions here
```
crystal spec
```

## More Information

* [Documentation](https://docs.sentry.io/clients/ruby)
* [Bug Tracker](https://github.com/sija/raven.cr/issues)
* [Code](https://github.com/sija/raven.cr)
* [Mailing List](https://groups.google.com/group/getsentry)
* [IRC](irc://irc.freenode.net/sentry) (irc.freenode.net, #sentry)

## Contributing

1. Fork it ( https://github.com/sija/raven.cr/fork )
2. Create your feature branch (git checkout -b my-new-feature)
3. Commit your changes (git commit -am 'Add some feature')
4. Push to the branch (git push origin my-new-feature)
5. Create a new Pull Request
1. [Fork it](https://github.com/sija/raven.cr/fork)
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create a new [Pull Request](https://github.com/sija/raven.cr/pulls)

## Contributors

Expand Down
7 changes: 6 additions & 1 deletion shard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ version: 0.1.0
authors:
- Sijawusz Pur Rahnama <[email protected]>

crystal: 0.20.5
dependencies:
any_hash:
github: sija/any_hash.cr
branch: master

crystal: 0.21.0

license: MIT
29 changes: 28 additions & 1 deletion src/raven.cr
Original file line number Diff line number Diff line change
@@ -1,5 +1,32 @@
require "any_hash"
require "./raven/*"

class Exception
any_json_property __raven_context
end

module Raven
# TODO Put your code here
module Delegators
delegate :context, :logger, :configuration, :client,
:report_status, :configure, :send_event, :capture,
:last_event_id, :annotate_exception, :user_context,
:tags_context, :extra_context, :breadcrumbs, to: :instance
end
end

module Raven
extend Delegators
class_getter instance : Raven::Instance { Raven::Instance.new }

def self.sys_command(command)
result = `#{command} 2>&1`.strip rescue nil
return if result.nil? || result.empty? || !$?.success?
result
end

macro sys_command_compiled(command)
%result = {{ system("#{command.id} || true").stringify.strip }}
return if %result.empty?
%result
end
end
48 changes: 48 additions & 0 deletions src/raven/backtrace.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
module Raven
class Backtrace
IGNORED_LINES_PATTERN = /CallStack|caller:|raise<(.+?)>:NoReturn/

class_getter default_filters = [
->(line : String) { line.match(IGNORED_LINES_PATTERN) ? nil : line },
] of String -> String?

getter lines : Array(Line)

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

filtered_lines = backtrace.map do |line|
filters.reduce(line) do |nested_line, proc|
proc.call(nested_line) || break
end
end.compact

lines = filtered_lines.map do |unparsed_line|
Line.parse(unparsed_line)
end
new(lines)
end

def self.parse(backtrace : String, **options)
parse(backtrace.lines, **options)
end

def initialize(@lines)
end

def_equals @lines

def to_s(io)
@lines.join '\n', io
end

def inspect(io)
io << "<Backtrace: "
@lines.join(", ", io, &.inspect(io))
io << ">"
end
end
end
82 changes: 82 additions & 0 deletions src/raven/backtrace_line.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
module Raven
# Handles backtrace parsing line by line
struct Backtrace::Line
# Examples:
#
# - `0x103a7bbee: __crystal_main at ??`
# - `0x100e1ea72: *CallStack::unwind:Array(Pointer(Void)) at ??`
# - `0x102dff5e7: *Foo::Bar#_baz:Foo::Bam at /home/fooBAR/code/awesome-shard.cr/lib/foo/src/foo/bar.cr 50:7`
# - `0x102de9035: *Foo::Bar::bar_by_id<String>:Foo::Bam at /home/fooBAR/code/awesome-shard.cr/lib/foo/src/foo/bar.cr 29:9`
# - `0x102cfe8f4: *Fiber#run:(IO::FileDescriptor | Nil) at /usr/local/Cellar/crystal-lang/0.20.5_2/src/fiber.cr 114:3`
CRYSTAL_METHOD_FORMAT = /\*?(?<method>.*?) at (?<file>[^:]+)(?:\s(?<line>\d+)(?:\:(?<col>\d+))?)?$/

# Examples:
#
# - `0x102cee376: ~procProc(Nil)@/usr/local/Cellar/crystal-lang/0.20.5_2/src/http/server.cr:148 at ??`
# - `0x102ce57db: ~procProc(HTTP::Server::Context, String)@lib/kemal/src/kemal/route.cr:11 at ??`
# - `0x1002d5180: ~procProc(HTTP::Server::Context, (File::PReader | HTTP::ChunkedContent | HTTP::Server::Response | HTTP::Server::Response::Output | HTTP::UnknownLengthContent | HTTP::WebSocket::Protocol::StreamIO | IO::ARGF | IO::Delimited | IO::FileDescriptor | IO::Hexdump | IO::Memory | IO::MultiWriter | IO::Sized | Int32 | OpenSSL::SSL::Socket | String::Builder | Zip::ChecksumReader | Zip::ChecksumWriter | Zlib::Deflate | Zlib::Inflate | Nil))@src/foo/bar/baz.cr:420 at ??`
CRYSTAL_PROC_FORMAT = /\~(?<proc_method>[^@]+)@(?<proc_file>[^:]+)(?:\:(?<proc_line>\d+)) at \?+$/

# See `CRYSTAL_PROC_FORMAT` and `CRYSTAL_METHOD_FORMAT`.
#
# Examples:
#
# - `0x103a7bbee: __crystal_main at ??`
# - `0x102cfe8f4: *Fiber#run:(IO::FileDescriptor | Nil) at /usr/local/Cellar/crystal-lang/0.20.5_2/src/fiber.cr 114:3`
# - `0x102cee376: ~procProc(Nil)@/usr/local/Cellar/crystal-lang/0.20.5_2/src/http/server.cr:148 at ??`
CRYSTAL_INPUT_FORMAT = /^(?<addr>0x[a-z0-9]+): #{CRYSTAL_PROC_FORMAT + CRYSTAL_METHOD_FORMAT}/

# The file portion of the line (such as `app/models/user.cr`).
getter file : String?

# The line number portion of the line.
getter number : Int32?

# The column number portion of the line.
getter column : Int32?

# The method of the line (such as index).
getter method : String?

private def self.empty_marker?(value)
value =~ /^\?+$/
end

# Parses a single line of a given backtrace, where *unparsed_line* is
# the raw line from `caller` or some backtrace.
# Returns the parsed backtrace line.
def self.parse(unparsed_line : String) : Line
if match = unparsed_line.match(CRYSTAL_INPUT_FORMAT)
file = match["proc_file"]? || match["file"]?
file = nil if empty_marker?(file)
number = match["proc_line"]? || match["line"]?
column = match["col"]?
method = match["proc_method"]? || match["method"]?
end
# pp match
new(file, number.try(&.to_i), column.try(&.to_i), method)
end

def initialize(@file, @number, @column, @method)
end

def_equals_and_hash @file, @number, @column, @method

# Reconstructs the line in a readable fashion
def to_s(io)
io << '`' << method << '`' if method
if file
io << " at " << file
io << ':' << number if number
end
end

def inspect(io)
io << "<Line: " << self << ">"
end

def in_app?
!!(file =~ Raven.configuration.in_app_pattern)
end
end
end
68 changes: 68 additions & 0 deletions src/raven/breadcrumb.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
module Raven
class Breadcrumb
# The type of breadcrumb. The default type is `Type::DEFAULT` which indicates
# no specific handling. Other types are currently:
# - `Type::HTTP` for HTTP requests and
# - `Type::NAVIGATION` for navigation events.
enum Type
DEFAULT
HTTP
NAVIGATION
end

# Levels are used in the UI to emphasize and deemphasize the crumb.
enum Severity
DEBUG
INFO
WARNING
ERROR
CRITICAL
end

# A timestamp representing when the breadcrumb occurred.
property timestamp : Time

# The type of breadcrumb. The default type is `default` which indicates
# no specific handling. Other types are currently:
# - `http` for HTTP requests and
# - `navigation` for navigation events.
property type : Type?

# If a message is provided it’s rendered as text and the whitespace is preserved.
# Very long text might be abbreviated in the UI.
property message : String?

# Categories are dotted strings that indicate what the crumb is or where it comes from.
# Typically it’s a module name or a descriptive string.
# For instance `ui.click` could be used to indicate that a click happened
# in the UI or `flask` could be used to indicate that the event originated
# in the Flask framework.
property category : String?

# This defines the level of the event. If not provided it defaults
# to `info` which is the middle level.
property level : Severity?

# Data associated with this breadcrumb. Contains a sub-object whose
# contents depend on the breadcrumb `type`. Additional parameters that
# are unsupported by the type are rendered as a key/value table.
any_json_property :data

def initialize
@timestamp = Time.now
end

def to_hash
{
"timestamp" => @timestamp.to_utc.epoch,
"type" => @type.try(&.to_s.downcase),
"message" => @message,
"data" => data.to_h,
"category" => @category,
"level" => @level.try(&.to_s.downcase),
}
end
end
end

require "./breadcrumbs/*"
Loading

0 comments on commit ea6d148

Please sign in to comment.