ruby_llm-monitoring 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 (48) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +179 -0
  3. data/Rakefile +8 -0
  4. data/app/assets/stylesheets/ruby_llm/monitoring/application.css +15 -0
  5. data/app/assets/stylesheets/ruby_llm/monitoring/bulma.min.css +3 -0
  6. data/app/controllers/ruby_llm/monitoring/alerts_controller.rb +7 -0
  7. data/app/controllers/ruby_llm/monitoring/application_controller.rb +12 -0
  8. data/app/controllers/ruby_llm/monitoring/metrics_controller.rb +91 -0
  9. data/app/helpers/ruby_llm/monitoring/alerts_helper.rb +4 -0
  10. data/app/helpers/ruby_llm/monitoring/application_helper.rb +6 -0
  11. data/app/helpers/ruby_llm/monitoring/metrics_helper.rb +7 -0
  12. data/app/javascript/ruby_llm/monitoring/application.js +5 -0
  13. data/app/javascript/ruby_llm/monitoring/controllers/application.js +11 -0
  14. data/app/javascript/ruby_llm/monitoring/controllers/chart_controller.js +93 -0
  15. data/app/javascript/ruby_llm/monitoring/controllers/index.js +4 -0
  16. data/app/jobs/ruby_llm/monitoring/application_job.rb +6 -0
  17. data/app/mailers/ruby_llm/monitoring/alert_mailer.rb +13 -0
  18. data/app/mailers/ruby_llm/monitoring/application_mailer.rb +8 -0
  19. data/app/models/concerns/ruby_llm/monitoring/alertable.rb +53 -0
  20. data/app/models/ruby_llm/monitoring/application_record.rb +7 -0
  21. data/app/models/ruby_llm/monitoring/event.rb +22 -0
  22. data/app/views/layouts/ruby_llm/monitoring/application.html.erb +29 -0
  23. data/app/views/layouts/ruby_llm/monitoring/mailer.html.erb +13 -0
  24. data/app/views/layouts/ruby_llm/monitoring/mailer.text.erb +1 -0
  25. data/app/views/ruby_llm/monitoring/alert_mailer/alert_notification.html.erb +1 -0
  26. data/app/views/ruby_llm/monitoring/alert_mailer/alert_notification.text.erb +1 -0
  27. data/app/views/ruby_llm/monitoring/alerts/_alert.html.erb +2 -0
  28. data/app/views/ruby_llm/monitoring/alerts/index.html.erb +26 -0
  29. data/app/views/ruby_llm/monitoring/application/_flashes.html.erb +9 -0
  30. data/app/views/ruby_llm/monitoring/application/_nav.html.erb +12 -0
  31. data/app/views/ruby_llm/monitoring/application/_pagination.html.erb +7 -0
  32. data/app/views/ruby_llm/monitoring/application/_tabs.html.erb +6 -0
  33. data/app/views/ruby_llm/monitoring/metrics/_filters.html.erb +25 -0
  34. data/app/views/ruby_llm/monitoring/metrics/_totals.html.erb +51 -0
  35. data/app/views/ruby_llm/monitoring/metrics/index.html.erb +16 -0
  36. data/config/importmap.rb +8 -0
  37. data/config/routes.rb +6 -0
  38. data/db/migrate/20251208171258_create_ruby_llm_monitoring_events.rb +70 -0
  39. data/lib/ruby_llm/monitoring/channel_registry.rb +19 -0
  40. data/lib/ruby_llm/monitoring/channels/base.rb +11 -0
  41. data/lib/ruby_llm/monitoring/channels/email.rb +18 -0
  42. data/lib/ruby_llm/monitoring/channels/slack.rb +18 -0
  43. data/lib/ruby_llm/monitoring/engine.rb +61 -0
  44. data/lib/ruby_llm/monitoring/event_subscriber.rb +20 -0
  45. data/lib/ruby_llm/monitoring/version.rb +5 -0
  46. data/lib/ruby_llm/monitoring.rb +24 -0
  47. data/lib/tasks/ruby_llm/monitoring_tasks.rake +4 -0
  48. metadata +185 -0
