solid_log-service 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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +112 -0
- data/Rakefile +11 -0
- data/app/controllers/solid_log/api/base_controller.rb +80 -0
- data/app/controllers/solid_log/api/v1/entries_controller.rb +55 -0
- data/app/controllers/solid_log/api/v1/facets_controller.rb +41 -0
- data/app/controllers/solid_log/api/v1/health_controller.rb +29 -0
- data/app/controllers/solid_log/api/v1/ingest_controller.rb +80 -0
- data/app/controllers/solid_log/api/v1/search_controller.rb +31 -0
- data/app/controllers/solid_log/api/v1/timelines_controller.rb +43 -0
- data/app/jobs/solid_log/application_job.rb +4 -0
- data/app/jobs/solid_log/cache_cleanup_job.rb +16 -0
- data/app/jobs/solid_log/field_analysis_job.rb +26 -0
- data/app/jobs/solid_log/parser_job.rb +130 -0
- data/app/jobs/solid_log/retention_job.rb +24 -0
- data/bin/solid_log_service +75 -0
- data/config/cable.yml +11 -0
- data/config/routes.rb +26 -0
- data/config.ru +7 -0
- data/lib/solid_log/service/application.rb +72 -0
- data/lib/solid_log/service/configuration.rb +84 -0
- data/lib/solid_log/service/engine.rb +25 -0
- data/lib/solid_log/service/job_processor.rb +82 -0
- data/lib/solid_log/service/scheduler.rb +146 -0
- data/lib/solid_log/service/version.rb +5 -0
- data/lib/solid_log/service.rb +37 -0
- data/lib/solid_log-service.rb +2 -0
- metadata +244 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 4c94d0fde51404dd97f27e8d437a9c9edab5366ba360bbd2719b0a658a28c5fe
|
|
4
|
+
data.tar.gz: 1addb2c0e3b2471fdadccbd3741905747679b1e6475e3f6a12f6a5f3a44f5193
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 8e3ab660ee56208d357bc6bf5baf5e81e89137df79b9fe5849fd8d3567934529335a77c264ae6de75432d07c3b1e1611827d2aa4c61fbba4c7b98edeebaccc72
|
|
7
|
+
data.tar.gz: 7edb569df3716558642f22c9a55d680eb2f022ff6832a8364ead35824959b1354873dab3caa2b6039c5aa5430545c005ca480dbc1f26bafc5ad4c343c79bc7f6
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright Dan Loman
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# SolidLog::Service
|
|
2
|
+
|
|
3
|
+
Standalone log ingestion and processing service with HTTP API and built-in job scheduler.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
`solid_log-service` provides:
|
|
8
|
+
|
|
9
|
+
- **HTTP Ingestion API**: Accept logs via POST with bearer token auth
|
|
10
|
+
- **Query APIs**: REST endpoints for searching, filtering, and retrieving logs
|
|
11
|
+
- **Background Processing**: Parse raw logs, retention cleanup, field analysis
|
|
12
|
+
- **Built-in Scheduler**: No external dependencies (or use ActiveJob/cron)
|
|
13
|
+
- **Health Monitoring**: Metrics endpoint for observability
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
gem 'solid_log-service'
|
|
19
|
+
|
|
20
|
+
# Database adapter (choose one)
|
|
21
|
+
gem 'sqlite3', '>= 2.1' # For SQLite (recommended for most deployments)
|
|
22
|
+
# OR
|
|
23
|
+
gem 'pg', '>= 1.1' # For PostgreSQL
|
|
24
|
+
# OR
|
|
25
|
+
gem 'mysql2', '>= 0.5' # For MySQL
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Standalone Deployment
|
|
29
|
+
|
|
30
|
+
**1. Create configuration file:**
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
# config/solid_log_service.rb
|
|
34
|
+
SolidLog::Service.configure do |config|
|
|
35
|
+
config.database_url = ENV['DATABASE_URL'] || 'sqlite3:///data/production_log.sqlite'
|
|
36
|
+
|
|
37
|
+
# Job processing mode (default: :scheduler)
|
|
38
|
+
config.job_mode = :scheduler # or :active_job, :manual
|
|
39
|
+
|
|
40
|
+
# Scheduler intervals (only used when job_mode = :scheduler)
|
|
41
|
+
config.parser_interval = 10.seconds
|
|
42
|
+
config.cache_cleanup_interval = 1.hour
|
|
43
|
+
config.retention_hour = 2 # Run at 2 AM
|
|
44
|
+
config.field_analysis_hour = 3 # Run at 3 AM
|
|
45
|
+
|
|
46
|
+
# Retention policies
|
|
47
|
+
config.retention_days = 30
|
|
48
|
+
config.error_retention_days = 90
|
|
49
|
+
end
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**2. Run the service:**
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
bundle exec solid_log_service
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Or with a Procfile:
|
|
59
|
+
```
|
|
60
|
+
service: bundle exec solid_log_service
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Kamal Deployment
|
|
64
|
+
|
|
65
|
+
See main SolidLog documentation for Kamal deployment examples.
|
|
66
|
+
|
|
67
|
+
## API Endpoints
|
|
68
|
+
|
|
69
|
+
### Ingestion
|
|
70
|
+
- `POST /api/v1/ingest` - Ingest logs (single or batch)
|
|
71
|
+
|
|
72
|
+
### Queries
|
|
73
|
+
- `GET /api/v1/entries` - List/filter entries
|
|
74
|
+
- `GET /api/v1/entries/:id` - Get single entry
|
|
75
|
+
- `GET /api/v1/search` - Full-text search
|
|
76
|
+
- `GET /api/v1/facets` - Get filter options
|
|
77
|
+
- `GET /api/v1/timelines/request/:id` - Request timeline
|
|
78
|
+
- `GET /api/v1/timelines/job/:id` - Job timeline
|
|
79
|
+
- `GET /api/v1/health` - Health metrics
|
|
80
|
+
|
|
81
|
+
## Job Processing Modes
|
|
82
|
+
|
|
83
|
+
### Built-in Scheduler (Default)
|
|
84
|
+
No external dependencies. Runs jobs in background threads.
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
config.job_mode = :scheduler
|
|
88
|
+
config.parser_interval = 10.seconds
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### ActiveJob Integration
|
|
92
|
+
Leverages host app's job backend (Solid Queue, Sidekiq, etc.)
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
config.job_mode = :active_job
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Manual (Cron)
|
|
99
|
+
You manage scheduling via cron.
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
config.job_mode = :manual
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Then in crontab:
|
|
106
|
+
```
|
|
107
|
+
*/1 * * * * cd /app && rails solid_log:parse_logs
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
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,80 @@
|
|
|
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
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
|
@@ -0,0 +1,41 @@
|
|
|
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
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
|
@@ -0,0 +1,80 @@
|
|
|
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
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|