solid_apm 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/README.md +57 -0
- data/Rakefile +8 -0
- data/app/assets/config/solid_apm_manifest.js +2 -0
- data/app/assets/javascripts/solid_apm/application.js +11 -0
- data/app/assets/javascripts/solid_apm/controllers/spans-chart_controller.js +97 -0
- data/app/assets/javascripts/solid_apm/controllers/transaction-chart_controller.js +57 -0
- data/app/assets/stylesheets/solid_apm/application.css +15 -0
- data/app/controllers/solid_apm/application_controller.rb +4 -0
- data/app/controllers/solid_apm/transactions_controller.rb +41 -0
- data/app/helpers/solid_apm/application_helper.rb +4 -0
- data/app/jobs/solid_apm/application_job.rb +4 -0
- data/app/models/solid_apm/application_record.rb +6 -0
- data/app/models/solid_apm/span.rb +9 -0
- data/app/models/solid_apm/span_subscriber/action_controller.rb +13 -0
- data/app/models/solid_apm/span_subscriber/action_view_render.rb +37 -0
- data/app/models/solid_apm/span_subscriber/active_record_sql.rb +12 -0
- data/app/models/solid_apm/span_subscriber/active_support_cache.rb +12 -0
- data/app/models/solid_apm/span_subscriber/base.rb +55 -0
- data/app/models/solid_apm/span_subscriber/net_http.rb +45 -0
- data/app/models/solid_apm/transaction.rb +8 -0
- data/app/views/javascripts/_javascripts.html.erb +0 -0
- data/app/views/layouts/solid_apm/application.html.erb +22 -0
- data/app/views/solid_apm/spans/index.html.erb +22 -0
- data/app/views/solid_apm/transactions/index.html.erb +29 -0
- data/app/views/solid_apm/transactions/show.html.erb +8 -0
- data/config/routes.rb +7 -0
- data/db/migrate/20240608015633_create_solid_apm_transactions.rb +16 -0
- data/db/migrate/20240608021940_create_solid_apm_spans.rb +19 -0
- data/lib/solid_apm/engine.rb +35 -0
- data/lib/solid_apm/middleware.rb +37 -0
- data/lib/solid_apm/version.rb +3 -0
- data/lib/solid_apm.rb +6 -0
- data/lib/tasks/solid_apm_tasks.rake +4 -0
- metadata +90 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b6159733df2a423e8080f5d8e118ead35dc63e70cd7ce41c0dd1b82c33922b07
|
4
|
+
data.tar.gz: 99752422f96489651453e355e358e8c60cf9c02ae52de2793d475a03599ce796
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2c41be36b7d999e112c283247062740eca1bbc27b150329e3300385ec40f4e983efdf2f47ac25f4c6e176b6f605a19894368d85dda8e69e826c4a02afafb745a
|
7
|
+
data.tar.gz: 9ee48dc1a610c5e2726908bbb78ed526ef1d62641d894af558ea932022c77308f11ef6ec392565f8ad96e3815ecaadf1d36acc4a43d245569c372ae198c59aa2
|
data/README.md
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
# SolidApm
|
2
|
+
Rails engine to manage APM data without using a third party service.
|
3
|
+
|
4
|
+
<img src="https://github.com/Bhacaz/solid_apm/assets/7858787/b83a4768-dbff-4c1c-8972-4b9db1092c99" width="400px">
|
5
|
+
<img src="https://github.com/Bhacaz/solid_apm/assets/7858787/87696866-1fb3-46d6-91ae-0137cc7da578" width="400px">
|
6
|
+
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
Add to your Gemfile:
|
11
|
+
|
12
|
+
```shell
|
13
|
+
bin/bundle add solid_apm
|
14
|
+
```
|
15
|
+
|
16
|
+
Mount the engine in your routes file:
|
17
|
+
```ruby
|
18
|
+
# config/routes.rb
|
19
|
+
Rails.application.routes.draw do
|
20
|
+
mount SolidApm::Engine => "/solid_apm"
|
21
|
+
end
|
22
|
+
```
|
23
|
+
|
24
|
+
Configure the database connection:
|
25
|
+
```ruby
|
26
|
+
# config/initializers/solid_apm.rb
|
27
|
+
SolidApm.connects_to = { database: { writing: :solid_apm } }
|
28
|
+
```
|
29
|
+
|
30
|
+
Install and run the migrations:
|
31
|
+
```shell
|
32
|
+
DATABASE=solid_apm bin/rails solid_apm:install:migrations
|
33
|
+
```
|
34
|
+
|
35
|
+
## Usage
|
36
|
+
|
37
|
+
Go to `http://localhost:3000/solid_apm` and start monitoring your application.
|
38
|
+
|
39
|
+
## TODOs
|
40
|
+
|
41
|
+
### Features
|
42
|
+
|
43
|
+
- [ ] Ignore `/solid_apm` requests
|
44
|
+
- [ ] Better handle subscribing to ActiveSupport notifications
|
45
|
+
- [ ] Add methods to add context to the transaction (i.e. `SolidApm.add_context(user_id: 1)`)
|
46
|
+
|
47
|
+
### Interface
|
48
|
+
|
49
|
+
- [ ] Paginate transactions list
|
50
|
+
- [ ] Allow date range transactions index
|
51
|
+
- [ ] Display transaction as aggregated data with avg latency, tpm and impact (Relative Avg. duration * transactions per minute)
|
52
|
+
|
53
|
+
## Contributing
|
54
|
+
Contribution directions go here.
|
55
|
+
|
56
|
+
## License
|
57
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
import {
|
2
|
+
Application,
|
3
|
+
Controller,
|
4
|
+
} from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.js";
|
5
|
+
window.Stimulus = Application.start();
|
6
|
+
|
7
|
+
//= require_tree .
|
8
|
+
|
9
|
+
// require "./controllers/spans-chart_controller"
|
10
|
+
// import "./controllers/spans-chart_controller.js"
|
11
|
+
// import "./controllers/transaction-chart_controller"
|
@@ -0,0 +1,97 @@
|
|
1
|
+
import {
|
2
|
+
Controller,
|
3
|
+
} from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.js";
|
4
|
+
|
5
|
+
// Connects to data-controller="spans-charts"
|
6
|
+
window.Stimulus.register('spans-chart',
|
7
|
+
class extends Controller {
|
8
|
+
static values = { id: String }
|
9
|
+
|
10
|
+
connect() {
|
11
|
+
console.log("Connected")
|
12
|
+
const options = {
|
13
|
+
series: [
|
14
|
+
{
|
15
|
+
data: []
|
16
|
+
}
|
17
|
+
],
|
18
|
+
chart: {
|
19
|
+
type: "rangeBar",
|
20
|
+
height: "250em",
|
21
|
+
},
|
22
|
+
plotOptions: {
|
23
|
+
bar: {
|
24
|
+
horizontal: true
|
25
|
+
}
|
26
|
+
},
|
27
|
+
xaxis: {
|
28
|
+
type: "datetime",
|
29
|
+
labels: {
|
30
|
+
show: false
|
31
|
+
}
|
32
|
+
},
|
33
|
+
tooltip: {
|
34
|
+
custom: function ({y1, y2,dataPointIndex, seriesIndex, w}) {
|
35
|
+
// custom: function (opts) {
|
36
|
+
// console.log(opts)
|
37
|
+
// console.log(value)
|
38
|
+
return (
|
39
|
+
'<div class="apexcharts-tooltip-title has-text-black" style="max-width: 40em; text-wrap: balance;">' +
|
40
|
+
w.globals.initialSeries[seriesIndex].data[dataPointIndex].duration + "ms" +
|
41
|
+
"<br>" +
|
42
|
+
w.globals.initialSeries[seriesIndex].data[dataPointIndex].name +
|
43
|
+
"<br>" +
|
44
|
+
w.globals.initialSeries[seriesIndex].data[dataPointIndex].summary +
|
45
|
+
'</div>'
|
46
|
+
)
|
47
|
+
}
|
48
|
+
},
|
49
|
+
yaxis: {
|
50
|
+
labels: {
|
51
|
+
formatter: function (value, opts) {
|
52
|
+
if (opts === undefined) {
|
53
|
+
return value[1] - value[0] + "ms"
|
54
|
+
}
|
55
|
+
if (opts.dataPointIndex >= 0) {
|
56
|
+
return opts.w.globals.initialSeries[opts.seriesIndex].data[opts.dataPointIndex].name
|
57
|
+
}
|
58
|
+
return value
|
59
|
+
}
|
60
|
+
}
|
61
|
+
},
|
62
|
+
}
|
63
|
+
|
64
|
+
fetch(this.idValue + "/spans.json")
|
65
|
+
.then(response => response.json())
|
66
|
+
.then(data => {
|
67
|
+
options.series[0].data = data.map(d => {
|
68
|
+
let startTime = new Date(d.timestamp).getTime()
|
69
|
+
let endTime = new Date(d.end_time).getTime()
|
70
|
+
if (endTime - startTime < 1) {
|
71
|
+
endTime = startTime + 1
|
72
|
+
}
|
73
|
+
return {
|
74
|
+
x: d.uuid,
|
75
|
+
y: [startTime, endTime],
|
76
|
+
name: d.name,
|
77
|
+
summary: d.summary || d.name,
|
78
|
+
duration: this.round(d.duration, 2)
|
79
|
+
}
|
80
|
+
})
|
81
|
+
this.chart = new ApexCharts(this.element, options)
|
82
|
+
this.chart.render()
|
83
|
+
})
|
84
|
+
}
|
85
|
+
|
86
|
+
disconnect() {
|
87
|
+
if (this.chart) {
|
88
|
+
this.chart.destroy()
|
89
|
+
this.chart = null
|
90
|
+
}
|
91
|
+
}
|
92
|
+
|
93
|
+
round(num, decimalPlaces = 0) {
|
94
|
+
num = Math.round(num + "e" + decimalPlaces);
|
95
|
+
return Number(num + "e" + -decimalPlaces);
|
96
|
+
}
|
97
|
+
})
|
@@ -0,0 +1,57 @@
|
|
1
|
+
import {
|
2
|
+
Controller,
|
3
|
+
} from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.js";
|
4
|
+
// unload chart https://github.com/Deanout/weight_tracker/blob/d4123acb952d91fcc9bedb96bbd786088a71482a/app/javascript/controllers/weights_controller.js#L4
|
5
|
+
// tooltip: {
|
6
|
+
// y: {
|
7
|
+
// formatter: function (value, {series, seriesIndex, dataPointIndex, w}) {
|
8
|
+
// return w.globals.initialSeries[seriesIndex].data[dataPointIndex].name + "\n" + value + "ms"
|
9
|
+
// }
|
10
|
+
// }
|
11
|
+
// }
|
12
|
+
|
13
|
+
// Connects to data-controller="transaction-chart"
|
14
|
+
window.Stimulus.register('transaction-chart',
|
15
|
+
class extends Controller {
|
16
|
+
connect() {
|
17
|
+
console.log('Connected')
|
18
|
+
var options = {
|
19
|
+
chart: {
|
20
|
+
type: 'bar',
|
21
|
+
height: '200em'
|
22
|
+
},
|
23
|
+
series: [{
|
24
|
+
name: 'tpm',
|
25
|
+
}],
|
26
|
+
xaxis: {
|
27
|
+
type: 'datetime'
|
28
|
+
},
|
29
|
+
tooltip: {
|
30
|
+
x: {
|
31
|
+
formatter: function (value) {
|
32
|
+
return new Date(value).toLocaleString()
|
33
|
+
}
|
34
|
+
}
|
35
|
+
}
|
36
|
+
}
|
37
|
+
fetch('transactions.json')
|
38
|
+
.then(response => response.json())
|
39
|
+
.then(data => {
|
40
|
+
const transformedData = []
|
41
|
+
for (let [key, value] of Object.entries(data)) {
|
42
|
+
transformedData.push({x: key, y: value})
|
43
|
+
}
|
44
|
+
options.series[0].data = transformedData
|
45
|
+
this.chart = new ApexCharts(this.element, options)
|
46
|
+
this.chart.render()
|
47
|
+
})
|
48
|
+
}
|
49
|
+
|
50
|
+
// Unloads the chart before loading new data.
|
51
|
+
disconnect() {
|
52
|
+
if (this.chart) {
|
53
|
+
this.chart.destroy();
|
54
|
+
this.chart = null;
|
55
|
+
}
|
56
|
+
}
|
57
|
+
})
|
@@ -0,0 +1,15 @@
|
|
1
|
+
/*
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
3
|
+
* listed below.
|
4
|
+
*
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
7
|
+
*
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
10
|
+
* files in this directory. Styles in this file should be added after the last require_* statement.
|
11
|
+
* It is generally better to create a new file per style scope.
|
12
|
+
*
|
13
|
+
*= require_tree .
|
14
|
+
*= require_self
|
15
|
+
*/
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SolidApm
|
4
|
+
class TransactionsController < ApplicationController
|
5
|
+
def index
|
6
|
+
@transactions = Transaction.all.order(timestamp: :desc).limit(10)
|
7
|
+
|
8
|
+
# uri = URI('https://dog-api.kinduff.com/api/facts')
|
9
|
+
# response = Net::HTTP.get(uri)
|
10
|
+
# @dog_fact = JSON.parse(response)
|
11
|
+
#
|
12
|
+
# Rails.cache.fetch('dog_fact', expires_in: 1.minutes) do
|
13
|
+
# 'This is a dog fact!'
|
14
|
+
# end
|
15
|
+
|
16
|
+
respond_to do |format|
|
17
|
+
format.html
|
18
|
+
format.json { render json: transactions_count_by_minutes }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def show
|
23
|
+
@transaction = Transaction.find(params[:id])
|
24
|
+
end
|
25
|
+
|
26
|
+
def spans
|
27
|
+
@transaction = Transaction.find(params[:id])
|
28
|
+
@spans = @transaction.spans
|
29
|
+
render json: @spans
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def transactions_count_by_minutes
|
35
|
+
Transaction.all.order(timestamp: :desc)
|
36
|
+
.where(created_at: 1.hour.ago..)
|
37
|
+
.group_by { |t| t.created_at.beginning_of_minute }
|
38
|
+
.transform_values!(&:count)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module SolidApm
|
2
|
+
class Span < ApplicationRecord
|
3
|
+
self.inheritance_column = :_type_disabled
|
4
|
+
belongs_to :related_transaction, class_name: 'SolidApm::Transaction', foreign_key: 'transaction_id'
|
5
|
+
# belongs_to :parent, class_name: 'Span', optional: true
|
6
|
+
|
7
|
+
attribute :uuid, :string, default: -> { SecureRandom.uuid }
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SolidApm
|
4
|
+
module SpanSubscriber
|
5
|
+
class ActionViewRender < Base
|
6
|
+
PATTERN = /^render_.+\.action_view/
|
7
|
+
|
8
|
+
def summary(payload)
|
9
|
+
identifier = payload[:identifier]
|
10
|
+
sanitize_path(identifier)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def sanitize_path(path)
|
16
|
+
if path.start_with? Rails.root.to_s
|
17
|
+
app_path(path)
|
18
|
+
else
|
19
|
+
gem_path(path)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def app_path(path)
|
24
|
+
return unless path.start_with? Rails.root.to_s
|
25
|
+
|
26
|
+
format '$APP_PATH%s', path[Rails.root.to_s.length, path.length]
|
27
|
+
end
|
28
|
+
|
29
|
+
def gem_path(path)
|
30
|
+
root = Gem.path.find { |gp| path.start_with? gp }
|
31
|
+
return unless root
|
32
|
+
|
33
|
+
format '$GEM_PATH%s', path[root.length, path.length]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module SolidApm
|
3
|
+
module SpanSubscriber
|
4
|
+
class Base
|
5
|
+
# PATTERN = /.*/
|
6
|
+
|
7
|
+
class_attribute :subscribers, default: Set.new
|
8
|
+
thread_cattr_accessor :transaction
|
9
|
+
thread_cattr_accessor :spans
|
10
|
+
|
11
|
+
def self.inherited(subclass)
|
12
|
+
subscribers << subclass
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.subscribe!
|
16
|
+
subscribers.each(&:subscribe)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.subscribe
|
20
|
+
ActiveSupport::Notifications.subscribe(self::PATTERN) do |name, start, finish, id, payload|
|
21
|
+
next unless SpanSubscriber::Base.transaction
|
22
|
+
|
23
|
+
subtype, type = name.split('.')
|
24
|
+
duration = ((finish.to_f - start.to_f) * 1000).round(6)
|
25
|
+
|
26
|
+
span = {
|
27
|
+
uuid: SecureRandom.uuid,
|
28
|
+
sequence: SpanSubscriber::Base.spans.size + 1,
|
29
|
+
timestamp: start,
|
30
|
+
end_time: finish,
|
31
|
+
duration: duration,
|
32
|
+
name: name,
|
33
|
+
type: type,
|
34
|
+
subtype: subtype,
|
35
|
+
summary: self.new.summary(payload),
|
36
|
+
}
|
37
|
+
|
38
|
+
SpanSubscriber::Base.spans << span
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# def summary(payload)
|
43
|
+
# if payload.is_a?(Hash)
|
44
|
+
# payload.first.last.inspect
|
45
|
+
# else
|
46
|
+
# payload.inspect
|
47
|
+
# end
|
48
|
+
# end
|
49
|
+
|
50
|
+
# private_class_method :subscribe
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
Dir[File.join(__dir__, '*.rb')].sort.each { |file| require file }
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'net/http'
|
4
|
+
module SolidApm
|
5
|
+
module SpanSubscriber
|
6
|
+
class NetHttp < Base
|
7
|
+
PATTERN = 'request.net_http'
|
8
|
+
|
9
|
+
def summary(payload)
|
10
|
+
payload
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.subscribe
|
14
|
+
if defined?(::Net::HTTP)
|
15
|
+
::Net::HTTP.prepend(NetHttpInstrumentationPrepend)
|
16
|
+
super
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# https://github.com/scoutapp/scout_apm_ruby/blob/3838109214503755c5cbd4caf78f6446adbe222f/lib/scout_apm/instruments/net_http.rb#L61
|
21
|
+
module NetHttpInstrumentationPrepend
|
22
|
+
def request(request, *args, &block)
|
23
|
+
ActiveSupport::Notifications.instrument PATTERN, request_solid_apm_description(request) do
|
24
|
+
super(request, *args, &block)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def request_solid_apm_description(req)
|
29
|
+
path = req.path
|
30
|
+
path = path.path if path.respond_to?(:path)
|
31
|
+
|
32
|
+
# Protect against a nil address value
|
33
|
+
if @address.nil?
|
34
|
+
return "No Address Found"
|
35
|
+
end
|
36
|
+
|
37
|
+
max_length = 500
|
38
|
+
req.method.upcase + " " + (@address + path.split('?').first)[0..(max_length - 1)]
|
39
|
+
rescue
|
40
|
+
""
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
module SolidApm
|
2
|
+
class Transaction < ApplicationRecord
|
3
|
+
self.inheritance_column = :_type_disabled
|
4
|
+
has_many :spans, -> { order(:timestamp, :sequence) }, foreign_key: 'transaction_id', dependent: :delete_all
|
5
|
+
|
6
|
+
attribute :uuid, :string, default: -> { SecureRandom.uuid }
|
7
|
+
end
|
8
|
+
end
|
File without changes
|
@@ -0,0 +1,22 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Solid apm</title>
|
5
|
+
<%= csrf_meta_tags %>
|
6
|
+
<%= csp_meta_tag %>
|
7
|
+
|
8
|
+
<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
|
9
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.1/css/bulma.min.css">
|
10
|
+
<%= stylesheet_link_tag "solid_apm/application", media: "all" %>
|
11
|
+
<%= javascript_include_tag "solid_apm/application", "data-turoblinks-track": "reload", type: "module" %>
|
12
|
+
<%= javascript_include_tag "solid_apm/controllers/transaction-chart_controller", "data-turoblinks-track": "reload", type: "module" %>
|
13
|
+
<%= javascript_include_tag "solid_apm/controllers/spans-chart_controller", "data-turoblinks-track": "reload", type: "module" %>
|
14
|
+
</head>
|
15
|
+
|
16
|
+
<body style="overflow: scroll">
|
17
|
+
<section class="section">
|
18
|
+
<%= yield %>
|
19
|
+
</section>
|
20
|
+
</body>
|
21
|
+
|
22
|
+
</html>
|
@@ -0,0 +1,22 @@
|
|
1
|
+
<div data-controller="spans-chart" data-spans-chart-id-value="<%= params[:id] %>"></div>
|
2
|
+
|
3
|
+
<table class="table">
|
4
|
+
<thead>
|
5
|
+
<tr>
|
6
|
+
<% SolidApm::Span.attribute_names.each do |attribute| %>
|
7
|
+
<% next if attribute.to_s.end_with?('_at') %>
|
8
|
+
<th scope="col"><%= attribute.humanize %></th>
|
9
|
+
<% end %>
|
10
|
+
</tr>
|
11
|
+
</thead>
|
12
|
+
<tbody>
|
13
|
+
<% spans.each do |span| %>
|
14
|
+
<tr>
|
15
|
+
<% span.attributes.each do |attribute| %>
|
16
|
+
<% next if attribute[0].to_s.end_with?('_at') %>
|
17
|
+
<td><%= attribute[1] %></td>
|
18
|
+
<% end %>
|
19
|
+
</tr>
|
20
|
+
<% end %>
|
21
|
+
</tbody>
|
22
|
+
</table>
|
@@ -0,0 +1,29 @@
|
|
1
|
+
<h1 class="title">Transactions</h1>
|
2
|
+
|
3
|
+
<h2 class="title is-4 has-text-grey">Last hour</h2>
|
4
|
+
<div data-controller="transaction-chart"></div>
|
5
|
+
|
6
|
+
<table class="table">
|
7
|
+
<thead>
|
8
|
+
<tr>
|
9
|
+
<% SolidApm::Transaction.attribute_names.each do |attribute| %>
|
10
|
+
<% next if attribute.to_s.end_with?('_at') %>
|
11
|
+
<th scope="col"><%= attribute.humanize %></th>
|
12
|
+
<% end %>
|
13
|
+
</tr>
|
14
|
+
</thead>
|
15
|
+
<tbody>
|
16
|
+
<% @transactions.each do |transaction| %>
|
17
|
+
<tr>
|
18
|
+
<% transaction.attributes.each do |attribute| %>
|
19
|
+
<% next if attribute[0].to_s.end_with?('_at') %>
|
20
|
+
<% if attribute[0] == 'uuid' %>
|
21
|
+
<td><%= link_to attribute[1], transaction %></td>
|
22
|
+
<% else %>
|
23
|
+
<td><%= attribute[1] %></td>
|
24
|
+
<% end %>
|
25
|
+
<% end %>
|
26
|
+
</tr>
|
27
|
+
<% end %>
|
28
|
+
</tbody>
|
29
|
+
</table>
|
@@ -0,0 +1,8 @@
|
|
1
|
+
|
2
|
+
<h1 class="title"><%= @transaction.name %></h1>
|
3
|
+
<h2 class="title is-6"><span class="has-text-grey-dark">Trace ID:</span> <%= @transaction.uuid %></h2>
|
4
|
+
<h2 class="title is-6"><span class="has-text-grey-dark">Timestamp:</span> <%= @transaction.timestamp %></h2>
|
5
|
+
<h2 class="title is-6"><span class="has-text-grey-dark">Duration:</span> <%= @transaction.duration %> ms</h2>
|
6
|
+
<h2 class="title is-6"><span class="has-text-grey-dark">Metadata:</span> <%= @transaction.metadata %></h2>
|
7
|
+
|
8
|
+
<%= render template: 'solid_apm/spans/index', collection: @transaction.spans, locals: { spans: @transaction.spans } %>
|
data/config/routes.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
class CreateSolidApmTransactions < ActiveRecord::Migration[7.1]
|
2
|
+
def change
|
3
|
+
create_table :solid_apm_transactions do |t|
|
4
|
+
t.string :uuid, index: { unique: true }, null: false
|
5
|
+
t.datetime :timestamp, index: { order: :desc }, null: false # start_time
|
6
|
+
t.string :type, index: true, null: false
|
7
|
+
t.string :name, index: true
|
8
|
+
t.datetime :end_time, null: false
|
9
|
+
t.float :duration # in ms
|
10
|
+
t.integer :unix_minute
|
11
|
+
t.json :metadata
|
12
|
+
|
13
|
+
t.timestamps
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class CreateSolidApmSpans < ActiveRecord::Migration[7.1]
|
2
|
+
def change
|
3
|
+
create_table :solid_apm_spans do |t|
|
4
|
+
t.string :uuid, index: { unique: true }, null: false
|
5
|
+
t.references :transaction, null: false, foreign_key: { to_table: :solid_apm_transactions }
|
6
|
+
t.integer :sequence, null: false
|
7
|
+
t.datetime :timestamp, index: { order: :desc }, null: false # start_time
|
8
|
+
t.string :name
|
9
|
+
t.string :type
|
10
|
+
t.string :subtype
|
11
|
+
t.string :summary
|
12
|
+
t.datetime :end_time
|
13
|
+
t.float :duration
|
14
|
+
t.json :stacktrace
|
15
|
+
|
16
|
+
t.timestamps
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require_relative './middleware'
|
2
|
+
|
3
|
+
module SolidApm
|
4
|
+
class Engine < ::Rails::Engine
|
5
|
+
isolate_namespace SolidApm
|
6
|
+
|
7
|
+
config.app_middleware.use Middleware
|
8
|
+
|
9
|
+
initializer "solid_apm.assets.precompile" do |app|
|
10
|
+
app.config.assets.precompile += %w( application.css application.js )
|
11
|
+
end
|
12
|
+
|
13
|
+
config.after_initialize do
|
14
|
+
ActiveSupport::Notifications.subscribe("start_processing.action_controller") do |name, start, finish, id, payload|
|
15
|
+
SpanSubscriber::Base.transaction = Transaction.new(
|
16
|
+
uuid: SecureRandom.uuid,
|
17
|
+
timestamp: start,
|
18
|
+
type: 'request',
|
19
|
+
name: "#{payload[:controller]}##{payload[:action]}",
|
20
|
+
metadata: { params: payload[:request].params.except(:controller, :action) }
|
21
|
+
)
|
22
|
+
SpanSubscriber::Base.spans = []
|
23
|
+
end
|
24
|
+
|
25
|
+
ActiveSupport::Notifications.subscribe("process_action.action_controller") do |name, start, finish, id, payload|
|
26
|
+
# Set the end time and duration of the transaction with the process_action event
|
27
|
+
transaction = SpanSubscriber::Base.transaction
|
28
|
+
transaction.end_time = finish
|
29
|
+
transaction.duration = ((transaction.end_time.to_f - transaction.timestamp.to_f) * 1000).round(6)
|
30
|
+
end
|
31
|
+
|
32
|
+
SpanSubscriber::Base.subscribe!
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SolidApm
|
4
|
+
class Middleware
|
5
|
+
def initialize(app)
|
6
|
+
@app = app
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(env)
|
10
|
+
env['rack.after_reply'] ||= []
|
11
|
+
env['rack.after_reply'] << ->() do
|
12
|
+
self.class.call
|
13
|
+
rescue StandardError => e
|
14
|
+
Rails.logger.error e
|
15
|
+
Rails.logger.error e.backtrace&.join("\n")
|
16
|
+
end
|
17
|
+
|
18
|
+
@app.call(env)
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.call
|
22
|
+
transaction = SpanSubscriber::Base.transaction
|
23
|
+
return unless transaction
|
24
|
+
|
25
|
+
SpanSubscriber::Base.transaction = nil
|
26
|
+
ApplicationRecord.transaction do
|
27
|
+
transaction.save!
|
28
|
+
|
29
|
+
SpanSubscriber::Base.spans.each do |span|
|
30
|
+
span[:transaction_id] = transaction.id
|
31
|
+
end
|
32
|
+
SolidApm::Span.insert_all SpanSubscriber::Base.spans
|
33
|
+
end
|
34
|
+
SpanSubscriber::Base.spans = nil
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/solid_apm.rb
ADDED
metadata
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: solid_apm
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jean-Francis Bastien
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-06-10 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 7.1.3.2
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 7.1.3.2
|
27
|
+
description: Description of SolidApm.
|
28
|
+
email:
|
29
|
+
- bhacaz@gmail.com
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- README.md
|
35
|
+
- Rakefile
|
36
|
+
- app/assets/config/solid_apm_manifest.js
|
37
|
+
- app/assets/javascripts/solid_apm/application.js
|
38
|
+
- app/assets/javascripts/solid_apm/controllers/spans-chart_controller.js
|
39
|
+
- app/assets/javascripts/solid_apm/controllers/transaction-chart_controller.js
|
40
|
+
- app/assets/stylesheets/solid_apm/application.css
|
41
|
+
- app/controllers/solid_apm/application_controller.rb
|
42
|
+
- app/controllers/solid_apm/transactions_controller.rb
|
43
|
+
- app/helpers/solid_apm/application_helper.rb
|
44
|
+
- app/jobs/solid_apm/application_job.rb
|
45
|
+
- app/models/solid_apm/application_record.rb
|
46
|
+
- app/models/solid_apm/span.rb
|
47
|
+
- app/models/solid_apm/span_subscriber/action_controller.rb
|
48
|
+
- app/models/solid_apm/span_subscriber/action_view_render.rb
|
49
|
+
- app/models/solid_apm/span_subscriber/active_record_sql.rb
|
50
|
+
- app/models/solid_apm/span_subscriber/active_support_cache.rb
|
51
|
+
- app/models/solid_apm/span_subscriber/base.rb
|
52
|
+
- app/models/solid_apm/span_subscriber/net_http.rb
|
53
|
+
- app/models/solid_apm/transaction.rb
|
54
|
+
- app/views/javascripts/_javascripts.html.erb
|
55
|
+
- app/views/layouts/solid_apm/application.html.erb
|
56
|
+
- app/views/solid_apm/spans/index.html.erb
|
57
|
+
- app/views/solid_apm/transactions/index.html.erb
|
58
|
+
- app/views/solid_apm/transactions/show.html.erb
|
59
|
+
- config/routes.rb
|
60
|
+
- db/migrate/20240608015633_create_solid_apm_transactions.rb
|
61
|
+
- db/migrate/20240608021940_create_solid_apm_spans.rb
|
62
|
+
- lib/solid_apm.rb
|
63
|
+
- lib/solid_apm/engine.rb
|
64
|
+
- lib/solid_apm/middleware.rb
|
65
|
+
- lib/solid_apm/version.rb
|
66
|
+
- lib/tasks/solid_apm_tasks.rake
|
67
|
+
homepage: https://github.com/Bhacaz/solid_apm
|
68
|
+
licenses: []
|
69
|
+
metadata:
|
70
|
+
homepage_uri: https://github.com/Bhacaz/solid_apm
|
71
|
+
post_install_message:
|
72
|
+
rdoc_options: []
|
73
|
+
require_paths:
|
74
|
+
- lib
|
75
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - ">="
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '0'
|
80
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
81
|
+
requirements:
|
82
|
+
- - ">="
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: '0'
|
85
|
+
requirements: []
|
86
|
+
rubygems_version: 3.5.9
|
87
|
+
signing_key:
|
88
|
+
specification_version: 4
|
89
|
+
summary: Summary of SolidApm.
|
90
|
+
test_files: []
|