summaryrefslogtreecommitdiff
path: root/docs/handbook/hooks/logging-system.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/handbook/hooks/logging-system.md')
-rw-r--r--docs/handbook/hooks/logging-system.md492
1 files changed, 492 insertions, 0 deletions
diff --git a/docs/handbook/hooks/logging-system.md b/docs/handbook/hooks/logging-system.md
new file mode 100644
index 0000000000..5bb0b79e01
--- /dev/null
+++ b/docs/handbook/hooks/logging-system.md
@@ -0,0 +1,492 @@
+# Logging System
+
+## Table of Contents
+
+- [Introduction](#introduction)
+- [The log() Function](#the-log-function)
+ - [Function Signature](#function-signature)
+ - [Timestamp Generation](#timestamp-generation)
+ - [Dual Output with tee](#dual-output-with-tee)
+ - [Graceful Degradation](#graceful-degradation)
+- [Log File Configuration](#log-file-configuration)
+ - [The MIRROR_LOG Variable](#the-mirror_log-variable)
+ - [Default Path](#default-path)
+ - [Custom Log Paths](#custom-log-paths)
+ - [Log File Permissions](#log-file-permissions)
+- [Log Message Format](#log-message-format)
+ - [Timestamp Format](#timestamp-format)
+ - [Session Boundaries](#session-boundaries)
+ - [Ref Update Entries](#ref-update-entries)
+ - [Push Status Entries](#push-status-entries)
+ - [Summary Block](#summary-block)
+- [Complete Log Output Example](#complete-log-output-example)
+- [Git Push Output Capture](#git-push-output-capture)
+- [Log Rotation](#log-rotation)
+ - [Using logrotate](#using-logrotate)
+ - [Manual Rotation](#manual-rotation)
+ - [Size-Based Rotation](#size-based-rotation)
+- [Log Analysis](#log-analysis)
+ - [Counting Mirror Sessions](#counting-mirror-sessions)
+ - [Finding Failures](#finding-failures)
+ - [Extracting Push Duration](#extracting-push-duration)
+ - [Monitoring with tail](#monitoring-with-tail)
+- [Fallback Behavior](#fallback-behavior)
+- [Concurrency and Log Interleaving](#concurrency-and-log-interleaving)
+- [Security Considerations](#security-considerations)
+
+---
+
+## Introduction
+
+The Project-Tick `post-receive` hook (`hooks/post-receive`) includes a built-in logging system that records every mirror push operation. The system is implemented as a single bash function, `log()`, that writes timestamped messages to both standard output and a persistent log file.
+
+The logging system is designed with three priorities:
+1. **Reliability** — Logging never causes the hook to fail, even if the log file is unwritable
+2. **Visibility** — Messages appear on the pusher's terminal and in the persistent log
+3. **Simplicity** — A single function, no external logging frameworks
+
+---
+
+## The log() Function
+
+### Function Signature
+
+```bash
+log() {
+ local timestamp
+ timestamp="$(date -u '+%Y-%m-%d %H:%M:%S UTC')"
+ echo "[$timestamp] $*" | tee -a "$MIRROR_LOG" 2>/dev/null || echo "[$timestamp] $*"
+}
+```
+
+The function accepts any number of string arguments via `$*`, which concatenates them with a space separator.
+
+**Usage examples** from the hook:
+
+```bash
+log "=== Mirror push triggered ==="
+log " ref: $refname ($oldrev -> $newrev)"
+log "Pushing to remote: $remote"
+log " ✓ Successfully pushed to $remote"
+log " ✗ FAILED to push to $remote"
+log "--- Summary ---"
+log " Succeeded: ${SUCCEEDED_REMOTES[*]:-none}"
+log " Failed: ${FAILED_REMOTES[*]:-none}"
+log "=== Finished with errors ==="
+log "=== Finished successfully ==="
+```
+
+### Timestamp Generation
+
+```bash
+local timestamp
+timestamp="$(date -u '+%Y-%m-%d %H:%M:%S UTC')"
+```
+
+The `date` command is invoked with two key options:
+
+| Option | Purpose |
+|--------|---------|
+| `-u` | Use UTC time regardless of the server's local timezone |
+| `'+%Y-%m-%d %H:%M:%S UTC'` | ISO 8601-inspired format with explicit UTC suffix |
+
+**Why UTC?** Server environments may span multiple time zones. UTC ensures all log entries are comparable without timezone conversion. The explicit `UTC` suffix in the format string makes it unambiguous — a reader seeing `[2026-04-05 14:30:00 UTC]` knows this is not local time.
+
+**Why `local`?** The `local` keyword restricts `timestamp` to the function scope. Without it, `timestamp` would be a global variable, persisting after the function returns and potentially conflicting with other variables.
+
+### Dual Output with tee
+
+```bash
+echo "[$timestamp] $*" | tee -a "$MIRROR_LOG" 2>/dev/null
+```
+
+The `tee` command reads from stdin and writes to both stdout and the specified file:
+
+```
+echo "message" ──► tee ──► stdout (pusher's terminal)
+ │
+ └──► $MIRROR_LOG (append mode)
+```
+
+The `-a` flag means **append**. Without it, `tee` would truncate the log file on each write, losing previous entries.
+
+### Graceful Degradation
+
+```bash
+echo "[$timestamp] $*" | tee -a "$MIRROR_LOG" 2>/dev/null || echo "[$timestamp] $*"
+```
+
+The error handling chain works in three stages:
+
+1. **Primary path**: `echo | tee -a "$MIRROR_LOG"` — write to both stdout and log file
+2. **Error suppression**: `2>/dev/null` — if `tee` can't write to the log file, suppress its error message (e.g., "Permission denied")
+3. **Fallback**: `|| echo "[$timestamp] $*"` — if the entire `echo | tee` pipeline fails, write to stdout only
+
+This means the log function **never fails silently** — even if the log file is inaccessible, the message still reaches the pusher's terminal. And it **never crashes the hook** — log file errors don't propagate despite `set -e` (because they're handled by the `||` fallback).
+
+---
+
+## Log File Configuration
+
+### The MIRROR_LOG Variable
+
+```bash
+MIRROR_LOG="${MIRROR_LOG:-/var/log/git-mirror.log}"
+```
+
+The log file path is controlled by the `MIRROR_LOG` environment variable with a default fallback.
+
+### Default Path
+
+The default log file location is `/var/log/git-mirror.log`. This follows the Linux Filesystem Hierarchy Standard (FHS) convention of placing log files under `/var/log/`.
+
+**Requirements for the default path**:
+- The directory `/var/log/` must exist (it always does on standard Linux systems)
+- The user running the git daemon must have write permission to the file
+- The file will be created if it doesn't exist (assuming directory write permission)
+
+### Custom Log Paths
+
+Override the default by setting `MIRROR_LOG` in the hook's environment:
+
+```bash
+# In the git daemon's environment
+export MIRROR_LOG=/var/log/project-tick/mirror.log
+
+# Or per-repository via a wrapper
+MIRROR_LOG=/home/git/logs/mirror.log hooks/post-receive
+```
+
+**Common custom paths**:
+
+| Path | Use Case |
+|------|----------|
+| `/var/log/git-mirror.log` | Default — shared system log |
+| `/var/log/project-tick/mirror.log` | Project-specific log directory |
+| `/home/git/logs/mirror.log` | User-local log (no root needed) |
+| `/tmp/mirror.log` | Temporary/testing |
+| `/dev/null` | Disable file logging (stdout only) |
+
+### Log File Permissions
+
+Set up the log file with appropriate ownership:
+
+```bash
+# Create the log file with correct ownership
+sudo touch /var/log/git-mirror.log
+sudo chown git:git /var/log/git-mirror.log
+sudo chmod 640 /var/log/git-mirror.log
+
+# Or create a project-specific log directory
+sudo mkdir -p /var/log/project-tick
+sudo chown git:git /var/log/project-tick
+sudo chmod 750 /var/log/project-tick
+```
+
+The `640` permission (`rw-r-----`) allows the git user to write and the git group to read, while preventing other users from accessing potentially sensitive information.
+
+---
+
+## Log Message Format
+
+### Timestamp Format
+
+Every log line follows this pattern:
+
+```
+[YYYY-MM-DD HH:MM:SS UTC] <message>
+```
+
+Example:
+```
+[2026-04-05 14:30:00 UTC] === Mirror push triggered ===
+```
+
+The square brackets delimit the timestamp, making it easy to parse programmatically:
+
+```bash
+# Extract just the messages (remove timestamps)
+sed 's/^\[[^]]*\] //' /var/log/git-mirror.log
+
+# Extract just the timestamps
+grep -oP '^\[\K[^]]+' /var/log/git-mirror.log
+```
+
+### Session Boundaries
+
+Each mirror operation is delimited by banner lines:
+
+```
+[2026-04-05 14:30:00 UTC] === Mirror push triggered ===
+...
+[2026-04-05 14:30:15 UTC] === Finished successfully ===
+```
+
+Or on failure:
+
+```
+[2026-04-05 14:30:00 UTC] === Mirror push triggered ===
+...
+[2026-04-05 14:30:15 UTC] === Finished with errors ===
+```
+
+The `===` delimiters serve as visual and programmatic session markers.
+
+### Ref Update Entries
+
+Each ref in the push is logged with indentation:
+
+```
+[2026-04-05 14:30:00 UTC] ref: refs/heads/main (abc1234 -> def5678)
+[2026-04-05 14:30:00 UTC] ref: refs/tags/v1.0.0 (0000000 -> abc1234)
+```
+
+The format `($oldrev -> $newrev)` shows the transition. The all-zeros SHA (`0000000...`) indicates a new ref creation or deletion:
+
+| Pattern | Meaning |
+|---------|---------|
+| `(000... -> abc...)` | New ref created |
+| `(abc... -> def...)` | Ref updated |
+| `(abc... -> 000...)` | Ref deleted |
+
+### Push Status Entries
+
+Each remote push produces a status line:
+
+```
+[2026-04-05 14:30:05 UTC] Pushing to remote: github
+[2026-04-05 14:30:08 UTC] ✓ Successfully pushed to github
+```
+
+Or on failure:
+
+```
+[2026-04-05 14:30:10 UTC] Pushing to remote: sourceforge
+[2026-04-05 14:30:25 UTC] ✗ FAILED to push to sourceforge
+```
+
+The Unicode symbols (✓ and ✗) provide quick visual scanning in the log.
+
+### Summary Block
+
+At the end of each session:
+
+```
+[2026-04-05 14:30:15 UTC] --- Summary ---
+[2026-04-05 14:30:15 UTC] Succeeded: github gitlab codeberg
+[2026-04-05 14:30:15 UTC] Failed: sourceforge
+```
+
+Or when all succeed:
+
+```
+[2026-04-05 14:30:15 UTC] --- Summary ---
+[2026-04-05 14:30:15 UTC] Succeeded: github gitlab codeberg sourceforge
+[2026-04-05 14:30:15 UTC] Failed: none
+```
+
+---
+
+## Complete Log Output Example
+
+A typical successful mirror operation produces:
+
+```
+[2026-04-05 14:30:00 UTC] === Mirror push triggered ===
+[2026-04-05 14:30:00 UTC] ref: refs/heads/main (a1b2c3d4e5f6 -> f6e5d4c3b2a1)
+[2026-04-05 14:30:00 UTC] ref: refs/tags/v2.1.0 (0000000000000000000000000000000000000000 -> a1b2c3d4e5f6)
+[2026-04-05 14:30:00 UTC] Pushing to remote: github
+To github.com:Project-Tick/Project-Tick.git
+ + a1b2c3d..f6e5d4c main -> main (forced update)
+ * [new tag] v2.1.0 -> v2.1.0
+[2026-04-05 14:30:03 UTC] ✓ Successfully pushed to github
+[2026-04-05 14:30:03 UTC] Pushing to remote: gitlab
+To gitlab.com:Project-Tick/Project-Tick.git
+ + a1b2c3d..f6e5d4c main -> main (forced update)
+ * [new tag] v2.1.0 -> v2.1.0
+[2026-04-05 14:30:06 UTC] ✓ Successfully pushed to gitlab
+[2026-04-05 14:30:06 UTC] Pushing to remote: codeberg
+To codeberg.org:Project-Tick/Project-Tick.git
+ + a1b2c3d..f6e5d4c main -> main (forced update)
+ * [new tag] v2.1.0 -> v2.1.0
+[2026-04-05 14:30:09 UTC] ✓ Successfully pushed to codeberg
+[2026-04-05 14:30:09 UTC] --- Summary ---
+[2026-04-05 14:30:09 UTC] Succeeded: github gitlab codeberg
+[2026-04-05 14:30:09 UTC] Failed: none
+[2026-04-05 14:30:09 UTC] === Finished successfully ===
+```
+
+Note that the raw `git push` output (the `To ...` and `+ ... (forced update)` lines) is **interleaved** with the hook's log messages. This is because `git push` output goes through `tee` to the log file alongside the hook's `log()` calls.
+
+---
+
+## Git Push Output Capture
+
+Beyond the `log()` function's messages, the raw output of each `git push` is also captured:
+
+```bash
+git push --mirror --force "$remote" 2>&1 | tee -a "$MIRROR_LOG" 2>/dev/null
+```
+
+The `2>&1` redirect merges git's stderr into stdout before piping to `tee`. Git sends progress messages and transfer statistics to stderr, so this redirect ensures the complete output is logged:
+
+```
+To github.com:Project-Tick/Project-Tick.git
+ + a1b2c3d..f6e5d4c main -> main (forced update)
+ * [new tag] v2.1.0 -> v2.1.0
+```
+
+This raw output appears in the log file **without timestamps** because it bypasses the `log()` function. It sits between the "Pushing to remote:" and "✓ Successfully pushed" entries.
+
+---
+
+## Log Rotation
+
+The hook appends to the log file indefinitely. Without rotation, the file will grow without bound. Here are strategies for managing log file size.
+
+### Using logrotate
+
+Create `/etc/logrotate.d/git-mirror`:
+
+```
+/var/log/git-mirror.log {
+ weekly
+ rotate 12
+ compress
+ delaycompress
+ missingok
+ notifempty
+ create 640 git git
+}
+```
+
+| Directive | Effect |
+|-----------|--------|
+| `weekly` | Rotate once per week |
+| `rotate 12` | Keep 12 rotated files (3 months) |
+| `compress` | Compress rotated files with gzip |
+| `delaycompress` | Don't compress the most recent rotated file |
+| `missingok` | Don't error if the log file doesn't exist |
+| `notifempty` | Don't rotate if the file is empty |
+| `create 640 git git` | Create new log file with these permissions |
+
+### Manual Rotation
+
+```bash
+# Rotate manually
+mv /var/log/git-mirror.log /var/log/git-mirror.log.1
+touch /var/log/git-mirror.log
+chown git:git /var/log/git-mirror.log
+```
+
+No signal or restart is needed — the hook appends to `$MIRROR_LOG` on each invocation, so it will create a new file if the old one was moved.
+
+### Size-Based Rotation
+
+Add a cron job that rotates when the file exceeds a certain size:
+
+```bash
+# /etc/cron.daily/git-mirror-log-rotate
+#!/bin/sh
+LOG=/var/log/git-mirror.log
+MAX_SIZE=10485760 # 10 MB
+
+if [ -f "$LOG" ]; then
+ SIZE=$(stat -c %s "$LOG" 2>/dev/null || echo 0)
+ if [ "$SIZE" -gt "$MAX_SIZE" ]; then
+ mv "$LOG" "${LOG}.$(date +%Y%m%d)"
+ gzip "${LOG}.$(date +%Y%m%d)"
+ fi
+fi
+```
+
+---
+
+## Log Analysis
+
+### Counting Mirror Sessions
+
+```bash
+grep -c "=== Mirror push triggered ===" /var/log/git-mirror.log
+```
+
+### Finding Failures
+
+```bash
+# Find all failure entries
+grep "✗ FAILED" /var/log/git-mirror.log
+
+# Find sessions that ended with errors
+grep "=== Finished with errors ===" /var/log/git-mirror.log
+
+# Count failures per remote
+grep "✗ FAILED" /var/log/git-mirror.log | awk '{print $NF}' | sort | uniq -c | sort -rn
+```
+
+### Extracting Push Duration
+
+Calculate the time between trigger and finish:
+
+```bash
+# Extract session start and end times
+grep -E "(Mirror push triggered|Finished)" /var/log/git-mirror.log
+```
+
+### Monitoring with tail
+
+For real-time monitoring during a push:
+
+```bash
+tail -f /var/log/git-mirror.log
+```
+
+---
+
+## Fallback Behavior
+
+The logging system handles the following failure scenarios:
+
+| Scenario | Behavior |
+|----------|----------|
+| Log file doesn't exist | `tee` creates it (if directory is writable) |
+| Log file is not writable | `tee` error suppressed; message goes to stdout only |
+| Log directory doesn't exist | `tee` fails silently; message goes to stdout only |
+| `/dev/null` as log path | All file output discarded; stdout works normally |
+| `$MIRROR_LOG` is empty | `tee -a ""` fails; fallback echo to stdout |
+
+In every case, the hook continues to function. Logging is strictly best-effort and never causes a hook failure.
+
+---
+
+## Concurrency and Log Interleaving
+
+When multiple pushes trigger the hook simultaneously, multiple `post-receive` instances write to the same log file concurrently. The `-a` (append) flag on `tee` uses `O_APPEND` semantics, which means writes are atomic at the kernel level for sizes up to `PIPE_BUF` (4096 bytes on Linux).
+
+Since individual log lines are well under 4096 bytes, **individual lines will not be corrupted**. However, lines from different sessions may interleave:
+
+```
+[2026-04-05 14:30:00 UTC] === Mirror push triggered === # Session A
+[2026-04-05 14:30:00 UTC] === Mirror push triggered === # Session B
+[2026-04-05 14:30:00 UTC] ref: refs/heads/main (...) # Session A
+[2026-04-05 14:30:00 UTC] ref: refs/heads/feature (...) # Session B
+```
+
+To disambiguate, you could modify the `log()` function to include a PID:
+
+```bash
+echo "[$$][$timestamp] $*" | tee -a "$MIRROR_LOG" 2>/dev/null || echo "[$$][$timestamp] $*"
+```
+
+This produces lines like `[12345][2026-04-05 14:30:00 UTC] message` which can be filtered by PID.
+
+---
+
+## Security Considerations
+
+1. **Log file contents** — The log records ref names, remote names, and git push output. It should **not** contain credentials (tokens are in the git config, not in push output). However, treat the log as moderately sensitive.
+
+2. **Log file permissions** — Use `640` or `600` permissions. Avoid world-readable (`644`) logs on multi-user systems.
+
+3. **Log injection** — Ref names come from the pusher and appear in log messages (`log " ref: $refname ..."`). While this is a cosmetic concern (log files aren't executed), extremely long or crafted ref names could produce misleading log entries. Git itself limits ref names to valid characters.
+
+4. **Disk exhaustion** — Without log rotation, the log file grows indefinitely. A hostile actor with push access could trigger many pushes to fill the disk. Use log rotation and monitoring to mitigate.