Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement periskop-ruby client #1

Merged
merged 14 commits into from
Jul 26, 2021
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.gem
julioz marked this conversation as resolved.
Show resolved Hide resolved
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,31 @@
# periskop-ruby
Ruby client for Periskop

### Contributing

With the gemspec file in the root directory of the repository, we can locally build a gem from its source code to test it out.

```
$ gem build periskop-client.gemspec
Successfully built RubyGem
Name: periskop-client
Version: 0.0.1
File: periskop-client-0.0.1.gem

$ gem install periskop-client-0.0.1.gem
Successfully installed periskop-client-0.0.1
...
1 gem installed
```

The final step is to require the gem and use it:
```
$ irb
>> require 'periskop-client'
=> true
```

### Test

1. `gem install rspec`
2. `rspec`
62 changes: 62 additions & 0 deletions lib/periskop/client/collector.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
require 'periskop/client/models'
require 'json'
require 'securerandom'

module Periskop
module Client
class ExceptionCollector
def initialize
@aggregated_exceptions_dict = {}
@uuid = SecureRandom.uuid
end

attr_reader :aggregated_exceptions_dict

def aggregated_exceptions
Payload.new(@aggregated_exceptions_dict.values, @uuid)
end

# Report an exception
# Params:
# exception:: captured exception
def report(exception)
add_exception(exception, nil)
end

# Report an exception with context
# Params:
# exception:: captured exception
# context:: HTTP context of the exception
def report_with_context(exception, context)
add_exception(exception, context)
end

private

def add_exception(exception, context)
exception_instance = ExceptionInstance.new(
exception.class.name,
exception.message,
exception.backtrace,
exception.cause
)
exception_with_context = ExceptionWithContext.new(
exception_instance,
context,
'error'
)
aggregation_key = exception_with_context.aggregation_key()

unless @aggregated_exceptions_dict.key?(aggregation_key)
aggregated_exception = AggregatedException.new(
aggregation_key,
'error'
)
@aggregated_exceptions_dict.store(aggregation_key, aggregated_exception)
end
aggregated_exception = @aggregated_exceptions_dict[aggregation_key]
aggregated_exception.add_exception(exception_with_context)
end
end
end
end
13 changes: 13 additions & 0 deletions lib/periskop/client/exporter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module Periskop
module Client
class Exporter
def initialize(collector)
@collector = collector
end

def export
@collector.aggregated_exceptions.to_json
end
end
end
end
124 changes: 124 additions & 0 deletions lib/periskop/client/models.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
require 'time'
require 'securerandom'
require 'digest'

module Periskop
module Client
class ExceptionInstance
attr_accessor :class, :message, :stacktrace, :cause

def initialize(cls, message, stacktrace, cause)
@class = cls
@message = message
@stacktrace = stacktrace
@cause = cause
end

def as_json(_options = {})
{
class: @class,
message: @message,
stacktrace: @stacktrace,
cause: @cause
}
end

def to_json(*options)
as_json(*options).to_json(*options)
end
end

class HTTPContext
attr_accessor :request_method

def initialize(request_method, request_url, request_headers, request_body)
@request_method = request_method
@request_url = request_url
@request_headers = request_headers
@request_body = request_body
end
end

class ExceptionWithContext
attr_accessor :exception_instance, :http_context

NUM_HASH_CHARS = 8
MAX_TRACES = 5

def initialize(exception_instance, http_context, severity)
@exception_instance = exception_instance
@http_context = http_context
@severity = severity
@uuid = SecureRandom.uuid
@timestamp = Time.now.utc.iso8601
end

def aggregation_key
stacktrace_head = @exception_instance.stacktrace.first(MAX_TRACES).join('')
error_hash = Digest::MD5.hexdigest(stacktrace_head)[0..NUM_HASH_CHARS - 1]
"#{@exception_instance.class}@#{error_hash}"
end

def as_json(_options = {})
{
error: @exception_instance,
http_context: @http_context,
severity: @severity,
uuid: @uuid,
timestamp: @timestamp
}
end

def to_json(*options)
as_json(*options).to_json(*options)
end
end

class AggregatedException
attr_accessor :latest_errors

def initialize(aggregation_key, severity)
@aggregation_key = aggregation_key
@latest_errors = []
@total_count = 0
@severity = severity
end

def add_exception(exception_with_context)
@latest_errors.push(exception_with_context)
marctc marked this conversation as resolved.
Show resolved Hide resolved
@total_count += 1
end

def as_json(_options = {})
{
aggregation_key: @aggregation_key,
total_count: @total_count,
severity: @severity,
latest_errors: @latest_errors
}
end

def to_json(*options)
as_json(*options).to_json(*options)
end
end

class Payload
def initialize(aggregated_errors, target_uuid)
@aggregated_errors = aggregated_errors
@target_uuid = target_uuid
end

def as_json(_options = {})
{
aggregated_errors: @aggregated_errors,
target_uuid: @target_uuid
}
end

def to_json(*options)
as_json(*options).to_json(*options)
end
end
end
end
3 changes: 3 additions & 0 deletions lib/periskop/periskop.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Periskop module
module Periskop
end
10 changes: 10 additions & 0 deletions periskop-client.gemspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Gem::Specification.new do |s|
s.name = 'periskop-client'
s.version = '0.0.1'
s.summary = "Periskop cleint for Ruby"
#s.description = "A simple hello world gem"
s.authors = ["Julio Zynger", "Marc Tuduri"]
#s.email = '[email protected]'
s.files = Dir.glob('{lib/**/*}')
s.license = 'MIT'
end
39 changes: 39 additions & 0 deletions spec/periskop/client/collector_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
require 'periskop/client/collector'
require 'periskop/client/models'

describe Periskop::Client::ExceptionCollector do
let(:collector) { Periskop::Client::ExceptionCollector.new }

describe '#report' do
before do
raise Exception
rescue Exception => e
collector.report(e)
end

it 'adds the exception to hash of exceptions' do
expect(collector.aggregated_exceptions_dict.size).to eq(1)
end

it 'has the valid exception name' do
expect(collector.aggregated_exceptions_dict.values[0].latest_errors[0].exception_instance.class).to eq('Exception')
end
end

describe '#report_with_context' do
before do
raise Exception
rescue Exception => e
http_context = Periskop::Client::HTTPContext.new('GET', 'http://example.com', nil, '{}')
collector.report_with_context(e, http_context)
end

it 'adds the exception to hash of exceptions' do
expect(collector.aggregated_exceptions_dict.size).to eq(1)
end

it 'has a context GET method' do
expect(collector.aggregated_exceptions_dict.values[0].latest_errors[0].http_context.request_method).to eq('GET')
end
end
end
26 changes: 26 additions & 0 deletions spec/periskop/client/models_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
require 'periskop/client/models'

describe Periskop::Client::ExceptionWithContext do
def build_exception_with_context(stacktrace)
exception_instance = Periskop::Client::ExceptionInstance.new(
'Exception',
'test',
[stacktrace],
nil
)
Periskop::Client::ExceptionWithContext.new(
exception_instance,
nil,
'error'
)
end
describe '#aggregation_key' do
it 'generates an aggregation key for an exception instance' do
expect(build_exception_with_context('test').aggregation_key).to eq('Exception@098f6bcd')
end

it 'generates different aggregation key for an exception instance without same stack trace' do
expect(build_exception_with_context('other').aggregation_key).to_not eq('Exception@098f6bcd')
end
end
end