[SOUTH AFRICA] SciELO Nginx Log Export to FTP --- Operations Manual
1. Purpose
This manual documents the daily process used to export the SciELO Nginx access log to an FTP server while keeping a local monthly archive.
The workflow:
- Nginx writes requests to
scielo-access.log. logrotaterotates the active log daily.- The export script reads the newly rotated, uncompressed log without modifying it.
- The script creates a gzip copy using the required filename convention.
- The gzip file is validated.
- The file is uploaded to the FTP server.
- Only after the FTP server confirms a successful transfer is the generated copy stored in the local monthly archive.
The original Nginx/logrotate files remain under logrotate control.
2. File Naming Convention
The exported filename is:
YYYY-MM-DD_scielo.za.log.gz
Example:
2026-10-09_scielo.za.log.gz
The local archive is organized by year and month:
/var/log/nginx/scielo-org-za/YYYY-MM/
Example:
/var/log/nginx/scielo-org-za/2026-10/2026-10-09_scielo.za.log.gz
Using YYYY-MM instead of only the month number prevents files from
different years from being mixed.
3. Existing Nginx Log Rotation
The server currently uses the following logrotate configuration:
/var/log/nginx/*.log {
create 0640 nginx root
daily
rotate 10
missingok
notifempty
compress
delaycompress
sharedscripts
postrotate
/bin/kill -USR1 `cat /run/nginx.pid 2>/dev/null` 2>/dev/null || true
endscript
}
Important behavior
Because delaycompress is enabled, the most recently rotated file
remains uncompressed for one rotation cycle.
For example:
scielo-access.log
scielo-access.log-20261009
scielo-access.log-20261008.gz
scielo-access.log-20261007.gz
The export process must not modify or remove
scielo-access.log-20261009. It creates an independent compressed copy.
4. FTP Credentials
Store FTP credentials separately from the export script.
Create:
vi /root/.scielo-ftp.conf
Contents:
FTP_USER='your_username'
FTP_PASS='your_password'
Protect the file:
chmod 600 /root/.scielo-ftp.conf
chown root:root /root/.scielo-ftp.conf
Verify:
ls -l /root/.scielo-ftp.conf
Expected permissions:
-rw-------. 1 root root ... /root/.scielo-ftp.conf
Do not commit this file to Git or copy its contents into operational documentation.
Security note: traditional FTP does not encrypt credentials or transferred data. If the service later supports SFTP or FTPS, migration to an encrypted transport is recommended.
5. Daily Export Script
Create:
vi /usr/local/sbin/export-scielo-za.sh
Use the following script:
#!/bin/bash
set -u
umask 027
LOGDIR="/var/log/nginx"
ARCHIVE="$LOGDIR/scielo-org-za"
HOST="ftp.ratchet.scielo.org"
CREDENTIALS="/root/.scielo-ftp.conf"
# Load FTP credentials
if [ ! -r "$CREDENTIALS" ]; then
echo "ERROR: credentials file not found: $CREDENTIALS"
exit 1
fi
source "$CREDENTIALS"
if [ -z "${FTP_USER:-}" ] || [ -z "${FTP_PASS:-}" ]; then
echo "ERROR: FTP_USER or FTP_PASS is not defined."
exit 1
fi
# The date suffix generated by logrotate
DATA=$(date +%Y%m%d)
SOURCE="$LOGDIR/scielo-access.log-$DATA"
if [ ! -s "$SOURCE" ]; then
echo "ERROR: rotated log was not found or is empty:"
echo "$SOURCE"
exit 1
fi
YEAR="${DATA:0:4}"
MONTH="${DATA:4:2}"
DAY="${DATA:6:2}"
FORMATTED_DATE="${YEAR}-${MONTH}-${DAY}"
FILENAME="${FORMATTED_DATE}_scielo.za.log.gz"
DESTDIR="$ARCHIVE/${YEAR}-${MONTH}"
DEST="$DESTDIR/$FILENAME"
TMP="$DESTDIR/.${FILENAME}.tmp"
mkdir -p "$DESTDIR"
echo "=============================================="
echo "SciELO ZA daily export"
echo "=============================================="
echo "Source : $SOURCE"
echo "Export file : $FILENAME"
echo "Destination : $DEST"
echo
# Prevent duplicate processing
if [ -f "$DEST" ]; then
echo "File has already been processed:"
echo "$DEST"
exit 0
fi
# Create an independent compressed copy.
# The logrotate-managed source file is not changed.
echo "Compressing..."
gzip -c "$SOURCE" > "$TMP"
if [ $? -ne 0 ]; then
echo "ERROR: compression failed."
rm -f "$TMP"
exit 1
fi
# Validate the gzip stream
if ! gzip -t "$TMP"; then
echo "ERROR: invalid gzip file."
rm -f "$TMP"
exit 1
fi
echo "Compression successful:"
ls -lh "$TMP"
# FTP transfer
echo
echo "Uploading to FTP..."
FTP_LOG=$(mktemp)
ftp -n -v "$HOST" >"$FTP_LOG" 2>&1 <<EOF
quote USER $FTP_USER
quote PASS $FTP_PASS
binary
put $TMP $FILENAME
quit
EOF
cat "$FTP_LOG"
# Detect common FTP failures
if grep -Eqi \
'530 |550 |425 |426 |not connected|failed|failure|error|refused|timed out|no such|not found' \
"$FTP_LOG"; then
echo "ERROR: FTP reported a failure."
rm -f "$FTP_LOG"
rm -f "$TMP"
exit 1
fi
# Require FTP server completion confirmation
if ! grep -Eqi '^226 ' "$FTP_LOG"; then
echo "ERROR: FTP server did not confirm successful transfer."
rm -f "$FTP_LOG"
rm -f "$TMP"
exit 1
fi
rm -f "$FTP_LOG"
echo
echo "FTP transfer completed."
# Archive only after FTP success
mv -f -- "$TMP" "$DEST"
if [ $? -ne 0 ]; then
echo "ERROR: FTP transfer succeeded, but local archival failed:"
echo "$DEST"
exit 1
fi
echo
echo "=============================================="
echo "PROCESS COMPLETED"
echo "=============================================="
echo "Archived file:"
echo "$DEST"
exit 0
Set permissions:
chmod 750 /usr/local/sbin/export-scielo-za.sh
chown root:root /usr/local/sbin/export-scielo-za.sh
6. Why the Script Uses gzip -c
The command:
gzip -c "$SOURCE" > "$TMP"
reads the rotated log and writes a compressed copy.
It does not replace or delete:
/var/log/nginx/scielo-access.log-YYYYMMDD
This separation is intentional. The original file remains under
logrotate lifecycle management.
7. FTP Transfer Method
The FTP implementation uses the same connection method already validated in the environment:
ftp -n -v "$HOST" <<EOF
quote USER $FTP_USER
quote PASS $FTP_PASS
binary
put "$LOCAL_FILE" "$REMOTE_FILE"
quit
EOF
binary is required for gzip files.
The process does not force passive mode because the existing FTP workflow has already been validated without that change.
Successful transfer
The FTP server should return a 226 response, for example:
226 Transfer complete.
The script requires a 226 response before considering the upload
successful.
Common authentication failure
331 Please specify the password.
530 Login incorrect.
This indicates an authentication problem, not an upload or passive-mode problem.
Check the configured username/password and the credentials file.
8. Manual Test
Run:
/usr/local/sbin/export-scielo-za.sh
For a rotation dated October 9, 2026, the expected local file is:
/var/log/nginx/scielo-org-za/2026-10/2026-10-09_scielo.za.log.gz
Verify it:
ls -lh /var/log/nginx/scielo-org-za/2026-10/
Test gzip integrity:
gzip -t /var/log/nginx/scielo-org-za/2026-10/2026-10-09_scielo.za.log.gz
echo $?
Expected:
0
Inspect the beginning of the archived log:
gzip -dc /var/log/nginx/scielo-org-za/2026-10/2026-10-09_scielo.za.log.gz | head
9. Daily Scheduling
The observed log rotation normally occurs around 03:00--04:00. Run the export after the rotation has completed.
Create:
vi /etc/cron.d/scielo-za-export
Contents:
SHELL=/bin/bash
PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin
0 5 * * * root /usr/local/sbin/export-scielo-za.sh >> /var/log/export-scielo-za.log 2>&1
Set permissions:
chmod 644 /etc/cron.d/scielo-za-export
chown root:root /etc/cron.d/scielo-za-export
Verify:
cat /etc/cron.d/scielo-za-export
Monitor execution:
tail -100 /var/log/export-scielo-za.log
10. Historical Log Migration
Previously compressed logrotate files may look like:
scielo-access.log-20260930.gz
scielo-access.log-20261001.gz
scielo-access.log-20261002.gz
These files are already compressed and should not be recompressed.
They can be uploaded with a new remote filename and then moved/renamed locally.
Example mapping:
scielo-access.log-20260930.gz
-> 2026-09-30_scielo.za.log.gz
scielo-access.log-20261001.gz
-> 2026-10-01_scielo.za.log.gz
Local destinations:
/var/log/nginx/scielo-org-za/2026-09/2026-09-30_scielo.za.log.gz
/var/log/nginx/scielo-org-za/2026-10/2026-10-01_scielo.za.log.gz
Historical migration script
#!/bin/bash
source /root/.scielo-ftp.conf
HOST="ftp.ratchet.scielo.org"
ARCHIVE="/var/log/nginx/scielo-org-za"
for f in /var/log/nginx/scielo-access.log-????????.gz; do
[ -f "$f" ] || continue
base=$(basename "$f")
data="${base#scielo-access.log-}"
data="${data%.gz}"
year="${data:0:4}"
month="${data:4:2}"
day="${data:6:2}"
FILE_REMOTE="${year}-${month}-${day}_scielo.za.log.gz"
DESTDIR="${ARCHIVE}/${year}-${month}"
DEST="${DESTDIR}/${FILE_REMOTE}"
echo
echo "=========================================="
echo "Source : $f"
echo "FTP filename: $FILE_REMOTE"
echo "Destination : $DEST"
echo "=========================================="
if ! gzip -t "$f"; then
echo "ERROR: invalid gzip file: $f"
continue
fi
mkdir -p "$DESTDIR"
FTP_LOG=$(mktemp)
ftp -n -v "$HOST" >"$FTP_LOG" 2>&1 <<EOF
quote USER $FTP_USER
quote PASS $FTP_PASS
binary
put $f $FILE_REMOTE
quit
EOF
cat "$FTP_LOG"
if grep -Eqi \
'530 |550 |425 |426 |not connected|failed|failure|error|refused|timed out|no such|not found' \
"$FTP_LOG"; then
echo "ERROR: FTP transfer failed: $FILE_REMOTE"
rm -f "$FTP_LOG"
continue
fi
if ! grep -Eqi '^226 ' "$FTP_LOG"; then
echo "ERROR: FTP server did not confirm transfer: $FILE_REMOTE"
rm -f "$FTP_LOG"
continue
fi
rm -f "$FTP_LOG"
mv -f -- "$f" "$DEST"
if [ $? -ne 0 ]; then
echo "ERROR: FTP succeeded, but local move failed:"
echo "$f -> $DEST"
continue
fi
echo "OK: $FILE_REMOTE"
done
The order is intentionally:
Validate gzip
|
v
Upload to FTP
|
v
Receive FTP 226
|
v
Move/Rename locally
If FTP fails, the original historical file remains in place for a later retry.
11. Troubleshooting
Rotated source file does not exist
Example:
ERROR: rotated log was not found or is empty:
/var/log/nginx/scielo-access.log-20261009
Check:
ls -lh /var/log/nginx/scielo-access.log*
Also check logrotate status and execution time.
FTP login fails
Typical response:
331 Please specify the password.
530 Login incorrect.
Verify that credentials are loaded:
source /root/.scielo-ftp.conf
printf 'FTP_USER=[%s]\n' "$FTP_USER"
printf 'FTP_PASS length=%d\n' "${#FTP_PASS}"
Do not print the actual password.
Check for Windows CRLF characters:
cat -A /root/.scielo-ftp.conf
If lines contain ^M, normalize them:
sed -i 's/\r$//' /root/.scielo-ftp.conf
FTP transfer starts but fails
Relevant FTP response codes include:
425 Cannot open data connection
426 Connection closed; transfer aborted
550 Requested action not taken
Check firewall rules, FTP data-channel behavior, available disk space and server-side permissions.
Gzip validation fails
Test manually:
gzip -t FILE.gz
echo $?
A successful validation returns:
0
Do not upload or archive a file that fails gzip validation.
File was already processed
The daily script checks:
/var/log/nginx/scielo-org-za/YYYY-MM/YYYY-MM-DD_scielo.za.log.gz
If the file already exists, the script exits successfully without overwriting or retransmitting it.
12. Operational Verification
Useful daily checks:
tail -100 /var/log/export-scielo-za.log
find /var/log/nginx/scielo-org-za -type f -name '*_scielo.za.log.gz' -printf '%TY-%Tm-%Td %TH:%TM %s %p\n' | sort
Check gzip integrity for a specific file:
gzip -t /var/log/nginx/scielo-org-za/2026-10/2026-10-09_scielo.za.log.gz
Check the most recent archive:
find /var/log/nginx/scielo-org-za -type f -name '*_scielo.za.log.gz' -printf '%T@ %p\n' \
| sort -nr \
| head
13. Recovery Considerations
The current daily script uses:
DATA=$(date +%Y%m%d)
This means it expects the logrotate output for the current date to exist when the script runs.
For a more resilient production implementation, a future version should:
- discover all eligible
scielo-access.log-YYYYMMDDand.gzfiles; - determine whether the corresponding
YYYY-MM-DD_scielo.za.log.gzhas already been archived; - process only missing exports;
- safely retry failed FTP transfers;
- keep a clear success/failure audit trail.
This removes the dependency on a single daily execution window.
14. Security and Preservation Notes
The export archive and the original Nginx logs serve different operational purposes.
The export process should not alter the original logrotate-managed source. Keeping the source untouched provides a reference copy if an export must be regenerated or investigated.
Recommended controls include:
- keep FTP credentials readable only by root;
- do not store credentials in source control;
- validate gzip files before transfer;
- require an FTP success response before considering the transfer complete;
- retain execution logs;
- monitor failed scheduled jobs;
- periodically verify that local and remote expected files are present;
- migrate from unencrypted FTP to SFTP or FTPS when supported.
15. Key Paths
Purpose Path
Active Nginx access log /var/log/nginx/scielo-access.log
Rotated Nginx logs /var/log/nginx/scielo-access.log-YYYYMMDD[.gz]
Export archive /var/log/nginx/scielo-org-za/YYYY-MM/
Daily export script /usr/local/sbin/export-scielo-za.sh
FTP credentials /root/.scielo-ftp.conf
Cron configuration /etc/cron.d/scielo-za-export
Export execution log /var/log/export-scielo-za.log
FTP host ftp.ratchet.scielo.org
16. Expected Final State
After successful daily processing:
/var/log/nginx/
├── scielo-access.log
├── scielo-access.log-20261009
├── scielo-access.log-20261008.gz
│
└── scielo-org-za/
└── 2026-10/
├── 2026-10-01_scielo.za.log.gz
├── 2026-10-02_scielo.za.log.gz
├── ...
└── 2026-10-09_scielo.za.log.gz
The FTP server should contain the corresponding exported filenames:
2026-10-01_scielo.za.log.gz
2026-10-02_scielo.za.log.gz
...
2026-10-09_scielo.za.log.gz
Document purpose: operational runbook for the SciELO Nginx access-log FTP export workflow.