periskop-client 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/periskop/client/collector.rb +71 -0
- data/lib/periskop/client/exporter.rb +25 -0
- data/lib/periskop/client/models.rb +154 -0
- data/lib/periskop/periskop.rb +3 -0
- data/lib/periskop/rack/middleware.rb +92 -0
- metadata +48 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 0d35f32e41d59a38412f7c5a424fe1586c836313c48af76ffa2aadbddd36b15b
|
4
|
+
data.tar.gz: 6a03127bfa7a1f521d2719557330c8dc9c4d2d4f11f24c8da612ce2249e16302
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1e7fdfa8becf464beba2e157dc5baf55d7a71f23f628b64e43d7f39ec830a67e497fe02bddaff931b37b56279abcbf20fd6144827286a1d1b734ce7b2f6172fb
|
7
|
+
data.tar.gz: 4c50cdae5ac0f5c619567074ef53470157b730c6005799c365a47decb99ec740beee943a10bad70fcbf322db9e7d293082b1455723802ccd374028d89e8b6ce7
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'periskop/client/models'
|
2
|
+
require 'json'
|
3
|
+
require 'securerandom'
|
4
|
+
|
5
|
+
module Periskop
|
6
|
+
module Client
|
7
|
+
# ExceptionCollector collects reported exceptions and aggregates them
|
8
|
+
class ExceptionCollector
|
9
|
+
def initialize
|
10
|
+
@aggregated_exceptions_dict = {}
|
11
|
+
@uuid = SecureRandom.uuid
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :aggregated_exceptions_dict
|
15
|
+
|
16
|
+
def aggregated_exceptions
|
17
|
+
Payload.new(@aggregated_exceptions_dict.values, @uuid)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Report an exception
|
21
|
+
# Params:
|
22
|
+
# exception:: captured exception
|
23
|
+
def report(exception)
|
24
|
+
add_exception(exception, nil)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Report an exception with context
|
28
|
+
# Params:
|
29
|
+
# exception:: captured exception
|
30
|
+
# context:: HTTP context of the exception
|
31
|
+
def report_with_context(exception, context)
|
32
|
+
add_exception(exception, context)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def add_exception(exception, context)
|
38
|
+
exception_instance = ExceptionInstance.new(
|
39
|
+
exception.class.name,
|
40
|
+
exception.message,
|
41
|
+
exception.backtrace,
|
42
|
+
get_cause(exception)
|
43
|
+
)
|
44
|
+
exception_with_context = ExceptionWithContext.new(
|
45
|
+
exception_instance,
|
46
|
+
context,
|
47
|
+
Periskop::Client::SEVERITY_ERROR
|
48
|
+
)
|
49
|
+
aggregation_key = exception_with_context.aggregation_key()
|
50
|
+
|
51
|
+
unless @aggregated_exceptions_dict.key?(aggregation_key)
|
52
|
+
aggregated_exception = AggregatedException.new(
|
53
|
+
aggregation_key,
|
54
|
+
Periskop::Client::SEVERITY_ERROR
|
55
|
+
)
|
56
|
+
@aggregated_exceptions_dict.store(aggregation_key, aggregated_exception)
|
57
|
+
end
|
58
|
+
aggregated_exception = @aggregated_exceptions_dict[aggregation_key]
|
59
|
+
aggregated_exception.add_exception(exception_with_context)
|
60
|
+
end
|
61
|
+
|
62
|
+
def get_cause(exception)
|
63
|
+
if RUBY_VERSION > '2.0'
|
64
|
+
return exception.cause
|
65
|
+
end
|
66
|
+
|
67
|
+
nil
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
module Periskop
|
5
|
+
module Client
|
6
|
+
# Exporter exposes in json format all collected exceptions from the specified `collector`
|
7
|
+
class Exporter
|
8
|
+
def initialize(collector)
|
9
|
+
@collector = collector
|
10
|
+
end
|
11
|
+
|
12
|
+
def export
|
13
|
+
@collector.aggregated_exceptions.to_json
|
14
|
+
end
|
15
|
+
|
16
|
+
def push_to_gateway(addr)
|
17
|
+
uri = URI.parse("#{addr}/errors")
|
18
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
19
|
+
request = Net::HTTP::Post.new(uri.request_uri, 'Content-Type' => 'application/json')
|
20
|
+
request.body = export
|
21
|
+
http.request(request)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
require 'time'
|
2
|
+
require 'securerandom'
|
3
|
+
require 'digest'
|
4
|
+
|
5
|
+
module Periskop
|
6
|
+
module Client
|
7
|
+
SEVERITY_INFO = "info"
|
8
|
+
SEVERITY_WARNING = "warning"
|
9
|
+
SEVERITY_ERROR = "error"
|
10
|
+
# ExceptionInstance has all metadata of a reported exception
|
11
|
+
class ExceptionInstance
|
12
|
+
attr_accessor :class, :message, :stacktrace, :cause
|
13
|
+
|
14
|
+
def initialize(cls, message, stacktrace, cause)
|
15
|
+
@class = cls
|
16
|
+
@message = message
|
17
|
+
@stacktrace = stacktrace
|
18
|
+
@cause = cause
|
19
|
+
end
|
20
|
+
|
21
|
+
def as_json(_options = {})
|
22
|
+
{
|
23
|
+
class: @class,
|
24
|
+
message: @message,
|
25
|
+
stacktrace: @stacktrace,
|
26
|
+
cause: @cause
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_json(*options)
|
31
|
+
as_json(*options).to_json(*options)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# HTTPContext represents data from HTTP context of an exception
|
36
|
+
class HTTPContext
|
37
|
+
attr_accessor :request_method
|
38
|
+
|
39
|
+
def initialize(request_method, request_url, request_headers, request_body)
|
40
|
+
@request_method = request_method
|
41
|
+
@request_url = request_url
|
42
|
+
@request_headers = request_headers
|
43
|
+
@request_body = request_body
|
44
|
+
end
|
45
|
+
|
46
|
+
def as_json(_options = {})
|
47
|
+
{
|
48
|
+
request_method: @request_method,
|
49
|
+
request_url: @request_url,
|
50
|
+
request_headers: @request_headers,
|
51
|
+
request_body: @request_body
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_json(*options)
|
56
|
+
as_json(*options).to_json(*options)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# ExceptionWithContext represents a reported exception with HTTP context
|
61
|
+
class ExceptionWithContext
|
62
|
+
attr_accessor :exception_instance, :http_context
|
63
|
+
|
64
|
+
NUM_HASH_CHARS = 8
|
65
|
+
MAX_TRACES = 5
|
66
|
+
|
67
|
+
def initialize(exception_instance, http_context, severity)
|
68
|
+
@exception_instance = exception_instance
|
69
|
+
@http_context = http_context
|
70
|
+
@severity = severity
|
71
|
+
@uuid = SecureRandom.uuid
|
72
|
+
@timestamp = Time.now.utc.iso8601
|
73
|
+
end
|
74
|
+
|
75
|
+
# Generates the aggregation key with a hash using the last MAX_TRACES
|
76
|
+
def aggregation_key
|
77
|
+
stacktrace_head = @exception_instance.stacktrace.first(MAX_TRACES).join('')
|
78
|
+
error_hash = Digest::MD5.hexdigest(stacktrace_head)[0..NUM_HASH_CHARS - 1]
|
79
|
+
"#{@exception_instance.class}@#{error_hash}"
|
80
|
+
end
|
81
|
+
|
82
|
+
def as_json(_options = {})
|
83
|
+
{
|
84
|
+
error: @exception_instance,
|
85
|
+
http_context: @http_context,
|
86
|
+
severity: @severity,
|
87
|
+
uuid: @uuid,
|
88
|
+
timestamp: @timestamp
|
89
|
+
}
|
90
|
+
end
|
91
|
+
|
92
|
+
def to_json(*options)
|
93
|
+
as_json(*options).to_json(*options)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# AggregatedException represents the aggregation of a group of exceptions
|
98
|
+
class AggregatedException
|
99
|
+
attr_accessor :latest_errors, :total_count
|
100
|
+
|
101
|
+
MAX_ERRORS = 10
|
102
|
+
|
103
|
+
def initialize(aggregation_key, severity)
|
104
|
+
@aggregation_key = aggregation_key
|
105
|
+
@latest_errors = []
|
106
|
+
@total_count = 0
|
107
|
+
@severity = severity
|
108
|
+
@created_at = Time.now.utc.iso8601
|
109
|
+
end
|
110
|
+
|
111
|
+
# Add exception to the list of latest errors up to MAX_ERRORS
|
112
|
+
def add_exception(exception_with_context)
|
113
|
+
if @latest_errors.size >= MAX_ERRORS
|
114
|
+
@latest_errors.shift
|
115
|
+
end
|
116
|
+
@latest_errors.push(exception_with_context)
|
117
|
+
@total_count += 1
|
118
|
+
end
|
119
|
+
|
120
|
+
def as_json(_options = {})
|
121
|
+
{
|
122
|
+
aggregation_key: @aggregation_key,
|
123
|
+
created_at: @created_at,
|
124
|
+
total_count: @total_count,
|
125
|
+
severity: @severity,
|
126
|
+
latest_errors: @latest_errors
|
127
|
+
}
|
128
|
+
end
|
129
|
+
|
130
|
+
def to_json(*options)
|
131
|
+
as_json(*options).to_json(*options)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Payload represents the aggregated structure of errors
|
136
|
+
class Payload
|
137
|
+
def initialize(aggregated_errors, target_uuid)
|
138
|
+
@aggregated_errors = aggregated_errors
|
139
|
+
@target_uuid = target_uuid
|
140
|
+
end
|
141
|
+
|
142
|
+
def as_json(_options = {})
|
143
|
+
{
|
144
|
+
aggregated_errors: @aggregated_errors,
|
145
|
+
target_uuid: @target_uuid
|
146
|
+
}
|
147
|
+
end
|
148
|
+
|
149
|
+
def to_json(*options)
|
150
|
+
as_json(*options).to_json(*options)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'periskop/client/collector'
|
2
|
+
require 'periskop/client/exporter'
|
3
|
+
require 'periskop/client/models'
|
4
|
+
|
5
|
+
module Periskop
|
6
|
+
module Rack
|
7
|
+
class Middleware
|
8
|
+
attr_accessor :collector
|
9
|
+
|
10
|
+
def initialize(app, options = {})
|
11
|
+
@app = app
|
12
|
+
@pushgateway_address = options.fetch(:pushgateway_address)
|
13
|
+
options[:collector] ||= Periskop::Client::ExceptionCollector.new
|
14
|
+
@collector = options.fetch(:collector)
|
15
|
+
|
16
|
+
@exporter =
|
17
|
+
if @pushgateway_address
|
18
|
+
@exporter = Periskop::Client::Exporter.new(@collector)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def call(env)
|
23
|
+
begin
|
24
|
+
response = @app.call(env)
|
25
|
+
rescue Exception => ex
|
26
|
+
report_push(env, ex)
|
27
|
+
raise(ex)
|
28
|
+
end
|
29
|
+
|
30
|
+
maybe_ex = framework_exception(env)
|
31
|
+
report_push(env, maybe_ex) if maybe_ex
|
32
|
+
|
33
|
+
response
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
# Web framework middlewares often store rescued exceptions inside the
|
39
|
+
# Rack env, but Rack doesn't have a standard key for it:
|
40
|
+
#
|
41
|
+
# - Rails uses action_dispatch.exception: https://goo.gl/Kd694n
|
42
|
+
# - Sinatra uses sinatra.error: https://goo.gl/LLkVL9
|
43
|
+
# - Goliath uses rack.exception: https://goo.gl/i7e1nA
|
44
|
+
def framework_exception(env)
|
45
|
+
env['rack.exception'] ||
|
46
|
+
env['sinatra.error'] ||
|
47
|
+
env['action_dispatch.exception']
|
48
|
+
end
|
49
|
+
|
50
|
+
def find_request(env)
|
51
|
+
if defined?(ActionDispatch::Request)
|
52
|
+
ActionDispatch::Request.new(env)
|
53
|
+
elsif defined?(Sinatra::Request)
|
54
|
+
Sinatra::Request.new(env)
|
55
|
+
else
|
56
|
+
::Rack::Request.new(env)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def get_http_headers(request_env)
|
61
|
+
header_prefixes = %w[
|
62
|
+
HTTP_
|
63
|
+
CONTENT_TYPE
|
64
|
+
CONTENT_LENGTH
|
65
|
+
].freeze
|
66
|
+
|
67
|
+
request_env.map.with_object({}) do |(key, value), headers|
|
68
|
+
if header_prefixes.any? { |prefix| key.to_s.start_with?(prefix) }
|
69
|
+
headers[key] = value
|
70
|
+
end
|
71
|
+
headers
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def get_http_context(env)
|
76
|
+
request = find_request(env)
|
77
|
+
Periskop::Client::HTTPContext.new(request.request_method, request.url, get_http_headers(request.env), nil)
|
78
|
+
end
|
79
|
+
|
80
|
+
def report_push(env, maybe_ex)
|
81
|
+
ex =
|
82
|
+
if maybe_ex.is_a?(Exception)
|
83
|
+
maybe_ex
|
84
|
+
else
|
85
|
+
RuntimeError.new(maybe_ex.to_s)
|
86
|
+
end
|
87
|
+
@collector.report_with_context(ex, get_http_context(env))
|
88
|
+
@exporter&.push_to_gateway(@pushgateway_address)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
metadata
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: periskop-client
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Julio Zynger
|
8
|
+
- Marc Tuduri
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2022-01-14 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: Periskop client for Ruby
|
15
|
+
email:
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- lib/periskop/client/collector.rb
|
21
|
+
- lib/periskop/client/exporter.rb
|
22
|
+
- lib/periskop/client/models.rb
|
23
|
+
- lib/periskop/periskop.rb
|
24
|
+
- lib/periskop/rack/middleware.rb
|
25
|
+
homepage:
|
26
|
+
licenses:
|
27
|
+
- Apache
|
28
|
+
metadata: {}
|
29
|
+
post_install_message:
|
30
|
+
rdoc_options: []
|
31
|
+
require_paths:
|
32
|
+
- lib
|
33
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
34
|
+
requirements:
|
35
|
+
- - ">="
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '0'
|
43
|
+
requirements: []
|
44
|
+
rubygems_version: 3.1.2
|
45
|
+
signing_key:
|
46
|
+
specification_version: 4
|
47
|
+
summary: Periskop client for Ruby
|
48
|
+
test_files: []
|