solid_apm 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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: []
|