feat: add paperless-ngx observability — startup logging, job logging, settings UI status

- Add health_check method to Submissions::UploadToPaperless (GET /api/ with 3s timeout)
- Add Rails initializer that logs connection status at boot
- Add INFO logging to UploadToPaperlessJob (start, success with doc count/task IDs)
- Add paperless-ngx status section to Settings → Notifications page
  (live connection check with 60s cache, DaisyUI badges: Connected/Unreachable/Not Configured)
- Add specs for health_check method (5 scenarios) and system specs (3 scenarios)
pull/681/head
Sebastian Noe 1 month ago
parent d767e8a030
commit fe87513416

@ -4,6 +4,7 @@ class NotificationsSettingsController < ApplicationController
before_action :load_bcc_config, only: :index
before_action :load_reminder_config, only: :index
before_action :load_pending_reminders, only: :index
before_action :load_paperless_status, only: :index
authorize_resource :bcc_config, only: :index
authorize_resource :reminder_config, only: :index
@ -73,6 +74,12 @@ class NotificationsSettingsController < ApplicationController
@pending_reminders.sort_by! { |r| r[:next_at] }
end
def load_paperless_status
@paperless_status = Rails.cache.fetch('paperless_ngx_health_check', expires_in: 60.seconds) do
Submissions::UploadToPaperless.health_check
end
end
def email_config_params
params.require(:account_config).permit(:key, :value, { value: {} }, { value: [] }).tap do |attrs|
attrs[:key] = nil unless attrs[:key].in?([AccountConfig::BCC_EMAILS, AccountConfig::SUBMITTER_REMINDERS])

@ -16,7 +16,14 @@ class UploadToPaperlessJob
attempt = params['attempt'].to_i
Submissions::UploadToPaperless.call(submission)
Rails.logger.info("[Paperless-ngx] Uploading documents for submission #{submission.id}")
results = Submissions::UploadToPaperless.call(submission)
if results
Rails.logger.info("[Paperless-ngx] Upload complete for submission #{submission.id}: " \
"#{results.size} document(s), task IDs: #{results.join(', ')}")
end
rescue Submissions::UploadToPaperless::UploadError, Faraday::Error => e
return if attempt >= MAX_ATTEMPTS

@ -0,0 +1,28 @@
<div class="mt-8 mb-4">
<h2 class="text-3xl font-bold mb-4">
Paperless-ngx
</h2>
<div class="flex items-center space-x-3">
<% if !@paperless_status[:configured] %>
<span class="badge badge-ghost">Not Configured</span>
<span class="text-sm opacity-70">
Set PAPERLESS_NGX_URL and PAPERLESS_NGX_TOKEN environment variables to enable.
</span>
<% elsif @paperless_status[:reachable] %>
<span class="badge badge-success">Connected</span>
<span class="text-sm opacity-70">
<%= @paperless_status[:url] %>
</span>
<% else %>
<span class="badge badge-error">Unreachable</span>
<span class="text-sm opacity-70">
<%= @paperless_status[:url] %> — <%= @paperless_status[:error] %>
</span>
<% end %>
</div>
<% if @paperless_status[:configured] %>
<p class="text-sm opacity-70 mt-2">
Signed documents are automatically uploaded to Paperless-ngx when all parties complete signing.
</p>
<% end %>
</div>

@ -21,6 +21,7 @@
<% end %>
</div>
<%= render 'bcc_form', config: @bcc_config %>
<%= render 'paperless_status' %>
<div class="flex justify-between items-end mb-4 mt-8">
<h2 class="text-3xl font-bold">
<%= t('sign_request_email_reminders') %>

@ -0,0 +1,15 @@
# frozen_string_literal: true
Rails.application.config.after_initialize do
status = Submissions::UploadToPaperless.health_check
if !status[:configured]
Rails.logger.info('[Paperless-ngx] Integration not configured (PAPERLESS_NGX_URL / PAPERLESS_NGX_TOKEN not set)')
elsif status[:reachable]
Rails.logger.info("[Paperless-ngx] Connected to #{status[:url]}")
else
Rails.logger.warn("[Paperless-ngx] Configured but unreachable at #{status[:url]}: #{status[:error]}")
end
rescue StandardError => e
Rails.logger.warn("[Paperless-ngx] Health check failed during startup: #{e.message}")
end

