chronicle-rails 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 (50) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +28 -0
  4. data/Rakefile +6 -0
  5. data/app/controllers/chronicle/api_logs_controller.rb +61 -0
  6. data/app/controllers/chronicle/api_routes_controller.rb +35 -0
  7. data/app/controllers/chronicle/application_controller.rb +23 -0
  8. data/app/controllers/chronicle/auth_controller.rb +17 -0
  9. data/app/controllers/chronicle/error_groups_controller.rb +58 -0
  10. data/app/controllers/chronicle/error_logs_controller.rb +47 -0
  11. data/app/controllers/chronicle/resource_controller.rb +41 -0
  12. data/app/controllers/concerns/chronicle/filterable.rb +57 -0
  13. data/app/controllers/concerns/chronicle/pagination.rb +41 -0
  14. data/app/errors/chronicle/authentication_error.rb +7 -0
  15. data/app/errors/chronicle/bad_request_error.rb +7 -0
  16. data/app/errors/chronicle/base_error.rb +10 -0
  17. data/app/errors/chronicle/forbidden_error.rb +7 -0
  18. data/app/errors/chronicle/not_acceptable_error.rb +7 -0
  19. data/app/errors/chronicle/not_found_error.rb +7 -0
  20. data/app/errors/chronicle/resource_busy_error.rb +7 -0
  21. data/app/errors/chronicle/validation_error.rb +7 -0
  22. data/app/jobs/chronicle/application_job.rb +4 -0
  23. data/app/jobs/chronicle/flush_api_logs_job.rb +9 -0
  24. data/app/mailers/chronicle/application_mailer.rb +6 -0
  25. data/app/models/chronicle/admin_user.rb +16 -0
  26. data/app/models/chronicle/api_log.rb +25 -0
  27. data/app/models/chronicle/api_route.rb +5 -0
  28. data/app/models/chronicle/application_record.rb +5 -0
  29. data/app/models/chronicle/error_group.rb +53 -0
  30. data/app/models/chronicle/error_log.rb +69 -0
  31. data/app/services/chronicle/api_logs/buffer.rb +62 -0
  32. data/app/services/chronicle/api_logs/flusher.rb +98 -0
  33. data/app/services/chronicle/api_logs/metrics.rb +285 -0
  34. data/app/services/chronicle/api_logs/updater.rb +19 -0
  35. data/app/services/chronicle/api_routes/stats.rb +131 -0
  36. data/app/services/chronicle/error_logs/group_resolver.rb +66 -0
  37. data/config/routes.rb +28 -0
  38. data/db/migrate/20260101000001_create_chronicle_admin_users.rb +16 -0
  39. data/db/migrate/20260101000002_create_chronicle_api_logs.rb +32 -0
  40. data/db/migrate/20260101000003_create_chronicle_api_routes.rb +13 -0
  41. data/db/migrate/20260101000004_create_chronicle_error_groups.rb +26 -0
  42. data/db/migrate/20260101000005_create_chronicle_error_logs.rb +19 -0
  43. data/lib/chronicle/configuration.rb +56 -0
  44. data/lib/chronicle/engine.rb +12 -0
  45. data/lib/chronicle/util.rb +26 -0
  46. data/lib/chronicle/version.rb +3 -0
  47. data/lib/chronicle-rails.rb +1 -0
  48. data/lib/chronicle.rb +70 -0
  49. data/lib/tasks/chronicle_tasks.rake +4 -0
  50. metadata +127 -0
