solid_log-service 0.1.0 → 0.2.1

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.
metadata CHANGED
@@ -1,143 +1,163 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solid_log-service
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dan Loman
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-12-29 00:00:00.000000000 Z
11
+ date: 2026-01-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: solid_log-core
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 0.2.1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 0.2.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: rack
15
29
  requirement: !ruby/object:Gem::Requirement
16
30
  requirements:
17
31
  - - "~>"
18
32
  - !ruby/object:Gem::Version
19
- version: 0.1.0
33
+ version: '3.0'
20
34
  type: :runtime
21
35
  prerelease: false
22
36
  version_requirements: !ruby/object:Gem::Requirement
23
37
  requirements:
24
38
  - - "~>"
25
39
  - !ruby/object:Gem::Version
26
- version: 0.1.0
40
+ version: '3.0'
27
41
  - !ruby/object:Gem::Dependency
28
- name: rails
42
+ name: puma
29
43
  requirement: !ruby/object:Gem::Requirement
30
44
  requirements:
31
45
  - - ">="
32
46
  - !ruby/object:Gem::Version
33
- version: 8.0.2
47
+ version: '6'
48
+ - - "<"
49
+ - !ruby/object:Gem::Version
50
+ version: '8'
34
51
  type: :runtime
35
52
  prerelease: false
36
53
  version_requirements: !ruby/object:Gem::Requirement
37
54
  requirements:
38
55
  - - ">="
39
56
  - !ruby/object:Gem::Version
40
- version: 8.0.2
57
+ version: '6'
58
+ - - "<"
59
+ - !ruby/object:Gem::Version
60
+ version: '8'
41
61
  - !ruby/object:Gem::Dependency
42
- name: puma
62
+ name: activesupport
43
63
  requirement: !ruby/object:Gem::Requirement
44
64
  requirements:
45
65
  - - "~>"
46
66
  - !ruby/object:Gem::Version
47
- version: '6.0'
67
+ version: '8.0'
48
68
  type: :runtime
49
69
  prerelease: false
50
70
  version_requirements: !ruby/object:Gem::Requirement
51
71
  requirements:
52
72
  - - "~>"
53
73
  - !ruby/object:Gem::Version
54
- version: '6.0'
74
+ version: '8.0'
55
75
  - !ruby/object:Gem::Dependency
56
- name: sqlite3
76
+ name: activerecord
57
77
  requirement: !ruby/object:Gem::Requirement
58
78
  requirements:
59
- - - ">="
79
+ - - "~>"
60
80
  - !ruby/object:Gem::Version
61
- version: '2.1'
62
- type: :development
81
+ version: '8.0'
82
+ type: :runtime
63
83
  prerelease: false
64
84
  version_requirements: !ruby/object:Gem::Requirement
65
85
  requirements:
66
- - - ">="
86
+ - - "~>"
67
87
  - !ruby/object:Gem::Version
68
- version: '2.1'
88
+ version: '8.0'
69
89
  - !ruby/object:Gem::Dependency
70
- name: minitest
90
+ name: actioncable
71
91
  requirement: !ruby/object:Gem::Requirement
72
92
  requirements:
73
- - - ">="
93
+ - - "~>"
74
94
  - !ruby/object:Gem::Version
75
- version: '5.0'
76
- type: :development
95
+ version: '8.0'
96
+ type: :runtime
77
97
  prerelease: false
78
98
  version_requirements: !ruby/object:Gem::Requirement
79
99
  requirements:
80
- - - ">="
100
+ - - "~>"
81
101
  - !ruby/object:Gem::Version
82
- version: '5.0'
102
+ version: '8.0'
83
103
  - !ruby/object:Gem::Dependency
84
- name: combustion
104
+ name: rack-cors
85
105
  requirement: !ruby/object:Gem::Requirement
86
106
  requirements:
87
107
  - - "~>"
88
108
  - !ruby/object:Gem::Version
89
- version: '1.4'
90
- type: :development
109
+ version: '2.0'
110
+ type: :runtime
91
111
  prerelease: false
92
112
  version_requirements: !ruby/object:Gem::Requirement
93
113
  requirements:
94
114
  - - "~>"
95
115
  - !ruby/object:Gem::Version
96
- version: '1.4'
116
+ version: '2.0'
97
117
  - !ruby/object:Gem::Dependency
98
- name: rake
118
+ name: sqlite3
99
119
  requirement: !ruby/object:Gem::Requirement
