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_bcc_config, only: :index
before_action :load_reminder_config, only: :index before_action :load_reminder_config, only: :index
before_action :load_pending_reminders, only: :index before_action :load_pending_reminders, only: :index
before_action :load_paperless_status, only: :index
authorize_resource :bcc_config, only: :index authorize_resource :bcc_config, only: :index
authorize_resource :reminder_config, only: :index authorize_resource :reminder_config, only: :index
@ -73,6 +74,12 @@ class NotificationsSettingsController < ApplicationController
@pending_reminders.sort_by! { |r| r[:next_at] } @pending_reminders.sort_by! { |r| r[:next_at] }
end 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 def email_config_params
params.require(:account_config).permit(:key, :value, { value: {} }, { value: [] }).tap do |attrs| 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]) attrs[:key] = nil unless attrs[:key].in?([AccountConfig::BCC_EMAILS, AccountConfig::SUBMITTER_REMINDERS])

@ -16,7 +16,14 @@ class UploadToPaperlessJob
attempt = params['attempt'].to_i 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 rescue Submissions::UploadToPaperless::UploadError, Faraday::Error => e
return if attempt >= MAX_ATTEMPTS 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 %> <% end %>
</div> </div>
<%= render 'bcc_form', config: @bcc_config %> <%= render 'bcc_form', config: @bcc_config %>
<%= render 'paperless_status' %>
<div class="flex justify-between items-end mb-4 mt-8"> <div class="flex justify-between items-end mb-4 mt-8">
<h2 class="text-3xl font-bold"> <h2 class="text-3xl font-bold">
<%= t('sign_request_email_reminders') %> <%= 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? ENV['PAPERLESS_NGX_URL'].present? && ENV['PAPERLESS_NGX_TOKEN'].present?
end 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) def documents_to_upload(submission, title)
documents = [] documents = []

@ -205,4 +205,75 @@ RSpec.describe Submissions::UploadToPaperless do
end end
end 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 end

@ -69,6 +69,59 @@ RSpec.describe 'Notifications Settings' do
end end
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 context 'when changes sign request email reminders settings' do
it 'updates first reminder duration' do it 'updates first reminder duration' do
visit settings_notifications_path visit settings_notifications_path

Loading…
Cancel
Save