vicigeeksimple guides
Browse
All guides

Reports & results · Monitoring setup

How to monitor VICIdial SFTP pushes with append-only JSONL

Validate an explicit run/event log contract, reproduce six offline failure cases, then check an approved stable log after its completion deadline; recipient receipt remains separate.

Reader setup

Before you start

  1. Python 3 on a local workstation for the fictional model; no VICIdial or SFTP account is required for that exercise.
  2. For an operational read, the owner’s actual log path, expected completion deadline/cadence and documented adapter to the three-field example schema.
  3. An approved read-only monitoring account and the existing log-rotation/reopen owner.
Expected result
A scoped log validation and monitoring plan, with delivery checked separately.
Before making changes
Keep log access restricted and avoid copying sensitive payloads into tickets.

01 / 08

How this guide watches the push script

Identify the actual delivery job and log path, then read its documented fields and expected events. A JSON Lines file contains one JSON value per line.

Check syntax and access separately from remote receipt. Rotation changes configuration, so test it on a disposable log before applying a policy.

  • Record the actual log path and documented event schema.
  • Use a dedicated read-only monitor account.
  • Compare local outcomes with recipient-side confirmation.
Trace path · read left to right
01Root-only push script02Read-only JSONL checks03Rotation without data loss

02 / 08

Step 1 — Confirm the log exists

This guide does not install a delivery job. Look up the actual JSONL path in the deployed push-job configuration; the example path below is illustrative, not a stock VICIdial path. Inspect file metadata privately.

Before you use it
Replace <ACTUAL_JSONL_PATH> with the deployed job’s log path. No file is created by this check.
Confirm the log file and its rough size
sudo ls -l -- '<ACTUAL_JSONL_PATH>'
Reader-run example · not executed for this guide

This example has no recorded result here. Follow its preparation, expected result and stop conditions before using it.

Success looks like
A listing shows the file metadata; it does not prove delivery.
Stop if
If ls fails, verify the configured path and access with the job owner; the error alone does not prove whether a job ran.

03 / 08

2. Reproduce the six run-health cases locally

This teaching schema contains exactly run_id,at and event. at is timezone-aware UTC; event is started,completed or failed. Each run needs one start and one terminal result in order. completed means the local job reported completion; it is not recipient-side integrity confirmation.

The fixed as-of time is2026-11-07T12:00:00Z. These fixture runs are already past their expected completion deadline; the model is not an in-progress polling policy. A fresh log can still contain a failed or missing-terminal run.

Run the complete model below. It rejects stale history, missing completion, explicit failure, duplicate terminal events and invalid JSON. These are real local checks of fictional lines, not a transfer test.

  1. Copy the complete fixed Python model into a local terminal or file and run it with Python3. Keep its fictional lines and fixed as-of time unchanged for this first check; it does not read your delivery log.
  2. Read each named case in the actual stdout: complete must pass the local contract; missing,failed,duplicate,stale and malformed JSON must each produce the stated rejection category. Check the process exits successfully after all assertions.
  3. Compare the results with the retained local recording. If any assertion differs,save the exact code,stdout and Python version and stop before adapting this reader to a real job.
  4. Only after the six cases agree,document the real producer’s field adapter,cadence and completion deadline. Keep recipient-side file/hash confirmation separate from the log contract.
Before you use it
Run locally with Python 3. No log file, delivery process or remote destination is read.
Run six fictional JSONL monitor cases
python3 - <<'PYMODEL'import jsonfrom datetime import datetime,timezoneASOF=datetime.fromisoformat("2026-11-07T12:00:00+00:00")def check(lines):    runs={}; times=[]    try:        for line in lines:            row=json.loads(line)            if set(row)!={"run_id","at","event"}: return "invalid schema"            rid,event=row["run_id"],row["event"]            if not isinstance(rid,str) or not rid or event not in {"started","completed","failed"}:return "invalid schema"            at=datetime.fromisoformat(row["at"].replace("Z","+00:00"))            if at.tzinfo is None or at>ASOF:return "invalid time"            events=runs.setdefault(rid,[])            if events and at<events[-1][1]:return "out of order"            events.append((event,at)); times.append(at)    except (ValueError,TypeError,KeyError):return "invalid JSON/schema"    if not times or (ASOF-max(times)).total_seconds()>3600:return "stale"    for events in runs.values():        names=[e[0] for e in events]        if names.count("started")!=1 or names[0]!="started":return "invalid start"        if sum(e in {"completed","failed"} for e in names)>1:return "duplicate terminal"        if "failed" in names:return "failed run"        if names[-1]!="completed":return "missing completion"    return "pass local log contract"def row(event,at="2026-11-07T11:45:00Z"):    return json.dumps(dict(run_id="fixtureA",at=at,event=event))cases=[("complete",[row("started"),row("completed")],"pass local log contract"),       ("missing",[row("started")],"missing completion"),       ("error",[row("started"),row("failed")],"failed run"),       ("duplicate",[row("started"),row("completed"),row("completed")],"duplicate terminal"),       ("stale",[row("started","2026-11-07T09:00:00Z"),row("completed","2026-11-07T09:01:00Z")],"stale"),       ("bad-json",["not-json"],"invalid JSON/schema")]for name,lines,expected in cases:    actual=check(lines); assert actual==expected,(name,actual)    print("PASS",name,"->",actual)print("PASS 6 fictional monitor cases; no SFTP transfer or recipient receipt tested.")PYMODEL
Evidence · Local fictional fixture · actual Python execution; no PBX/database/provider operation

Captured local fixture result · 2026-11-07T20:35:30.127231+00:00. Any filtering, hidden values or test-only data are identified in the capture label. Select Replay transcript to read the recorded output.

Command output line: python3 - <<'PYMODEL' import json from datetime import datetime,timezone ASOF=datetime.fromisoformat("2026-11-07T12:00:00+00:00") def check(lines): runs={}; times=[] try: for line in lines: row=json.loads(line) if set(row)!={"run_id","at","event"}: return "invalid schema" rid,event=row["run_id"],row["event"] if not isinstance(rid,str) or not rid or event not in {"started","completed","failed"}:return "invalid schema" at=datetime.fromisoformat(row["at"].replace("Z","+00:00")) if at.tzinfo is None or at>ASOF:return "invalid time" events=runs.setdefault(rid,[]) if events and at<events[-1][1]:return "out of order" events.append((event,at)); times.append(at) except (ValueError,TypeError,KeyError):return "invalid JSON/schema" if not times or (ASOF-max(times)).total_seconds()>3600:return "stale" for events in runs.values(): names=[e[0] for e in events] if names.count("started")!=1 or names[0]!="started":return "invalid start" if sum(e in {"completed","failed"} for e in names)>1:return "duplicate terminal" if "failed" in names:return "failed run" if names[-1]!="completed":return "missing completion" return "pass local log contract" def row(event,at="2026-11-07T11:45:00Z"): return json.dumps(dict(run_id="fixtureA",at=at,event=event)) cases=[("complete",[row("started"),row("completed")],"pass local log contract"), ("missing",[row("started")],"missing completion"), ("error",[row("started"),row("failed")],"failed run"), ("duplicate",[row("started"),row("completed"),row("completed")],"duplicate terminal"), ("stale",[row("started","2026-11-07T09:00:00Z"),row("completed","2026-11-07T09:01:00Z")],"stale"), ("bad-json",["not-json"],"invalid JSON/schema")] for name,lines,expected in cases: actual=check(lines); assert actual==expected,(name,actual) print("PASS",name,"->",actual) print("PASS 6 fictional monitor cases; no SFTP transfer or recipient receipt tested.") PYMODEL
PASS complete -> pass local log contract
PASS missing -> missing completion
PASS error -> failed run
PASS duplicate -> duplicate terminal
PASS stale -> stale
PASS bad-json -> invalid JSON/schema
PASS 6 fictional monitor cases; no SFTP transfer or recipient receipt tested.
Success looks like
All six named assertions pass; the healthy fixture passes its local log contract while each negative fixture receives its expected failure category.
Stop if
A failed assertion blocks adopting the model. An operational job without this reviewed schema/deadline adapter is unsupported, not healthy.
The documented teaching contractScroll sideways to compare columns
FieldAccepted valueRequired owner adaptation
run_idOne nonempty identity per scheduled runMap a genuine stable run identity; do not use an event label as identity.
atTimezone-aware event timestampRecord source time and clock basis; reject future/ambiguous times.
eventstarted / completed / failedMap actual job phases and verify what completion means.
ScheduleOne-hour freshness in the fixtureSet actual expected cadence, run deadline and grace before alerts.

Local JSONL snapshot rejection model

Duration 0:11
Actual local fixture execution

Actual local fixture execution with fictional input. No VICIdial, database, carrier or provider operation.

Steps and result3 steps

Fictional JSONL snapshot contract at a fixed ASOF time;no SFTP transfer,live monitoring service,delivery or recipient receipt tested. Actual localhost browser capture,November7,2026;fixed source command executed by Run fixture. Video/still include ViciGeek attribution;this is not native VICIdial UI.

Observed local fixture resultSix fictional cases checked:complete,missing,failed,duplicate,stale,invalid JSON.

Read the chapter notes
  1. Local fictional scope

    Actual local fixture execution with fictional input. No VICIdial, database, carrier or provider operation.

  2. Click Run fixture

    Six fictional cases checked:complete,missing,failed,duplicate,stale,invalid JSON.

  3. Actual stdout and model limits

    Six fictional cases checked:complete,missing,failed,duplicate,stale,invalid JSON.

2026-11-07T20:35:30.127231+00:00 · Local Python fixture / actual browser run. fictional explicit log contract only Use fullscreen to read the actual local inputs and results.

View a still screenshot

Observed local fictional model

Open to enlarge
Local JSONL snapshot rejection model with the actual successful fixture output and a LOCAL FICTIONAL FIXTURE label.
Actual localhost browser capture, November 7, 2026.
Capture detailsFictional JSONL snapshot contract at a fixed ASOF time; no SFTP transfer,live monitoring service,delivery or recipient receipt tested. The Run fixture action executed the exact adjacent command; its exit code and stdout were retained. This is a local model interface, not native VICIdial UI.

You should seeSyntax alone is no longer the test: run identity, state, freshness and completion are explicit.

04 / 08

3. Read the actual adapted log after its deadline

Have the job owner verify the adapter and choose a stable completed window of at most5MB. This reader expects the exact teaching schema; it does not guess the producer’s fields. Change the maximum age only from the documented cadence and alert policy.

Run after the relevant job’s expected completion deadline plus its agreed grace. A run still legitimately in progress must not be declared a delivery failure by this snapshot reader. If an active writer or rotation changes the window, take a new approved snapshot rather than editing or truncating the live log.

Record the reader’s exit/category, as-of time and next owner. Nonzero results go to the delivery/log owner; independently ask the recipient to confirm the selected files and hashes. Never disable delivery by merely turning off this monitor.

  1. With the job owner,select the actual read-only log snapshot,verify its adapter emits exactly run_id/at/event and record the expected completion deadline plus grace. Confirm the chosen window is stable and no larger than the reader’s5MB bound.
  2. Replace only the reader’s approved snapshot path and reviewed freshness/deadline settings. Execute once as the authorized monitor account after that window’s deadline;do not reuse the fictional fixture result as evidence for this run.
  3. Record the complete stdout/stderr,exit category,as-of time and snapshot identity. For a missing/unreadable file,failed run,missing terminal,duplicate or stale result,hand off that specific boundary to its owner;do not truncate the log,widen permissions or suppress the failure.
  4. For a passing local log contract,obtain the independent recipient-side selected-file/hash confirmation before claiming delivery. For a failed or changing snapshot,retain the observation and ask the owner for a new approved window after correction;do not disable the delivery job to silence this monitor.
Before you use it
Replace the path and verify schema/cadence/deadline with the owner. Run as the authorized read-only monitor account; this is not the offline fixture capture.
Read one scoped stable JSONL snapshot
(  LOG_PATH='/REPLACE/WITH/APPROVED/STABLE/JSONL'  MAX_AGE_SECONDS=3600  python3 - "$LOG_PATH" "$MAX_AGE_SECONDS" <<'PYLOG'import json,sys,pathlibfrom datetime import datetime,timezoneASOF=datetime.now(timezone.utc)MAX_AGE=int(sys.argv[2])if MAX_AGE<=0: raise SystemExit("STOP: freshness must be positive")def check(lines):    runs={}; times=[]    try:        for line in lines:            row=json.loads(line)            if not isinstance(row,dict) or not isinstance(row.get("at"),str): return "invalid schema"            if set(row)!={"run_id","at","event"}: return "invalid schema"            rid,event=row["run_id"],row["event"]            if not isinstance(rid,str) or not rid or event not in {"started","completed","failed"}:return "invalid schema"            at=datetime.fromisoformat(row["at"].replace("Z","+00:00"))            if at.tzinfo is None or at>ASOF:return "invalid time"            events=runs.setdefault(rid,[])            if events and at<events[-1][1]:return "out of order"            events.append((event,at)); times.append(at)    except (ValueError,TypeError,KeyError):return "invalid JSON/schema"    if not times or (ASOF-max(times)).total_seconds()>MAX_AGE:return "stale"    for events in runs.values():        names=[e[0] for e in events]        if names.count("started")!=1 or names[0]!="started":return "invalid start"        if sum(e in {"completed","failed"} for e in names)>1:return "duplicate terminal"        if "failed" in names:return "failed run"        if names[-1]!="completed":return "missing completion"    return "pass local log contract"path=pathlib.Path(sys.argv[1])try:    if path.stat().st_size>5_000_000:        raise SystemExit('STOP: log exceeds the reviewed5MB bound; ask owner for a bounded completed window')    with path.open(encoding='utf-8') as log:        status=check(line for line in log if line.strip())except OSError as error:    raise SystemExit('STOP: '+type(error).__name__+'; check the actual path/account with its owner')print(status)raise SystemExit(0 if status=='pass local log contract' else 1) PYLOG)
Reader-run example · not executed for this guide

This example has no recorded result here. Follow its preparation, expected result and stop conditions before using it.

Success looks like
Exit0 means only that this selected log window passes the declared local contract. Compare recipient evidence separately.
Stop if
Nonzero distinguishes schema/state/freshness failures; FileNotFoundError and PermissionError remain distinct. Hand off the category; do not suppress errors or widen access.

You should seeAn operational result has a documented schema, deadline, freshness rule and response owner.

05 / 08

4. Confirm the file and directory access boundary

Inspect the file and parent directory ownership, permissions and applicable access controls. Test the monitoring account on a disposable copy; do not attempt to append to a live operational log.

Before you use it
Replace the path and use the approved monitoring account. Keep errors visible; a permission error does not mean the file has not been created.
Confirm log ownership and mode
stat -c '%U %a %n' /REPLACE/WITH/ACTUAL/LOG
Reader-run example · not executed for this guide

This example has no recorded result here. Follow its preparation, expected result and stop conditions before using it.

Success looks like
Compare owner and mode with the intended access policy; stat alone does not account for ACLs or parent-directory permissions.
Stop if
Inspect file and directory permissions and ACLs if access differs from the intended policy.

06 / 08

5. Test rotation against the writer’s actual reopen behavior

Plan a policy for the actual log path and the writer’s reopen behavior. Test the policy against a disposable file before deploying it; rotation and retention are configuration changes.

Before you use it
Adapt this illustrative stanza to the real log and retention policy; do not overwrite an existing logrotate policy.
Rotation policy for the push log
/var/log/vicidial-sales-sftp/push.jsonl {  weekly  rotate 12  missingok  notifempty  compress  delaycompress}
Not executed · worksheet or reference text

This sample is a template or reading aid, not a terminal command. There is no output to show.

Success looks like
A disposable-file test confirms the writer can continue logging after rotation and the retention policy behaves as intended.
Stop if
Stop if the writer loses logs, another policy handles the same path, or the test affects unrelated files.

07 / 08

6. Compare stable read evidence without hiding failures

During a quiet test window, record the hash and count, read the file, then compare them again. An active writer or rotation can legitimately change the file during the test; investigate timing rather than attributing a change to the reader.

Before you use it
Use the same approved stable snapshot before and after reading. Preserve command errors; an active writer/rotation can change the file legitimately.
Capture before-and-after no-write evidence
sha256sum /REPLACE/WITH/ACTUAL/STABLE/LOGwc -l /REPLACE/WITH/ACTUAL/STABLE/LOG
Reader-run example · not executed for this guide

This example has no recorded result here. Follow its preparation, expected result and stop conditions before using it.

Success looks like
If the file stayed unchanged during the test, its hash and line count match; this does not prove the reader lacks write permission.
Stop if
If either differs, check for a concurrent writer or rotation and repeat on a stable copy.

08 / 08

7. Debug the test policy and preserve delivery ownership

Use logrotate debug mode on the actual test policy and inspect its output without suppressing errors. Restore the prior policy through the same configuration process if the test fails.

Disabling monitoring does not stop delivery. Independently disable the delivery job if transfers must stop.

Before you use it
Replace the placeholder with a disposable test policy path; debug mode reports intended actions.
Dry-run the rotation policy
logrotate --debug /path/to/test-policy
Reader-run example · not executed for this guide

This example has no recorded result here. Follow its preparation, expected result and stop conditions before using it.

Success looks like
Inspect the reported target and retention settings, then test writer reopen behavior on the disposable log.
Stop if
Investigate any error or unexpected target before deploying.

Checks and sources

What was checked

  • The six-case fixture is a local test of an explicitly fictional JSONL contract; it sends no SFTP data and proves no recipient receipt.
  • The operational reader is a prospective bounded adapter example. It requires the actual job’s schema, cadence, completion deadline and an approved stable window.
  • Rotation/reopen behavior, file permissions and recipient verification remain separate checks. No installed monitor, policy or delivery job is asserted.
  • The exact local fictional model command was executed through the recorded browser on November 7, 2026; command/stdout hashes and exit 0 match the capture provenance. This establishes only the model scope stated beside that exercise.

Primary references

Sources

  1. OpenSSH sftp manualOpenBSD · accessed August 4, 2026
  2. OpenSSH client configurationOpenBSD · accessed August 4, 2026
  3. VICIdial product overviewVICIdial Group · accessed August 4, 2026
  4. Python JSON decodingPython Software Foundation · accessed November 7, 2026

Follow without guesswork

Get the next article

RSS is live now. Email delivery below is an explicit local preview and sends nothing.Open the RSS feed
Email preview only. The address stays in this browser and is never transmitted.