100
120
  requirements:
101
- - - "~>"
121
+ - - ">="
102
122
  - !ruby/object:Gem::Version
103
- version: '13.0'
123
+ version: '2.1'
104
124
  type: :development
105
125
  prerelease: false
106
126
  version_requirements: !ruby/object:Gem::Requirement
107
127
  requirements:
108
- - - "~>"
128
+ - - ">="
109
129
  - !ruby/object:Gem::Version
110
- version: '13.0'
130
+ version: '2.1'
111
131
  - !ruby/object:Gem::Dependency
112
- name: debug
132
+ name: minitest
113
133
  requirement: !ruby/object:Gem::Requirement
114
134
  requirements:
115
135
  - - ">="
116
136
  - !ruby/object:Gem::Version
117
- version: '0'
137
+ version: '5.0'
118
138
  type: :development
119
139
  prerelease: false
120
140
  version_requirements: !ruby/object:Gem::Requirement
121
141
  requirements:
122
142
  - - ">="
123
143
  - !ruby/object:Gem::Version
124
- version: '0'
144
+ version: '5.0'
125
145
  - !ruby/object:Gem::Dependency
126
- name: rubocop
146
+ name: rake
127
147
  requirement: !ruby/object:Gem::Requirement
128
148
  requirements:
129
- - - ">="
149
+ - - "~>"
130
150
  - !ruby/object:Gem::Version
131
- version: '0'
151
+ version: '13.0'
132
152
  type: :development
133
153
  prerelease: false
134
154
  version_requirements: !ruby/object:Gem::Requirement
135
155
  requirements:
136
- - - ">="
156
+ - - "~>"
137
157
  - !ruby/object:Gem::Version
138
- version: '0'
158
+ version: '13.0'
139
159
  - !ruby/object:Gem::Dependency
140
- name: rubocop-rails-omakase
160
+ name: debug
141
161
  requirement: !ruby/object:Gem::Requirement
142
162
  requirements:
143
163
  - - ">="
@@ -151,7 +171,7 @@ dependencies:
151
171
  - !ruby/object:Gem::Version
152
172
  version: '0'
153
173
  - !ruby/object:Gem::Dependency
154
- name: rack-cors
174
+ name: rubocop
155
175
  requirement: !ruby/object:Gem::Requirement
156
176
  requirements:
157
177
  - - ">="
@@ -165,7 +185,7 @@ dependencies:
165
185
  - !ruby/object:Gem::Version
166
186
  version: '0'
167
187
  - !ruby/object:Gem::Dependency
168
- name: solid_cable
188
+ name: rubocop-rails-omakase
169
189
  requirement: !ruby/object:Gem::Requirement
170
190
  requirements:
171
191
  - - ">="
@@ -183,36 +203,20 @@ description: Provides HTTP API for log ingestion, background processing with bui
183
203
  Rails apps.
184
204
  email:
185
205
  - daniel.h.loman@gmail.com
186
- executables:
187
- - solid_log_service
206
+ executables: []
188
207
  extensions: []
189
208
  extra_rdoc_files: []
190
209
  files:
191
210
  - MIT-LICENSE
192
211
  - README.md
193
212
  - Rakefile
194
- - app/controllers/solid_log/api/base_controller.rb
195
- - app/controllers/solid_log/api/v1/entries_controller.rb
196
- - app/controllers/solid_log/api/v1/facets_controller.rb
197
- - app/controllers/solid_log/api/v1/health_controller.rb
198
- - app/controllers/solid_log/api/v1/ingest_controller.rb
199
- - app/controllers/solid_log/api/v1/search_controller.rb
200
- - app/controllers/solid_log/api/v1/timelines_controller.rb
201
- - app/jobs/solid_log/application_job.rb
202
- - app/jobs/solid_log/cache_cleanup_job.rb
203
- - app/jobs/solid_log/field_analysis_job.rb
204
- - app/jobs/solid_log/parser_job.rb
205
- - app/jobs/solid_log/retention_job.rb
206
- - bin/solid_log_service
207
213
  - config.ru
208
214
  - config/cable.yml
209
- - config/routes.rb
210
215
  - lib/solid_log-service.rb
211
216
  - lib/solid_log/service.rb
212
- - lib/solid_log/service/application.rb
213
217
  - lib/solid_log/service/configuration.rb
