Tail All Container Logs

The script below will print recent logs from all of the containers that are currently on the host. The number of logs to query can be controlled with -n/--number just like with tail and docker logs. If you want to go container-by-container and read the logs from the previous container before moving on, you can use the -w/--wait flag. This will cause the script to prompt you for confirmation before moving on to the next container.

#!/usr/bin/env bash
set -euo pipefail

DEFAULT_LOG_NUMBER=10
LOG_NUMBER="$DEFAULT_LOG_NUMBER"
SHOULD_WAIT="false"

print_usage() {
    echo "USAGE:"
    echo "  $(basename "$0") [OPTIONS...]"
    echo
    echo "OPTIONS:"
    echo "  -h, --help          Print this help message"
    echo "  -w, --wait          Wait for confirmation before moving on to the next container"
    echo "  -n, --number int    Set the number of logs to print for each container (default: $DEFAULT_LOG_NUMBER)"
}

handle_missing_arg() {
    echo "Missing argument: $1"
    print_usage
    exit 1
}

is_integer() {
    [[ "$1" =~ ^[0-9]+$ ]]
}

parse_arguments() {
    while [[ $# -ne 0 ]] && [[ "$1" != "" ]]; do
        case $1 in
        -h | --help)
            print_usage
            exit
            ;;
        -w | --wait)
            SHOULD_WAIT="true"
            ;;
        -n | --number)
            shift
            if [[ $# -eq 0 ]]; then handle_missing_arg "tag"; fi
            if ! is_integer "$1"; then echo "$1 is not an integer"; exit 1; fi
            LOG_NUMBER="$1"
            ;;
        *)
            echo "Unknown argument: $1"
            print_usage
            exit 1
            ;;
        esac
        shift
    done
}

get_all_containers() {
    docker ps --all --format '{{.Names}}' | sort
}

get_container_status() {
    local container_name="$1"
    docker ps --all --format '{{.Status}}' --filter "name=$container_name"
}

print_header() {
    local style='\033[37;44;1m' # blue background, white foreground
    local reset='\033[0m'
    echo -e "${style} ${1} ${reset}"
}

print_container_logs() {
    local container="$1"

    if [[ "$SHOULD_WAIT" == "true" ]]; then
        echo "Next container: $container"
        read -r -n 1 -p "Press enter to continue to the next container or any other character to cancel: "
        echo
        if [[ "$REPLY" != "" ]]; then
            echo "Quitting"
            exit 1
        fi
    fi

    print_header "Logs for '$container_name' [$(get_container_status "$container_name")]"
    docker logs -n "$LOG_NUMBER" "$container_name"
    echo
}

main() {
    parse_arguments "$@"
    # This loop is explicitly changing the file descriptor used for data to 3 so
    # that it doesn't use stdin which is needed by the 'read' command. Without
    # it this loop and the 'read' command used to confirm moving to the next
    # container would both use stdin which breaks the read command
    while IFS= read -r container_name <&3; do
        print_container_logs "$container_name"
    done 3< <(docker ps --all --format '{{.Names}}' | sort)
}

main "$@"