solid_apm 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []