Skip to main content

Service and logs

The host-side monitoring surface — what an ops engineer / SRE looks at when the web UI isn't enough or when wiring external monitoring. Covers the wrapper's status (works the same on Linux / macOS via nomaubl.sh and on Windows via nomaubl.cmd), the log file's structure, the two HTTP endpoints meant for probes and the patterns to push alerts when something goes wrong.


status — the quick health check​

Linux / macOS
./nomaubl.sh status
# NomaUBL [demo] is running (PID 12345)
# NomaUBL [uat] is running (PID 12346)
# NomaUBL [prod] is running (PID 12347)
Windows
nomaubl.cmd status
REM [demo] running (PID 12345)
REM [uat] running (PID 12346)
REM [prod] running (PID 12347)

Without arguments, the wrapper lists every nomaubl-*.pid file next to the JAR and prints the live state of each. With an environment name, it reports just that one:

Linux / macOS
./nomaubl.sh status prod
# NomaUBL [prod] is running (PID 12347)
Windows
nomaubl.cmd status prod
REM NomaUBL [prod] is running (PID 12347)

The wrapper also prunes stale PID files — entries for processes that are no longer alive are removed. So a single status call cleans up after itself.

Exit codes:

CodeMeaning
0Every named env is running.
1At least one env is down.
2The named env's PID file doesn't exist (Linux / macOS).

Useful in scripts and systemd ExecStartPre:

Linux / macOS
./nomaubl.sh status prod >/dev/null || { echo "prod is down"; exit 1; }
Windows
nomaubl.cmd status prod >NUL || ( echo prod is down & exit /b 1 )

The log file — nomaubl-<env>.log​

Each environment writes its stderr + stdout to a single log file next to the JAR:

Linux / macOS
/opt/nomaubl/
├── nomaubl.jar
├── nomaubl.sh
├── nomaubl-demo.log
├── nomaubl-uat.log
└── nomaubl-prod.log
Windows
C:\nomaubl\
├── nomaubl-fat.jar
├── nomaubl.cmd
├── nomaubl-demo.log
├── nomaubl-uat.log
└── nomaubl-prod.log

Tail it:

Linux / macOS
./nomaubl.sh log demo
# or directly
tail -f /opt/nomaubl/nomaubl-demo.log
Windows
nomaubl.cmd log demo
REM uses PowerShell Get-Content -Wait under the hood

Lines you'll see often​

