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.
Files changed (35) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +57 -0
  3. data/Rakefile +8 -0
  4. data/app/assets/config/solid_apm_manifest.js +2 -0
  5. data/app/assets/javascripts/solid_apm/application.js +11 -0
  6. data/app/assets/javascripts/solid_apm/controllers/spans-chart_controller.js +97 -0
  7. data/app/assets/javascripts/solid_apm/controllers/transaction-chart_controller.js +57 -0
  8. data/app/assets/stylesheets/solid_apm/application.css +15 -0
  9. data/app/controllers/solid_apm/application_controller.rb +4 -0
  10. data/app/controllers/solid_apm/transactions_controller.rb +41 -0
  11. data/app/helpers/solid_apm/application_helper.rb +4 -0
  12. data/app/jobs/solid_apm/application_job.rb +4 -0
  13. data/app/models/solid_apm/application_record.rb +6 -0
  14. data/app/models/solid_apm/span.rb +9 -0
  15. data/app/models/solid_apm/span_subscriber/action_controller.rb +13 -0
  16. data/app/models/solid_apm/span_subscriber/action_view_render.rb +37 -0
  17. data/app/models/solid_apm/span_subscriber/active_record_sql.rb +12 -0
  18. data/app/models/solid_apm/span_subscriber/active_support_cache.rb +12 -0
  19. data/app/models/solid_apm/span_subscriber/base.rb +55 -0
  20. data/app/models/solid_apm/span_subscriber/net_http.rb +45 -0
  21. data/app/models/solid_apm/transaction.rb +8 -0
  22. data/app/views/javascripts/_javascripts.html.erb +0 -0
  23. data/app/views/layouts/solid_apm/application.html.erb +22 -0
  24. data/app/views/solid_apm/spans/index.html.erb +22 -0
  25. data/app/views/solid_apm/transactions/index.html.erb +29 -0
  26. data/app/views/solid_apm/transactions/show.html.erb +8 -0
  27. data/config/routes.rb +7 -0
  28. data/db/migrate/20240608015633_create_solid_apm_transactions.rb +16 -0
  29. data/db/migrate/20240608021940_create_solid_apm_spans.rb +19 -0
  30. data/lib/solid_apm/engine.rb +35 -0
  31. data/lib/solid_apm/middleware.rb +37 -0
  32. data/lib/solid_apm/version.rb +3 -0
  33. data/lib/solid_apm.rb +6 -0
  34. data/lib/tasks/solid_apm_tasks.rake +4 -0
  35. 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,8 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ load "rails/tasks/statistics.rake"
7
+
8
+ require "bundler/gem_tasks"
@@ -0,0 +1,2 @@
1
+ //= link_directory ../stylesheets/solid_apm .css
2
+ //= link_tree ../javascripts/solid_apm .js
@@ -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,4 @@
1
+ module SolidApm
2
+ class ApplicationController < ActionController::Base
3
+ end
4
+ end
@@ -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,4 @@
1
+ module SolidApm
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module SolidApm
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module SolidApm
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ self.connects_to **SolidApm.connects_to
5
+ end
6
+ 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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidApm
4
+ module SpanSubscriber
5
+ class ActionController < Base
6
+ PATTERN = 'process_action.action_controller'
7
+
8
+ def summary(payload)
9
+ "#{payload[:controller]}##{payload[:action]}"
10
+ end
11
+ end
12
+ end
13
+ 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,12 @@
1
+ # frozen_string_literal: true
2
+ module SolidApm
3
+ module SpanSubscriber
4
+ class ActiveRecordSql < Base
5
+ PATTERN = "sql.active_record"
6
+
7
+ def summary(payload)
8
+ payload[:sql]
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+ module SolidApm
3
+ module SpanSubscriber
4
+ class ActiveSupportCache < Base
5
+ PATTERN = /^cache_.+.active_support/
6
+
7
+ def summary(payload)
8
+ payload[:key]
9
+ end
10
+ end
11
+ end
12
+ 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,7 @@
1
+ SolidApm::Engine.routes.draw do
2
+ root 'transactions#index'
3
+
4
+ get 'transactions', to: 'transactions#index'
5
+ get 'transactions/:id', to: 'transactions#show', as: 'transaction'
6
+ get 'transactions/:id/spans', to: 'transactions#spans', as: 'transaction_spans'
7
+ end
@@ -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
@@ -0,0 +1,3 @@
1
+ module SolidApm
2
+ VERSION = "0.1.0"
3
+ end
data/lib/solid_apm.rb ADDED
@@ -0,0 +1,6 @@
1
+ require "solid_apm/version"
2
+ require "solid_apm/engine"
3
+
4
+ module SolidApm
5
+ mattr_accessor :connects_to
6
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :solid_apm do
3
+ # # Task goes here
4
+ # end
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: []