@ -26,6 +26,25 @@ module Submissions
ENV['PAPERLESS_NGX_URL'].present? && ENV['PAPERLESS_NGX_TOKEN'].present?
end
def health_check
return { configured: false, reachable: false, url: nil, error: nil } unless configured?
url = ENV['PAPERLESS_NGX_URL'] # rubocop:disable Style/FetchEnvVar
response = connection.get('/api/') do |req|
req.headers['Authorization'] = "Token #{ENV['PAPERLESS_NGX_TOKEN']}" # rubocop:disable Style/FetchEnvVar
req.options.timeout = 3
req.options.open_timeout = 3
end
if response.status < 400
{ configured: true, reachable: true, url: url, error: nil }
else
{ configured: true, reachable: false, url: url, error: "HTTP #{response.status}" }
end
rescue Faraday::Error => e
{ configured: true, reachable: false, url: url, error: e.message }
end
def documents_to_upload(submission, title)
documents = []

@ -205,4 +205,75 @@ RSpec.describe Submissions::UploadToPaperless do
end
end
end
describe '.health_check' do
context 'when not configured' do
before do
allow(ENV).to receive(:[]).with('PAPERLESS_NGX_URL').and_return(nil)
allow(ENV).to receive(:[]).with('PAPERLESS_NGX_TOKEN').and_return(nil)
end
it 'returns configured false with no error' do
result = described_class.health_check
expect(result).to eq(configured: false, reachable: false, url: nil, error: nil)
end
end
context 'when configured and reachable' do
before do
stub_request(:get, "#{paperless_url}/api/")
.with(headers: { 'Authorization' => "Token #{paperless_token}" })
.to_return(status: 200, body: '{"version": "2.0"}')
end
it 'returns configured and reachable with the URL' do
result = described_class.health_check
expect(result).to eq(configured: true, reachable: true, url: paperless_url, error: nil)
end
end
context 'when configured but server returns error' do
before do
stub_request(:get, "#{paperless_url}/api/")
.to_return(status: 500, body: 'Internal Server Error')
end
it 'returns configured but unreachable with HTTP status error' do
result = described_class.health_check
expect(result).to eq(configured: true, reachable: false, url: paperless_url, error: 'HTTP 500')
end
end
context 'when configured but connection times out' do
before do
stub_request(:get, "#{paperless_url}/api/")
.to_timeout
end
it 'returns configured but unreachable with timeout error' do
result = described_class.health_check
expect(result[:configured]).to be true
expect(result[:reachable]).to be false
expect(result[:url]).to eq(paperless_url)
expect(result[:error]).to be_present
end
end
context 'when configured but connection refused' do
before do
stub_request(:get, "#{paperless_url}/api/")
.to_raise(Faraday::ConnectionFailed.new('Connection refused'))
end
it 'returns configured but unreachable with connection error' do
result = described_class.health_check
expect(result).to eq(configured: true, reachable: false, url: paperless_url, error: 'Connection refused')
end
end
end
end

@ -69,6 +69,59 @@ RSpec.describe 'Notifications Settings' do
end
end
context 'when paperless-ngx is not configured' do
before do
allow(ENV).to receive(:[]).and_call_original
allow(ENV).to receive(:[]).with('PAPERLESS_NGX_URL').and_return(nil)
allow(ENV).to receive(:[]).with('PAPERLESS_NGX_TOKEN').and_return(nil)
end
it 'shows not configured status' do
visit settings_notifications_path
expect(page).to have_content('Paperless-ngx')
expect(page).to have_content('Not Configured')
end
end
context 'when paperless-ngx is configured and reachable' do
before do
allow(ENV).to receive(:[]).and_call_original
allow(ENV).to receive(:[]).with('PAPERLESS_NGX_URL').and_return('http://paperless:8000')
allow(ENV).to receive(:[]).with('PAPERLESS_NGX_TOKEN').and_return('test-token')
stub_request(:get, 'http://paperless:8000/api/')
.to_return(status: 200, body: '{}')
end
it 'shows connected status with URL' do
visit settings_notifications_path
expect(page).to have_content('Paperless-ngx')
expect(page).to have_content('Connected')
expect(page).to have_content('http://paperless:8000')
end
end
context 'when paperless-ngx is configured but unreachable' do
before do
allow(ENV).to receive(:[]).and_call_original
allow(ENV).to receive(:[]).with('PAPERLESS_NGX_URL').and_return('http://paperless:8000')
allow(ENV).to receive(:[]).with('PAPERLESS_NGX_TOKEN').and_return('test-token')
stub_request(:get, 'http://paperless:8000/api/')
.to_return(status: 500, body: 'Internal Server Error')
end
it 'shows unreachable status with error' do
visit settings_notifications_path
expect(page).to have_content('Paperless-ngx')
expect(page).to have_content('Unreachable')
expect(page).to have_content('HTTP 500')
end
end
context 'when changes sign request email reminders settings' do
it 'updates first reminder duration' do
visit settings_notifications_path

Loading…
Cancel
Save