Line shapeWhat it means
[INFO] Loaded config: /opt/nomaubl/demo/config/config.jsonService started, config parsed.
[INFO] Master key file: /opt/nomaubl/demo/.nomaubl-master.keyThe master-key file was found and loaded.
[INFO] Connected to db-nomaubl as nomaublJDBC works.
[INFO] Schema bootstrap: 23 tables createdFirst start on an empty schema — F564* tables landed.
[INFO] HTTP server listening on :8090Web UI is up.
[INFO] Scheduler started — fetchImportInterval=15min fetchStatusInterval=15min fetchAllInterval=0Background scheduler intervals at startup.
[INFO] scheduler tick · fetch-import · 0 invoices to importRoutine scheduler tick.
[INFO] pa-default API call · import · status 200 · 1.2 sOne successful PA submission.
[WARN] pa-default API call · import · status 429 · retrying in 30sPA rate-limit; will retry.
[ERROR] Connection refused — jdbc:oracle:thin:... (or jdbc:postgresql://...)Database unreachable — diagnose at the network / DB side.

The format is line-based, level-prefixed. Easy to grep, easy to ship.

Common diagnostic flows​

QuestionLinux / macOSWindows
Did the last scheduler tick run?grep "scheduler tick" nomaubl-prod.log | tail -10Select-String -Path nomaubl-prod.log -Pattern "scheduler tick" | Select-Object -Last 10
Have there been PA failures recently?grep "pa-default" nomaubl-prod.log | grep -v "status 200"Select-String -Path nomaubl-prod.log -Pattern "pa-default" | Where-Object { $_ -notmatch "status 200" }
What was the last error?grep "\[ERROR\]" nomaubl-prod.log | tail -1Select-String -Path nomaubl-prod.log -Pattern "\[ERROR\]" | Select-Object -Last 1
Did the service start cleanly today?grep -A 5 "HTTP server listening" nomaubl-prod.log | tail -10Select-String -Path nomaubl-prod.log -Pattern "HTTP server listening" -Context 0,5 | Select-Object -Last 10

Verbose mode​

Some CLI modes accept --verbose to emit per-document lines (one log line per invoice). On heavy nightly batches this is off by default to keep the log readable; turn on per-run for investigation.


HTTP endpoints — /api/build-info and /api/license​

The two unauthenticated endpoints meant for machine probes:

/api/build-info​

curl http://localhost:8090/api/build-info
{
"version": "2026.05.26",
"buildDate": "2026-05-26T14:33:00Z",
"buildHash": "a8c4d1f2…"
}
FieldWhat it tells you
versionRelease tag (matches the JAR filename / release notes).
buildDateWhen the JAR was built.
buildHashGit short SHA — useful when correlating with a specific deployment.

Useful for:

  • Post-deploy smoke test — assert the version matches what you deployed.
  • Liveness probe — non-200 = service down.
  • Multi-instance audit — confirm every environment runs the expected version.

/api/license​

curl http://localhost:8090/api/license

Sample responses:

{ "mode": "full", "customer": "ACME Corp", "expiresAt": "2026-12-31" }
{ "mode": "restricted", "reason": "License expired on 2026-04-30" }
{ "mode": "restricted", "reason": "No license configured" }
FieldWhat it tells you
modefull (everything works) or restricted (limited functionality without a license).
customerSubject of the license (only when mode = full).
expiresAtLicense expiry date.
reasonWhy the license isn't loaded (only when mode = restricted).

Useful for:

  • Daily license check — alert when mode flips to restricted (somebody's license expired or wasn't installed).
  • Pre-deploy validation — confirm the new env has its license before going live.

What these endpoints are NOT​

MisconceptionReality
/api/build-info is a health check.It returns build metadata, not health state. The HTTP status code (200) is your liveness signal.
/api/license is rate-limited.No — poll as often as your probe cadence requires. Each call hits an in-memory cache, not the database.
They expose secrets.No — the build hash + customer name + expiry date are not secrets. No passwords, no tokens.
They respect permissions.No — unauthenticated. Restrict at the reverse proxy if your install is public-facing.

Scheduler — confirm it's ticking​

The background scheduler runs inside the -serve process. There's no separate scheduler daemon and no separate UI to confirm it's alive — the log file is the source of truth.

grep "scheduler tick" /opt/nomaubl/nomaubl-prod.log | tail -10

If the last tick was more than fetchImportInterval + fetchStatusInterval + fetchAllInterval minutes ago, the scheduler is probably stuck. The fixes:

SymptomCauseFix
No scheduler tick lines at all.All three intervals set to 0.Set non-zero intervals in global template's fetchImportInterval etc.
Ticks fire but never advance invoices.The PA template (pa-default) is unreachable — each call times out.Test from the host: curl https://<pa-base-url>/api/login_check.
Ticks fire occasionally then stop for hours.The JVM hit an OOM and the scheduler thread died without restarting the JVM.Check the log for OutOfMemoryError; bump -Xmx; consider Restart=on-failure in systemd (covered in Service and systemd).

Force a tick from the CLI​

When investigating, run the sweep manually:

./nomaubl.sh fetch-status prod
# → runs one synchronous -fetch-status pass; output goes to stdout

Both fetch-import and fetch-status are safe to run on demand — they're idempotent against the PA.


External monitoring patterns​

NomaUBL doesn't export Prometheus metrics or OpenTelemetry traces. The three patterns that cover most real installs:

Pattern A — Log-based alerting​

The single highest-leverage monitoring. Pipe the log file to your central log store (Loki / ELK / Splunk / Datadog Logs / Cloudwatch), then build alerts on log patterns:

Log patternAlert
\[ERROR\]Page on-call for any ERROR; throttle to N per hour.
\[WARN\] pa-default .* status 5..PA-side failures; page if persistent over 5 minutes.
OutOfMemoryErrorPage immediately.
No scheduler tick lines for > 30 minScheduler is dead.
Connection refused.*jdbc:(oracle|postgresql)Database unreachable.
# Example logrotate + filebeat config snippet
- type: log
paths:
- /opt/nomaubl/nomaubl-*.log
fields:
service: nomaubl
env: prod

Pattern B — Endpoint polling​

A small cron script that hits both endpoints and pages on failure:

#!/usr/bin/env bash
# /usr/local/bin/nomaubl-probe.sh
set -e
for env in demo uat prod; do
port=$(case "$env" in demo) echo 8090;; uat) echo 8091;; prod) echo 8092;; esac)
# Liveness
curl -fsS http://localhost:$port/api/build-info > /dev/null \
|| { echo "[$env] DOWN" | mail -s "NomaUBL $env down" ops@example.com; exit 1; }
# License
mode=$(curl -fsS http://localhost:$port/api/license | jq -r '.mode')
if [ "$mode" != "full" ]; then
echo "[$env] License mode=$mode" | mail -s "NomaUBL $env license alert" ops@example.com
fi
done

Schedule via systemd timer / cron (Linux / macOS) or Windows Task Scheduler — */5 * * * * (or a /SC MINUTE /MO 5 task) is a reasonable cadence.

Pattern C — Host + JVM metrics​

For deeper observability:

ToolWhat it captures
node_exporterHost CPU, memory, disk, network — every running NomaUBL process counts as one Java process.
process_exporterPer-process CPU / RSS — track each nomaubl-<env> separately.
JMX exporter (Prometheus JVM exporter as a Java agent)JVM-internal metrics — heap, GC, thread count. Add -javaagent:/opt/jmx_prometheus_javaagent.jar=9404:config.yml to the JAR command.
DB exporter (Oracle exporter, postgres_exporter, …)DB-side metrics — active sessions, top SQL, tablespace usage.

These four together give 95% of the observability a production install needs. Wire them into the same Prometheus / Grafana stack you already operate.


Log forwarding — keep history off-host​

The wrapper writes everything to nomaubl-<env>.log next to the JAR. For long-term retention + searchability:

SetupWhat
Filebeat → Logstash / Loki / CloudwatchThe standard pattern. One line per JSON record — Filebeat reads, ships, you query in Kibana / Grafana / Cloudwatch Logs Insights.
VectorLighter-weight alternative.
Custom — journalctl if running under systemdsystemd captures stderr / stdout into the journal; journalctl -u nomaubl@prod searches it.
logger for one-off pipes`tail -f nomaubl-prod.log

Logrotate (copytruncate, see Service and systemd) ensures the file doesn't grow unbounded. A typical retention: 8 weeks rotated locally + indefinite retention in the central log store.


Common ops pitfalls​

MistakeSymptomFix
Watching the Tech Dashboard but ignoring the log file.A scheduler hang shows up in logs hours before the dashboard reflects "no new invoices today".Wire log-based alerting (Pattern A above).
Polling /api/build-info every second.Pointless load + noise in access logs.30 s is more than enough.
Treating /api/license restricted mode as a hard failure.Service still serves licensed features in restricted mode for grace period configurations; alarms wake people up unnecessarily.Read the reason field; alert on expired / revoked; tolerate restricted only when expected.
Ignoring OutOfMemoryError in the log.The JVM continues serving but the scheduler thread died — no batch is fired.Set Restart=on-failure in systemd; bump -Xmx.
Logrotate without copytruncate.The JVM keeps writing to the deleted inode — the new log file stays empty.copytruncate in the logrotate config.
No PID file → systemd reports the service as dead while the JVM is actually running.systemd kills the JVM on the next restart.Always go through nomaubl.sh start (writes the PID file) or set PIDFile=... in the systemd unit.

What's next​