@@ -0,0 +1,66 @@
1
+ module Chronicle
2
+ module ErrorLogs
3
+ # Resolves the ErrorGroup for a new ErrorLog.
4
+ #
5
+ # Responsibilities:
6
+ # - Derive the fingerprint (use the caller-supplied one, or compute from
7
+ # the log's structural identity if none was given).
8
+ # - Find an existing group for that fingerprint and increment its
9
+ # occurrence tracking.
10
+ # - Create a brand-new group when none exists yet.
11
+ # - Handle the concurrent-insert race condition transparently.
12
+ class GroupResolver
13
+ def initialize(error_log)
14
+ @error_log = error_log
15
+ end
16
+
17
+ def call
18
+ existing = ErrorGroup.find_by(project: error_log.project, fingerprint: fingerprint)
19
+ return bump_occurrence(existing) if existing
20
+
21
+ create_group
22
+ rescue ActiveRecord::RecordNotUnique
23
+ # A concurrent request won the INSERT race. Find the winner and record
24
+ # this occurrence against it so the count stays accurate.
25
+ bump_occurrence(ErrorGroup.find_by!(project: error_log.project, fingerprint: fingerprint))
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :error_log
31
+
32
+ def fingerprint
33
+ @fingerprint ||= error_log.error_fingerprint.presence ||
34
+ ErrorGroup.compute_fingerprint(error_log)
35
+ end
36
+
37
+ def versions
38
+ { backend_version: error_log.backend_version, client_version: error_log.client_version }
39
+ end
40
+
41
+ def bump_occurrence(group)
42
+ group.record_occurrence!(**versions)
43
+ group
44
+ end
45
+
46
+ def create_group
47
+ now = Time.current
48
+ ErrorGroup.create!(
49
+ project: error_log.project,
50
+ fingerprint: fingerprint,
51
+ source_type: error_log.source_type,
52
+ source_name: error_log.source_name,
53
+ error_message: error_log.error_message,
54
+ original_backtrace: error_log.original_backtrace,
55
+ cleaned_backtrace: error_log.cleaned_backtrace,
56
+ backend_version: error_log.backend_version,
57
+ client_version: error_log.client_version,
58
+ status: ErrorGroup::OPEN,
59
+ first_seen_at: now,
60
+ last_seen_at: now,
61
+ occurrence_count: 1
62
+ )
63
+ end
64
+ end
65
+ end
66
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,28 @@
1
+ Chronicle::Engine.routes.draw do
2
+ # Auth (admin login only — sync endpoints removed, no longer needed as a separate service)
3
+ get 'auth/login', to: 'auth#login'
4
+
5
+ # API log updates + metrics (create is handled internally via service/buffer, not HTTP)
6
+ resources :api_logs, only: [] do
7
+ collection do
8
+ put ':request_id', to: 'api_logs#update', as: :update
9
+ get :kpi_cards
10
+ get :distribution_metrics
11
+ end
12
+ end
13
+
14
+ # Error log deletion (create is handled internally via Chronicle.log_error)
15
+ resources :error_logs, only: [:destroy]
16
+
17
+ # Admin-only reads and management
18
+ scope :admin, as: :admin do
19
+ resources :api_logs, only: [:index, :show]
20
+ resources :error_logs, only: [:index, :show]
21
+ resources :error_groups, only: [:index, :show, :update]
22
+ resources :api_routes, only: [:index] do
23
+ collection do
24
+ get :stats
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,16 @@
1
+ class CreateChronicleAdminUsers < ActiveRecord::Migration[7.1]
2
+ def change
3
+ create_table :chronicle_admin_users do |t|
4
+ t.string :email, null: false
5
+ t.string :name, null: false
6
+ t.string :password_digest, null: false
7
+ t.string :auth_token, null: false
8
+ t.integer :role, default: 0, null: false
9
+
10
+ t.timestamps
11
+ end
12
+
13
+ add_index :chronicle_admin_users, :email, unique: true
14
+ add_index :chronicle_admin_users, :auth_token, unique: true
15
+ end
16
+ end
@@ -0,0 +1,32 @@
1
+ class CreateChronicleApiLogs < ActiveRecord::Migration[7.1]
2
+ def change
3
+ create_table :chronicle_api_logs do |t|
4
+ t.bigint :user_id
5
+ t.string :request_id
6
+ t.string :device_os
7
+ t.string :device_id
8
+ t.string :device_type
9
+ t.string :device_model_name
10
+ t.string :brand
11
+ t.string :os_version
12
+ t.string :time_zone
13
+ t.string :client_version
14
+ t.string :backend_version
15
+ t.string :ip_address
16
+ t.string :http_method
17
+ t.string :api_endpoint
18
+ t.integer :http_status_code
19
+ t.integer :response_time_ms
20
+ t.integer :frontend_response_time_ms
21
+ t.datetime :timestamp
22
+ t.json :path_params
23
+ t.json :meta
24
+
25
+ t.timestamps
26
+ end
27
+
28
+ add_index :chronicle_api_logs, :user_id
29
+ add_index :chronicle_api_logs, :request_id
30
+ add_index :chronicle_api_logs, :timestamp
31
+ end
32
+ end
@@ -0,0 +1,13 @@
1
+ class CreateChronicleApiRoutes < ActiveRecord::Migration[7.1]
2
+ def change
3
+ create_table :chronicle_api_routes do |t|
4
+ t.string :path, null: false
5
+ t.string :http_method, null: false
6
+ t.datetime :first_seen_at, null: false
7
+
8
+ t.timestamps
9
+ end
10
+
11
+ add_index :chronicle_api_routes, [:path, :http_method], unique: true, name: 'index_chronicle_api_routes_on_path_and_method'
12
+ end
13
+ end
@@ -0,0 +1,26 @@
1
+ class CreateChronicleErrorGroups < ActiveRecord::Migration[7.1]
2
+ def change
3
+ create_table :chronicle_error_groups do |t|
4
+ t.string :project, null: false
5
+ t.string :fingerprint, null: false
6
+ t.string :source_type, null: false
7
+ t.string :source_name, null: false
8
+ t.string :error_message, null: false
9
+ t.text :original_backtrace
10
+ t.text :cleaned_backtrace
11
+ t.string :status, null: false, default: 'open'
12
+ t.integer :occurrence_count, null: false, default: 0
13
+ t.string :jira_link
14
+ t.string :backend_version
15
+ t.string :client_version
16
+ t.datetime :first_seen_at, null: false
17
+ t.datetime :last_seen_at, null: false
18
+
19
+ t.timestamps
20
+ end
21
+
22
+ add_index :chronicle_error_groups, [:project, :fingerprint], unique: true, name: 'index_chronicle_error_groups_on_project_and_fingerprint'
23
+ add_index :chronicle_error_groups, :status
24
+ add_index :chronicle_error_groups, :last_seen_at
25
+ end
26
+ end
@@ -0,0 +1,19 @@
1
+ class CreateChronicleErrorLogs < ActiveRecord::Migration[7.1]
2
+ def change
3
+ create_table :chronicle_error_logs do |t|
4
+ t.bigint :error_group_id, null: false
5
+ t.bigint :user_id
6
+ t.string :request_id
7
+ t.string :backend_version
8
+ t.string :client_version
9
+ t.json :context
10
+
11
+ t.timestamps
12
+ end
13
+
14
+ add_index :chronicle_error_logs, :error_group_id
15
+ add_index :chronicle_error_logs, :user_id
16
+
17
+ add_foreign_key :chronicle_error_logs, :chronicle_error_groups, column: :error_group_id
18
+ end
19
+ end
@@ -0,0 +1,56 @@
1
+ module Chronicle
2
+ class Configuration
3
+ attr_accessor :user_class,
4
+ :admin_user_class,
5
+ :api_token,
6
+ :project_name,
7
+ :backend_version,
8
+ :api_log_buffer,
9
+ :api_log_flush_interval,
10
+ :api_log_flush_size,
11
+ :api_log_buffer_dir,
12
+ :skip_paths,
13
+ :skip_api_log_proc,
14
+ :disable_api_logging,
15
+ :disable_error_logging
16
+
17
+ def initialize
18
+ @user_class = nil
19
+ @admin_user_class = 'Chronicle::AdminUser'
20
+ @api_token = nil
21
+ @project_name = nil
22
+ @backend_version = -> {}
23
+ @api_log_buffer = :file
24
+ @api_log_flush_interval = 30
25
+ @api_log_flush_size = 500
26
+ @api_log_buffer_dir = nil
27
+ @skip_paths = []
28
+ @skip_api_log_proc = nil
29
+ @disable_api_logging = false
30
+ @disable_error_logging = false
31
+ end
32
+
33
+ def user_model
34
+ return nil if user_class.nil?
35
+ user_class.is_a?(String) ? user_class.constantize : user_class
36
+ end
37
+
38
+ def admin_user_model
39
+ admin_user_class.is_a?(String) ? admin_user_class.constantize : admin_user_class
40
+ end
41
+
42
+ def resolved_backend_version
43
+ backend_version.respond_to?(:call) ? backend_version.call : backend_version
44
+ end
45
+
46
+ def api_logging_disabled?
47
+ val = disable_api_logging
48
+ val.respond_to?(:call) ? val.call : val
49
+ end
50
+
51
+ def error_logging_disabled?
52
+ val = disable_error_logging
53
+ val.respond_to?(:call) ? val.call : val
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,12 @@
1
+ module Chronicle
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace Chronicle
4
+
5
+ config.generators.api_only = true
6
+
7
+ config.generators do |g|
8
+ g.test_framework :rspec
9
+ g.factory_bot dir: 'spec/factories'
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,26 @@
1
+ module Chronicle
2
+ module Util
3
+ class << self
4
+ def coerce_to_hash(input)
5
+ case input
6
+ when Hash
7
+ input
8
+ when ActionController::Parameters
9
+ input.to_unsafe_h
10
+ else
11
+ raise ArgumentError, 'Input expected to be a hash'
12
+ end
13
+ end
14
+
15
+ def parse_date(str)
16
+ return nil if str.blank?
17
+
18
+ begin
19
+ Date.parse(str)
20
+ rescue StandardError
21
+ nil
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,3 @@
1
+ module Chronicle
2
+ VERSION = '0.1.0'.freeze
3
+ end
@@ -0,0 +1 @@
1
+ require 'chronicle'
data/lib/chronicle.rb ADDED
@@ -0,0 +1,70 @@
1
+ require 'chronicle/version'
2
+ require 'chronicle/configuration'
3
+ require 'chronicle/util'
4
+ require 'chronicle/engine'
5
+
6
+ module Chronicle
7
+ class Error < StandardError; end
8
+ class ConfigurationError < Error; end
9
+
10
+ class << self
11
+ def configuration
12
+ @configuration ||= Configuration.new
13
+ end
14
+
15
+ def configure
16
+ yield configuration
17
+ end
18
+
19
+ def reset_configuration!
20
+ @configuration = Configuration.new
21
+ end
22
+
23
+ def config
24
+ configuration
25
+ end
26
+
27
+ # Buffers an API log payload to a per-process file. The buffer is
28
+ # drained periodically by Chronicle::FlushApiLogsJob, and opportunistically
29
+ # when the configured flush size is exceeded.
30
+ def buffer_api_log(payload)
31
+ return if configuration.api_logging_disabled?
32
+ ApiLogs::Buffer.append(payload)
33
+ end
34
+
35
+ # Bulk-inserts API log payloads directly, bypassing the file buffer.
36
+ # Intended for tests, backfills, or hosts that implement their own buffering.
37
+ def bulk_log_api(payloads)
38
+ return if configuration.api_logging_disabled?
39
+ return if payloads.blank?
40
+
41
+ now = Time.current
42
+ allowed = ApiLog.column_names - ['id']
43
+ rows = payloads.map do |raw|
44
+ row = raw.respond_to?(:stringify_keys) ? raw.stringify_keys.slice(*allowed) : raw.slice(*allowed)
45
+ row['created_at'] ||= now
46
+ row['updated_at'] ||= now
47
+ row
48
+ end
49
+
50
+ rows.each_slice(ApiLogs::Flusher::INSERT_BATCH_SIZE) do |batch|
51
+ ApiLog.insert_all(batch) # rubocop:disable Rails/SkipsModelValidations
52
+ end
53
+
54
+ ApiLogs::Flusher.sync_routes(rows)
55
+ end
56
+
57
+ # Synchronously creates an ErrorLog. The model's before_validation hook
58
+ # routes it through ErrorLogs::GroupResolver for fingerprint dedup.
59
+ def log_error(payload)
60
+ return if configuration.error_logging_disabled?
61
+ ErrorLog.create!(payload)
62
+ end
63
+
64
+ # Drains all per-PID buffer files into the database. Safe to run
65
+ # concurrently — atomic file renames prevent double-processing.
66
+ def flush_api_logs!
67
+ ApiLogs::Flusher.call
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :chronicle do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,127 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: chronicle-rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Sathwik Anil
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: bcrypt
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '3.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '3.1'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rails
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '7.1'
33
+ - - "<"
34
+ - !ruby/object:Gem::Version
35
+ version: '9'
36
+ type: :runtime
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '7.1'
43
+ - - "<"
44
+ - !ruby/object:Gem::Version
45
+ version: '9'
46
+ description: Chronicle is a mountable Rails engine that captures API request logs,
47
+ error logs, and exposesadmin endpoints for observability. Designed to be embedded
48
+ into any Rails application as adrop-in observability layer.
49
+ email:
50
+ - sathwik139@gmail.com
51
+ executables: []
52
+ extensions: []
53
+ extra_rdoc_files: []
54
+ files:
55
+ - MIT-LICENSE
56
+ - README.md
57
+ - Rakefile
58
+ - app/controllers/chronicle/api_logs_controller.rb
59
+ - app/controllers/chronicle/api_routes_controller.rb
60
+ - app/controllers/chronicle/application_controller.rb
61
+ - app/controllers/chronicle/auth_controller.rb
62
+ - app/controllers/chronicle/error_groups_controller.rb
63
+ - app/controllers/chronicle/error_logs_controller.rb
64
+ - app/controllers/chronicle/resource_controller.rb
65
+ - app/controllers/concerns/chronicle/filterable.rb
66
+ - app/controllers/concerns/chronicle/pagination.rb
67
+ - app/errors/chronicle/authentication_error.rb
68
+ - app/errors/chronicle/bad_request_error.rb
69
+ - app/errors/chronicle/base_error.rb
70
+ - app/errors/chronicle/forbidden_error.rb
71
+ - app/errors/chronicle/not_acceptable_error.rb
72
+ - app/errors/chronicle/not_found_error.rb
73
+ - app/errors/chronicle/resource_busy_error.rb
74
+ - app/errors/chronicle/validation_error.rb
75
+ - app/jobs/chronicle/application_job.rb
76
+ - app/jobs/chronicle/flush_api_logs_job.rb
77
+ - app/mailers/chronicle/application_mailer.rb
78
+ - app/models/chronicle/admin_user.rb
79
+ - app/models/chronicle/api_log.rb
80
+ - app/models/chronicle/api_route.rb
81
+ - app/models/chronicle/application_record.rb
82
+ - app/models/chronicle/error_group.rb
83
+ - app/models/chronicle/error_log.rb
84
+ - app/services/chronicle/api_logs/buffer.rb
85
+ - app/services/chronicle/api_logs/flusher.rb
86
+ - app/services/chronicle/api_logs/metrics.rb
87
+ - app/services/chronicle/api_logs/updater.rb
88
+ - app/services/chronicle/api_routes/stats.rb
89
+ - app/services/chronicle/error_logs/group_resolver.rb
90
+ - config/routes.rb
91
+ - db/migrate/20260101000001_create_chronicle_admin_users.rb
92
+ - db/migrate/20260101000002_create_chronicle_api_logs.rb
93
+ - db/migrate/20260101000003_create_chronicle_api_routes.rb
94
+ - db/migrate/20260101000004_create_chronicle_error_groups.rb
95
+ - db/migrate/20260101000005_create_chronicle_error_logs.rb
96
+ - lib/chronicle-rails.rb
97
+ - lib/chronicle.rb
98
+ - lib/chronicle/configuration.rb
99
+ - lib/chronicle/engine.rb
100
+ - lib/chronicle/util.rb
101
+ - lib/chronicle/version.rb
102
+ - lib/tasks/chronicle_tasks.rake
103
+ homepage: https://github.com/Wildcats-Dev/chronicle-rails
104
+ licenses:
105
+ - MIT
106
+ metadata:
107
+ homepage_uri: https://github.com/Wildcats-Dev/chronicle-rails
108
+ source_code_uri: https://github.com/Wildcats-Dev/chronicle-rails
109
+ rubygems_mfa_required: 'true'
110
+ rdoc_options: []
111
+ require_paths:
112
+ - lib
113
+ required_ruby_version: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '3.2'
118
+ required_rubygems_version: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: '0'
123
+ requirements: []
124
+ rubygems_version: 3.6.9
125
+ specification_version: 4
126
+ summary: Pluggable Rails engine for API request and error logging.
127
+ test_files: []