@@ -0,0 +1,7 @@
1
+ module RubyLLM::Monitoring
2
+ class AlertsController < ApplicationController
3
+ def index
4
+ @alert_rules = RubyLLM::Monitoring.alert_rules
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,12 @@
1
+ module RubyLLM
2
+ module Monitoring
3
+ class ApplicationController < ActionController::Base
4
+ private
5
+
6
+ def filter_param
7
+ {}
8
+ end
9
+ helper_method :filter_param
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,91 @@
1
+ module RubyLLM::Monitoring
2
+ class MetricsController < ApplicationController
3
+ before_action :set_resolution
4
+ before_action :set_time_range
5
+
6
+ def index
7
+ @events = Event.group(:provider, :model)
8
+ .group_by_minute(:created_at, range: @time_range, n: @resolution.in_minutes.to_i)
9
+
10
+ error_count_by_time = @events.where.not(exception_class: nil).size
11
+
12
+ @metrics = [
13
+ build_metric_series(title: "Throughput", data: aggregate_metric(@events.count)),
14
+ build_metric_series(title: "Cost", data: aggregate_metric(@events.sum(:cost)), unit: "money"),
15
+ build_metric_series(
16
+ title: "Response time",
17
+ data: aggregate_metric(@events.average(:duration), default_value: 0),
18
+ unit: "ms"
19
+ ),
20
+ build_metric_series(
21
+ title: "Errors",
22
+ data: aggregate_metric(error_count_by_time, default_value: 0),
23
+ unit: "number"
24
+ )
25
+ ]
26
+
27
+ @totals_by_provider = Event.where(created_at: @time_range)
28
+ .group(:provider, :model)
29
+ .select(
30
+ :provider,
31
+ :model,
32
+ "COUNT(*) as requests",
33
+ "SUM(cost) as cost",
34
+ "AVG(duration) as avg_response_time"
35
+ ).to_a
36
+
37
+ total_requests = @totals_by_provider.sum(&:requests)
38
+ error_count = error_count_by_time.values.sum
39
+
40
+ @totals = {
41
+ requests: total_requests,
42
+ cost: @totals_by_provider.sum { |r| r.cost.to_f },
43
+ avg_response_time: @totals_by_provider.any? ? @totals_by_provider.sum do |r|
44
+ r.avg_response_time.to_f * r.requests
45
+ end / total_requests : nil,
46
+ error_rate: total_requests.positive? ? (error_count.to_f / total_requests * 100).round(1) : 0
47
+ }
48
+ end
49
+
50
+ private
51
+
52
+ def aggregate_metric(aggregated_data, default_value: nil)
53
+ aggregated_data
54
+ .group_by { |(provider, model, _), _| [ provider, model ] }
55
+ .transform_values do |entries|
56
+ entries.map do |(_, _, timestamp), value|
57
+ [ timestamp.to_i * 1000, value || default_value ]
58
+ end
59
+ end
60
+ end
61
+
62
+ def build_metric_series(title:, data:, unit: nil)
63
+ {
64
+ title: title,
65
+ unit: unit,
66
+ series: data.map { |k, v| { name: k.join("/"), data: v } }
67
+ }.compact
68
+ end
69
+
70
+ def filter_param
71
+ {
72
+ filter: {
73
+ created_at_start: @created_at_start,
74
+ created_at_end: @created_at_end,
75
+ resolution: @resolution
76
+ }
77
+ }.compact
78
+ end
79
+
80
+ def set_resolution
81
+ @resolution = params.dig(:filter, :resolution).try(:to_i).try(:minutes) || 1.minute
82
+ end
83
+
84
+ def set_time_range
85
+ @created_at_start = params.dig(:filter, :created_at_start).try(:in_time_zone) || 2.hours.ago
86
+ @created_at_end = params.dig(:filter, :created_at_end).try(:in_time_zone)
87
+
88
+ @time_range = @created_at_start..@created_at_end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,4 @@
1
+ module RubyLLM::Monitoring
2
+ module AlertsHelper
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module RubyLLM
2
+ module Monitoring
3
+ module ApplicationHelper
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,7 @@
1
+ module RubyLLM::Monitoring
2
+ module MetricsHelper
3
+ def resolution_options
4
+ { "1 minute" => 1, "10 minutes" => 10, "1 hour" => 60 }
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ import "@hotwired/turbo-rails"
2
+ import "controllers"
3
+
4
+ import ApexCharts from "apexcharts"
5
+ window.ApexCharts = ApexCharts
@@ -0,0 +1,11 @@
1
+ import { Application } from "@hotwired/stimulus"
2
+ import AutoSubmit from "@stimulus-components/auto-submit"
3
+
4
+ const application = Application.start()
5
+ application.register("auto-submit", AutoSubmit)
6
+
7
+ // Configure Stimulus development experience
8
+ application.debug = false
9
+ window.Stimulus = application
10
+
11
+ export { application }
@@ -0,0 +1,93 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ initialize() {
5
+ this.title = this.element.dataset.title
6
+ this.min = parseInt(this.element.dataset.min)
7
+ this.series = JSON.parse(this.element.dataset.series)
8
+ this.formatter = this.element.dataset.formatter
9
+ }
10
+
11
+ connect() {
12
+ this.chart = new ApexCharts(this.element, {
13
+ chart: {
14
+ id: this.title,
15
+ group: "metrics",
16
+ height: "200px",
17
+ type: "area",
18
+ toolbar: {
19
+ tools: {
20
+ download: false
21
+ }
22
+ },
23
+ events: {
24
+ rendered: () => this.syncAll()
25
+ }
26
+ },
27
+ colors: ["#3b82f6", "#4ade80", "#facc15", "#dc2626", "#7c3aed"],
28
+ dataLabels: {
29
+ enabled: false
30
+ },
31
+ onDatasetHover: {
32
+ highlightDataSeries: true,
33
+ },
34
+ series: this.series,
35
+ stroke: {
36
+ width: 1
37
+ },
38
+ title: {
39
+ text: this.title
40
+ },
41
+ tooltip: {
42
+ shared: true,
43
+ x: {
44
+ format: "dd/MM/yyyy HH:mm:ss"
45
+ },
46
+ y: {
47
+ formatter: this.formatterFunction()
48
+ }
49
+ },
50
+ xaxis: {
51
+ type: "datetime",
52
+ min: this.min
53
+ },
54
+ yaxis: {
55
+ labels: { show: false }
56
+ }
57
+ })
58
+
59
+ this.chart.render()
60
+ }
61
+
62
+ syncAll() {
63
+ if (typeof ApexCharts !== "undefined") {
64
+ ApexCharts.exec("metrics", "syncAll")
65
+ }
66
+ }
67
+
68
+ formatterFunction() {
69
+ let f = function(value) {
70
+ return value
71
+ }
72
+
73
+ if (this.formatter === "percentage") {
74
+ f = function(value) {
75
+ return `${value || 0}%`
76
+ }
77
+ }
78
+
79
+ if (this.formatter === "ms") {
80
+ f = function(value) {
81
+ return `${value.toFixed(2)} ms`
82
+ }
83
+ }
84
+
85
+ if (this.formatter === "money") {
86
+ f = function(value) {
87
+ return `$${value}`
88
+ }
89
+ }
90
+
91
+ return f
92
+ }
93
+ }
@@ -0,0 +1,4 @@
1
+ import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
2
+ import { application } from "controllers/application"
3
+
4
+ eagerLoadControllersFrom("controllers", application)
@@ -0,0 +1,6 @@
1
+ module RubyLLM
2
+ module Monitoring
3
+ class ApplicationJob < ActiveJob::Base
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,13 @@
1
+ module RubyLLM::Monitoring
2
+ class AlertMailer < ApplicationMailer
3
+ def alert_notification
4
+ @body = params[:body]
5
+
6
+ mail(
7
+ to: params[:to],
8
+ from: params[:from] || default_params[:from],
9
+ subject: params[:subject]
10
+ )
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,8 @@
1
+ module RubyLLM
2
+ module Monitoring
3
+ class ApplicationMailer < ActionMailer::Base
4
+ default from: "from@example.com"
5
+ layout "mailer"
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,53 @@
1
+ module RubyLLM
2
+ module Monitoring
3
+ module Alertable
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ after_create_commit :evaluate_alert_rules
8
+ end
9
+
10
+ private
11
+
12
+ def cooldown(rule_id, custom_cooldown)
13
+ ttl = custom_cooldown || RubyLLM::Monitoring.alert_cooldown
14
+
15
+ Rails.cache.write("ruby_llm-monitoring/#{rule_id}", Time.current + ttl, expires_in: ttl)
16
+ end
17
+
18
+ def cooling_down?(rule_id)
19
+ Rails.cache.exist?("ruby_llm-monitoring/#{rule_id}")
20
+ end
21
+
22
+ def dispatch_to_channels(alert_rule)
23
+ alert_rule[:channels].each do |channel_name|
24
+ channel = RubyLLM::Monitoring.channel_registry.fetch(channel_name)
25
+ channel_config = RubyLLM::Monitoring.channels[channel_name] || {}
26
+ channel.deliver(alert_rule[:message], channel_config)
27
+ rescue => e
28
+ Rails.logger.error "[RubyLLM::Monitoring] Failed to deliver alert to #{channel_name}: #{e.message}"
29
+ end
30
+ end
31
+
32
+ def evaluate_alert_rules
33
+ RubyLLM::Monitoring.alert_rules.each_with_index do |alert_rule, index|
34
+ rule_id = "rule_#{index}"
35
+
36
+ next unless valid_alert_rule?(alert_rule)
37
+ next if cooling_down?(rule_id)
38
+
39
+ events = self.class.where(created_at: alert_rule[:time_range].call)
40
+
41
+ if alert_rule[:rule].call(events)
42
+ cooldown(rule_id, alert_rule[:cooldown])
43
+ dispatch_to_channels(alert_rule)
44
+ end
45
+ end
46
+ end
47
+
48
+ def valid_alert_rule?(alert_rule)
49
+ alert_rule[:time_range].respond_to?(:call) && alert_rule[:rule].respond_to?(:call) && alert_rule[:channels].is_a?(Array) && alert_rule[:message].is_a?(Hash)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,7 @@
1
+ module RubyLLM
2
+ module Monitoring
3
+ class ApplicationRecord < ::ActiveRecord::Base
4
+ self.abstract_class = true
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,22 @@
1
+ module RubyLLM::Monitoring
2
+ class Event < ApplicationRecord
3
+ include Alertable
4
+
5
+ before_validation :set_cost
6
+
7
+ private
8
+
9
+ def set_cost
10
+ model, provider = RubyLLM.models.resolve payload["model"], provider: payload["provider"]
11
+
12
+ self.cost = if provider.local? || [ payload["input_tokens"], payload["output_tokens"] ].all?(nil)
13
+ 0.0
14
+ else
15
+ input_cost = payload["input_tokens"] / 1_000_000.0 * model.input_price_per_million
16
+ output_cost = payload["output_tokens"] / 1_000_000.0 * model.output_price_per_million
17
+
18
+ input_cost + output_cost
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,29 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>RubyLLM Monitoring - <%= content_for :title %></title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+
8
+ <meta name="viewport" content="width=device-width,initial-scale=1">
9
+ <meta name="turbo-cache-control" content="no-cache">
10
+
11
+ <%= yield :head %>
12
+
13
+ <%= stylesheet_link_tag "ruby_llm/monitoring/bulma.min" %>
14
+ <%= stylesheet_link_tag "ruby_llm/monitoring/application", media: "all" %>
15
+ <%= javascript_importmap_tags importmap: RubyLLM::Monitoring.importmap %>
16
+ </head>
17
+ <body>
18
+
19
+ <section class="section">
20
+ <div class="container">
21
+ <%= render "ruby_llm/monitoring/application/flashes" %>
22
+ <%= render "ruby_llm/monitoring/application/nav" %>
23
+ <%= render "ruby_llm/monitoring/application/tabs" %>
24
+ <%= yield %>
25
+ </div>
26
+ </section>
27
+
28
+ </body>
29
+ </html>
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
5
+ <style>
6
+ /* Email styles need to be inline */
7
+ </style>
8
+ </head>
9
+
10
+ <body>
11
+ <%= yield %>
12
+ </body>
13
+ </html>
@@ -0,0 +1 @@
1
+ <%= yield %>
@@ -0,0 +1,2 @@
1
+ <div id="<%= dom_id alert %>">
2
+ </div>
@@ -0,0 +1,26 @@
1
+ <% content_for :title, "Alerts" %>
2
+
3
+ <% if @alert_rules.any? %>
4
+ <table class="table is-hoverable is-fullwidth">
5
+ <thead>
6
+ <tr>
7
+ <th>Time range</th>
8
+ <th>Channels</th>
9
+ <th>Message</th>
10
+ <th>Status</th>
11
+ </tr>
12
+ </thead>
13
+ <tbody>
14
+ <% @alert_rules.each do |alert_rule| %>
15
+ <tr>
16
+ <td><%= time_range = alert_rule[:time_range].call %></td>
17
+ <td><%= alert_rule[:channels].to_sentence %></td>
18
+ <td><%= alert_rule[:message][:text] %></td>
19
+ <td><%= alert_rule[:rule].call(RubyLLM::Monitoring::Event.where(created_at: time_range)) ? "🟢" : "🔴" %></td>
20
+ </tr>
21
+ <% end %>
22
+ </tbody>
23
+ </table>
24
+ <% else %>
25
+ <div class="mt-6 has-text-centered is-size-4 has-text-grey">There are no alert rules</div>
26
+ <% end %>
@@ -0,0 +1,9 @@
1
+ <% flash.each do |name, message| -%>
2
+ <% message_class = { notice: "is-success", alert: "is-danger" }[name.to_sym] %>
3
+
4
+ <article class="message <%= message_class %>">
5
+ <div class="message-body">
6
+ <%= message %>
7
+ </div>
8
+ </article>
9
+ <% end -%>
@@ -0,0 +1,12 @@
1
+ <nav class="navbar" role="navigation" aria-label="main navigation">
2
+ <div class="navbar-menu is-active mb-4">
3
+ <div class="navbar-start">
4
+ <% if defined?(main_app.root_path) %>
5
+ <%= link_to "Back to main app", main_app.root_path %>
6
+ <% end %>
7
+ </div>
8
+
9
+ <div class="navbar-end">
10
+ </div>
11
+ </div>
12
+ </nav>
@@ -0,0 +1,7 @@
1
+ <nav class="buttons is-right" role="navigation" aria-label="pagination">
2
+ <span class="mr-3"><%= page.index %> / <%= page.pages_count || "..." %></span>
3
+ <%= link_to "↞", url_for(page: 1), class: "pagination-previous", disabled: page.first? %>
4
+ <%= link_to "Previous page", url_for(page: page.previous_index), class: "pagination-previous", disabled: page.first? %>
5
+ <%= link_to "Next page", url_for(page: page.next_index), class: "pagination-next", disabled: page.last? %>
6
+ <%= link_to "↠", url_for(page: page.pages_count), class: "pagination-next", disabled: page.last? %>
7
+ </nav>
@@ -0,0 +1,6 @@
1
+ <div class="tabs is-boxed">
2
+ <ul>
3
+ <li class="<%= "is-active" if controller_name == "metrics" %>"><%= link_to "Metrics", root_path %></li>
4
+ <li class="<%= "is-active" if controller_name == "alerts" %>"><%= link_to "Alerts", alerts_path %></li>
5
+ </ul>
6
+ </div>
@@ -0,0 +1,25 @@
1
+ <div class="level">
2
+ <div class="level-left">
3
+ <%= form_with scope: :filter, url: metrics_path, method: :get, data: {controller: "auto-submit"} do |form| %>
4
+ <div class="field is-grouped">
5
+ <div>
6
+ <%= form.label :created_at_start, class: "label" %>
7
+ <%= form.datetime_field :created_at_start, value: filter_param.dig(:filter, :created_at_start), class: "input", data: {action: "input->auto-submit#submit"} %>
8
+ </div>
9
+ <div>
10
+ <%= form.label :created_at_end, class: "label" %>
11
+ <%= form.datetime_field :created_at_end, value: filter_param.dig(:filter, :created_at_end), class: "input", data: {action: "input->auto-submit#submit"} %>
12
+ </div>
13
+ <div>
14
+ <%= form.label :resolution, class: "label" %>
15
+ <div class="select">
16
+ <%= form.select :resolution, resolution_options, {selected: filter_param.dig(:filter, :resolution).in_minutes.to_i}, data: {action: "auto-submit#submit"} %>
17
+ </div>
18
+ </div>
19
+ <div class="is-align-self-flex-end">
20
+ <%= link_to "Clear", clear_path, class: "button" %>
21
+ </div>
22
+ </div>
23
+ <% end %>
24
+ </div>
25
+ </div>
@@ -0,0 +1,51 @@
1
+ <div class="columns is-multiline mb-5">
2
+ <div class="column is-3">
3
+ <div class="box has-text-centered">
4
+ <p class="heading">Requests</p>
5
+ <p class="title"><%= number_with_delimiter(@totals[:requests]) %></p>
6
+ </div>
7
+ </div>
8
+ <div class="column is-3">
9
+ <div class="box has-text-centered">
10
+ <p class="heading">Cost</p>
11
+ <p class="title"><%= number_to_currency(@totals[:cost]) %></p>
12
+ </div>
13
+ </div>
14
+ <div class="column is-3">
15
+ <div class="box has-text-centered">
16
+ <p class="heading">Avg Response Time</p>
17
+ <p class="title"><%= @totals[:avg_response_time] ? "#{@totals[:avg_response_time].round(1)}ms" : "N/A" %></p>
18
+ </div>
19
+ </div>
20
+ <div class="column is-3">
21
+ <div class="box has-text-centered">
22
+ <p class="heading">Error Rate</p>
23
+ <p class="title"><%= @totals[:error_rate] %>%</p>
24
+ </div>
25
+ </div>
26
+ </div>
27
+
28
+ <% if @totals_by_provider.any? %>
29
+ <div class="box mb-5">
30
+ <table class="table is-fullwidth is-striped">
31
+ <thead>
32
+ <tr>
33
+ <th>Provider/Model</th>
34
+ <th class="has-text-right">Requests</th>
35
+ <th class="has-text-right">Cost</th>
36
+ <th class="has-text-right">Avg Response Time</th>
37
+ </tr>
38
+ </thead>
39
+ <tbody>
40
+ <% @totals_by_provider.each do |row| %>
41
+ <tr>
42
+ <td><%= row.provider %>/<%= row.model %></td>
43
+ <td class="has-text-right"><%= number_with_delimiter(row.requests) %></td>
44
+ <td class="has-text-right"><%= number_to_currency(row.cost || 0) %></td>
45
+ <td class="has-text-right"><%= row.avg_response_time ? "#{row.avg_response_time.round(1)}ms" : "N/A" %></td>
46
+ </tr>
47
+ <% end %>
48
+ </tbody>
49
+ </table>
50
+ </div>
51
+ <% end %>
@@ -0,0 +1,16 @@
1
+ <% content_for :title, "Metrics" %>
2
+
3
+ <%= render "filters", clear_path: metrics_path %>
4
+
5
+ <%= render "totals" %>
6
+
7
+ <% @metrics.each do |metric| %>
8
+ <div
9
+ data-controller="chart"
10
+ data-title="<%= metric[:title] %>"
11
+ data-min="<%= @created_at_start.to_i.in_milliseconds %>"
12
+ data-formatter="<%= metric[:unit] %>"
13
+ data-series="<%= metric[:series].to_json %>"
14
+ >
15
+ </div>
16
+ <% end %>
@@ -0,0 +1,8 @@
1
+ pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
2
+ pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true
3
+ pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
4
+ pin "@stimulus-components/auto-submit", to: "https://ga.jspm.io/npm:@stimulus-components/auto-submit@6.0.0/dist/stimulus-auto-submit.mjs"
5
+ pin "apexcharts", to: "https://ga.jspm.io/npm:apexcharts@4.3.0/dist/apexcharts.common.js"
6
+
7
+ pin "application", to: "ruby_llm/monitoring/application.js", preload: true
8
+ pin_all_from RubyLLM::Monitoring::Engine.root.join("app/javascript/ruby_llm/monitoring/controllers"), under: "controllers", to: "ruby_llm/monitoring/controllers"
data/config/routes.rb ADDED
@@ -0,0 +1,6 @@
1
+ RubyLLM::Monitoring::Engine.routes.draw do
2
+ resources :alerts, only: :index
3
+ resources :metrics, only: :index
4
+
5
+ root to: "metrics#index"
6
+ end