214
- - lib/solid_log/service/engine.rb
215
218
  - lib/solid_log/service/job_processor.rb
219
+ - lib/solid_log/service/rack_app.rb
216
220
  - lib/solid_log/service/scheduler.rb
217
221
  - lib/solid_log/service/version.rb
218
222
  homepage: https://github.com/namolnad/solid_log
@@ -1,80 +0,0 @@
1
- module SolidLog
2
- module Api
3
- class BaseController < ActionController::API
4
- before_action :authenticate_token!
5
-
6
- rescue_from ActionDispatch::Http::Parameters::ParseError do |exception|
7
- render json: {
8
- error: "Invalid JSON",
9
- message: exception.message
10
- }, status: :unprocessable_entity
11
- end
12
-
13
- rescue_from ActionController::BadRequest do |exception|
14
- render json: {
15
- error: "Invalid JSON",
16
- message: exception.message
17
- }, status: :unprocessable_entity
18
- end
19
-
20
- rescue_from StandardError do |exception|
21
- # Check if it's a parameter parsing error based on message
22
- if exception.message.include?("parsing request parameters")
23
- render json: {
24
- error: "Invalid JSON",
25
- message: exception.message
26
- }, status: :unprocessable_entity
27
- else
28
- Rails.logger.error "SolidLog API Error: #{exception.message}"
29
- Rails.logger.error exception.backtrace.join("\n")
30
-
31
- render json: {
32
- error: "Internal server error",
33
- message: exception.message
34
- }, status: :internal_server_error
35
- end
36
- end
37
-
38
- rescue_from ActiveRecord::RecordInvalid do |exception|
39
- render json: {
40
- error: "Validation error",
41
- details: exception.record.errors.full_messages
42
- }, status: :unprocessable_entity
43
- end
44
-
45
- private
46
-
47
- def authenticate_token!
48
- token_value = extract_bearer_token
49
-
50
- unless token_value
51
- render json: { error: "Missing or invalid Authorization header" }, status: :unauthorized
52
- return
53
- end
54
-
55
- @current_token = SolidLog::Token.authenticate(token_value)
56
-
57
- unless @current_token
58
- render json: { error: "Invalid token" }, status: :unauthorized
59
- return
60
- end
61
-
62
- # Touch last_used_at timestamp
63
- @current_token.touch_last_used!
64
- end
65
-
66
- def current_token
67
- @current_token
68
- end
69
-
70
- def extract_bearer_token
71
- header = request.headers["Authorization"]
72
- return nil unless header
73
-
74
- # Expected format: "Bearer <token>"
75
- matches = header.match(/^Bearer (.+)$/i)
76
- matches[1] if matches
77
- end
78
- end
79
- end
80
- end
@@ -1,55 +0,0 @@
1
- module SolidLog
2
- module Api
3
- module V1
4
- class EntriesController < Api::BaseController
5
- # GET /api/v1/entries
6
- def index
7
- search_service = SolidLog::SearchService.new(filter_params)
8
- entries = search_service.search
9
-
10
- render json: {
11
- entries: entries.as_json(methods: [:extra_fields_hash]),
12
- total: entries.count,
13
- limit: params[:limit]&.to_i || 100
14
- }
15
- end
16
-
17
- # GET /api/v1/entries/:id
18
- def show
19
- entry = Entry.find(params[:id])
20
-
21
- render json: {
22
- entry: entry.as_json(methods: [:extra_fields_hash])
23
- }
24
- rescue ActiveRecord::RecordNotFound
25
- render json: { error: "Entry not found" }, status: :not_found
26
- end
27
-
28
- private
29
-
30
- def filter_params
31
- # Extract filters from params[:filters] to avoid Rails routing params collision
32
- search_params = {}
33
- filters = params[:filters] || {}
34
-
35
- search_params[:levels] = [filters[:level]].compact if filters[:level].present?
36
- search_params[:app] = filters[:app] if filters[:app].present?
37
- search_params[:env] = filters[:env] if filters[:env].present?
38
- search_params[:controller] = filters[:controller] if filters[:controller].present?
39
- search_params[:action] = filters[:action] if filters[:action].present?
40
- search_params[:path] = filters[:path] if filters[:path].present?
41
- search_params[:method] = filters[:method] if filters[:method].present?
42
- search_params[:status_code] = filters[:status_code] if filters[:status_code].present?
43
- search_params[:start_time] = filters[:start_time] if filters[:start_time].present?
44
- search_params[:end_time] = filters[:end_time] if filters[:end_time].present?
45
- search_params[:min_duration] = filters[:min_duration] if filters[:min_duration].present?
46
- search_params[:max_duration] = filters[:max_duration] if filters[:max_duration].present?
47
- search_params[:query] = params[:q] if params[:q].present?
48
- search_params[:limit] = params[:limit] if params[:limit].present?
49
-
50
- search_params
51
- end
52
- end
53
- end
54
- end
55
- end
@@ -1,41 +0,0 @@
1
- module SolidLog
2
- module Api
3
- module V1
4
- class FacetsController < Api::BaseController
5
- # GET /api/v1/facets
6
- def index
7
- field = params[:field]
8
-
9
- if field.blank?
10
- return render json: { error: "Field parameter required" }, status: :bad_request
11
- end
12
-
13
- # Use Entry model directly for facets
14
- limit = params[:limit]&.to_i || 100
15
- facets = SolidLog::Entry.facets_for(field, limit: limit)
16
-
17
- render json: {
18
- field: field,
19
- values: facets,
20
- total: facets.size
21
- }
22
- end
23
-
24
- # GET /api/v1/facets/all
25
- def all
26
- facets = {
27
- level: SolidLog::Entry.facets_for("level"),
28
- app: SolidLog::Entry.facets_for("app"),
29
- env: SolidLog::Entry.facets_for("env"),
30
- controller: SolidLog::Entry.facets_for("controller", limit: 50),
31
- action: SolidLog::Entry.facets_for("action", limit: 50),
32
- method: SolidLog::Entry.facets_for("method"),
33
- status_code: SolidLog::Entry.facets_for("status_code")
34
- }
35
-
36
- render json: { facets: facets }
37
- end
38
- end
39
- end
40
- end
41
- end
@@ -1,29 +0,0 @@
1
- module SolidLog
2
- module Api
3
- module V1
4
- class HealthController < Api::BaseController
5
- skip_before_action :authenticate_token!, only: [:show]
6
-
7
- # GET /api/v1/health
8
- def show
9
- metrics = SolidLog::HealthService.metrics
10
-
11
- status = case metrics[:parsing][:health_status]
12
- when "critical"
13
- :service_unavailable
14
- when "warning", "degraded"
15
- :ok # Still functional
16
- else
17
- :ok
18
- end
19
-
20
- render json: {
21
- status: metrics[:parsing][:health_status],
22
- timestamp: Time.current.iso8601,
23
- metrics: metrics
24
- }, status: status
25
- end
26
- end
27
- end
28
- end
29
- end
@@ -1,80 +0,0 @@
1
- module SolidLog
2
- module Api
3
- module V1
4
- class IngestController < Api::BaseController
5
- # POST /api/v1/ingest
6
- # Accepts single log entry (hash) or batch (array of hashes)
7
- def create
8
- payload = params[:_json] || parse_ndjson_body
9
-
10
- if payload.blank?
11
- render json: { error: "Empty payload" }, status: :bad_request
12
- return
13
- end
14
-
15
- entries = Array.wrap(payload)
16
-
17
- if entries.size > max_batch_size
18
- render json: {
19
- error: "Batch too large",
20
- max_size: max_batch_size,
21
- received: entries.size
22
- }, status: :payload_too_large
23
- return
24
- end
25
-
26
- # Create raw entries
27
- raw_entries = entries.map do |entry|
28
- {
29
- token_id: current_token.id,
30
- payload: entry.to_json,
31
- received_at: Time.current,
32
- parsed: false
33
- }
34
- end
35
-
36
- # Bulk insert
37
- SolidLog.without_logging do
38
- SolidLog::RawEntry.insert_all(raw_entries)
39
- end
40
-
41
- render json: {
42
- status: "accepted",
43
- count: entries.size,
44
- message: "Log entries queued for processing"
45
- }, status: :accepted
46
- rescue JSON::ParserError => e
47
- render json: {
48
- error: "Invalid JSON",
49
- message: e.message
50
- }, status: :bad_request
51
- end
52
-
53
- private
54
-
55
- def max_batch_size
56
- SolidLog.configuration.max_batch_size
57
- end
58
-
59
- # Parse NDJSON (newline-delimited JSON) from request body
60
- def parse_ndjson_body
61
- return [] unless request.body
62
-
63
- body = request.body.read
64
- return [] if body.blank?
65
-
66
- # Check if it's NDJSON (multiple lines) or regular JSON
67
- if body.include?("\n")
68
- # NDJSON format
69
- body.lines.map do |line|
70
- JSON.parse(line.strip) unless line.strip.empty?
71
- end.compact
72
- else
73
- # Regular JSON (single entry or array)
74
- JSON.parse(body)
75
- end
76
- end
77
- end
78
- end
79
- end
80
- end
@@ -1,31 +0,0 @@
1
- module SolidLog
2
- module Api
3
- module V1
4
- class SearchController < Api::BaseController
5
- # POST /api/v1/search
6
- def create
7
- query = params[:q] || params[:query]
8
-
9
- if query.blank?
10
- return render json: { error: "Query parameter required" }, status: :bad_request
11
- end
12
-
13
- search_params = {
14
- query: query,
15
- limit: params[:limit]
16
- }.compact
17
-
18
- search_service = SolidLog::SearchService.new(search_params)
19
- entries = search_service.search
20
-
21
- render json: {
22
- query: query,
23
- entries: entries.as_json(methods: [:extra_fields_hash]),
24
- total: entries.count,
25
- limit: params[:limit]&.to_i || 100
26
- }
27
- end
28
- end
29
- end
30
- end
31
- end
@@ -1,43 +0,0 @@
1
- module SolidLog
2
- module Api
3
- module V1
4
- class TimelinesController < Api::BaseController
5
- # GET /api/v1/timelines/request/:request_id
6
- def show_request
7
- request_id = params[:request_id]
8
-
9
- if request_id.blank?
10
- return render json: { error: "Request ID required" }, status: :bad_request
11
- end
12
-
13
- entries = SolidLog::CorrelationService.request_timeline(request_id)
14
- stats = SolidLog::CorrelationService.request_stats(request_id)
15
-
16
- render json: {
17
- request_id: request_id,
18
- entries: entries.as_json(methods: [:extra_fields_hash]),
19
- stats: stats
20
- }
21
- end
22
-
23
- # GET /api/v1/timelines/job/:job_id
24
- def show_job
25
- job_id = params[:job_id]
26
-
27
- if job_id.blank?
28
- return render json: { error: "Job ID required" }, status: :bad_request
29
- end
30
-
31
- entries = SolidLog::CorrelationService.job_timeline(job_id)
32
- stats = SolidLog::CorrelationService.job_stats(job_id)
33
-
34
- render json: {
35
- job_id: job_id,
36
- entries: entries.as_json(methods: [:extra_fields_hash]),
37
- stats: stats
38
- }
39
- end
40
- end
41
- end
42
- end
43
- end
@@ -1,4 +0,0 @@
1
- module SolidLog
2
- class ApplicationJob < ActiveJob::Base
3
- end
4
- end
@@ -1,16 +0,0 @@
1
- module SolidLog
2
- class CacheCleanupJob < ApplicationJob
3
- queue_as :default
4
-
5
- def perform
6
- SolidLog.without_logging do
7
- expired_count = FacetCache.expired.count
8
-
9
- if expired_count > 0
10
- FacetCache.cleanup_expired!
11
- Rails.logger.info "SolidLog::CacheCleanupJob: Cleaned up #{expired_count} expired cache entries"
12
- end
13
- end
14
- end
15
- end
16
- end
@@ -1,26 +0,0 @@
1
- module SolidLog
2
- class FieldAnalysisJob < ApplicationJob
3
- queue_as :default
4
-
5
- def perform(auto_promote: false)
6
- SolidLog.without_logging do
7
- recommendations = FieldAnalyzer.analyze
8
-
9
- if recommendations.any?
10
- Rails.logger.info "SolidLog::FieldAnalysisJob: Found #{recommendations.size} fields for potential promotion"
11
-
12
- recommendations.take(10).each do |rec|
13
- Rails.logger.info " - #{rec[:field].name} (#{rec[:field].usage_count} uses, priority: #{rec[:priority]})"
14
- end
15
-
16
- if auto_promote
17
- promoted_count = FieldAnalyzer.auto_promote_candidates
18
- Rails.logger.info "SolidLog::FieldAnalysisJob: Auto-promoted #{promoted_count} fields"
19
- end
20
- else
21
- Rails.logger.info "SolidLog::FieldAnalysisJob: No fields meet promotion threshold"
22
- end
23
- end
24
- end
25
- end
26
- end