├── img ├── fuzzy.jpg ├── logo.png ├── regex.png ├── coloring.jpg ├── docker.jpg └── timestamp.jpg ├── .vscode ├── tasks.json └── launch.json ├── playground ├── prometheus.yml ├── index.json ├── README.md ├── docker-compose.yml └── foreground.sh ├── .gitignore ├── entrypoint.sh ├── go.mod ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.yml │ └── bug_report.yml └── workflows │ ├── docker.yml │ ├── merge-coverage.yml │ └── build.yml ├── .golangci.yml ├── LICENSE ├── docker-compose.yml ├── audit.yml ├── config.yml ├── install.ps1 ├── install.sh ├── Dockerfile ├── ROADMAP.md ├── Supfile.yml ├── Makefile ├── color.log ├── go.sum ├── README.md └── main_test.go /img/fuzzy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lifailon/lazyjournal/HEAD/img/fuzzy.jpg -------------------------------------------------------------------------------- /img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lifailon/lazyjournal/HEAD/img/logo.png -------------------------------------------------------------------------------- /img/regex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lifailon/lazyjournal/HEAD/img/regex.png -------------------------------------------------------------------------------- /img/coloring.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lifailon/lazyjournal/HEAD/img/coloring.jpg -------------------------------------------------------------------------------- /img/docker.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lifailon/lazyjournal/HEAD/img/docker.jpg -------------------------------------------------------------------------------- /img/timestamp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lifailon/lazyjournal/HEAD/img/timestamp.jpg -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": [ 3 | { 4 | "label": "Run remote debug", 5 | "type": "shell", 6 | "command": "wsl make run-debug" 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /playground/prometheus.yml: -------------------------------------------------------------------------------- 1 | scrape_configs: 2 | - job_name: logporter 3 | scrape_interval: 5s 4 | scrape_timeout: 1s 5 | static_configs: 6 | - targets: 7 | - logporter:9333 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build 2 | /bin/ 3 | *.exe 4 | lazyjournal 5 | 6 | # Packages 7 | /DEBIAN/ 8 | /usr/ 9 | *.snap 10 | *.deb 11 | *.rpm 12 | 13 | # Test reports 14 | *report.md 15 | *.out 16 | test.log 17 | 18 | # Profiling 19 | *.pprof -------------------------------------------------------------------------------- /playground/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "lazyjournal", 3 | "description": "Installation and running lazyjournal", 4 | "details": { 5 | "intro": { 6 | "text": "README.md", 7 | "foreground": "foreground.sh" 8 | } 9 | }, 10 | "backend": { 11 | "imageid": "ubuntu" 12 | } 13 | } -------------------------------------------------------------------------------- /playground/README.md: -------------------------------------------------------------------------------- 1 | # Playground 2 | 3 | Demonstration of installing and running the [lazyjournal](https://github.com/Lifailon/lazyjournal?tab=readme-ov-file) binary with pre-launched test containers using Docker Compose and Podman in active logging mode on the [Killercoda](https://killercoda.com/lazyjournal/scenario/playground) playground. -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TTYD_MODE=$(echo "$TTYD" | tr '[:upper:]' '[:lower:]') 4 | if [ "$TTYD_MODE" = "true" ]; then 5 | TTYD_OPTIONS="-W -p ${PORT:-5555}" 6 | if [ -n "${USERNAME}" ] && [ -n "${PASSWORD}" ]; then 7 | TTYD_OPTIONS="$TTYD_OPTIONS -c ${USERNAME}:${PASSWORD}" 8 | fi 9 | ttyd $TTYD_OPTIONS lazyjournal $OPTIONS 10 | else 11 | lazyjournal $OPTIONS 12 | fi -------------------------------------------------------------------------------- /playground/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | logporter: 3 | image: lifailon/logporter:latest 4 | container_name: logporter 5 | restart: unless-stopped 6 | volumes: 7 | - /var/run/docker.sock:/var/run/docker.sock:ro 8 | ports: 9 | - 9333:9333 10 | 11 | prometheus: 12 | image: prom/prometheus:latest 13 | container_name: prometheus 14 | restart: unless-stopped 15 | volumes: 16 | - ./prometheus.yml:/etc/prometheus/prometheus.yml 17 | ports: 18 | - 9092:9090 -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Lifailon/lazyjournal 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.2 6 | 7 | require ( 8 | github.com/awesome-gocui/gocui v1.1.0 9 | golang.org/x/text v0.23.0 10 | gopkg.in/yaml.v3 v3.0.1 11 | ) 12 | 13 | require ( 14 | github.com/gdamore/encoding v1.0.1 // indirect 15 | github.com/gdamore/tcell/v2 v2.8.1 // indirect 16 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 17 | github.com/mattn/go-runewidth v0.0.16 // indirect 18 | github.com/rivo/uniseg v0.4.7 // indirect 19 | golang.org/x/sys v0.31.0 // indirect 20 | golang.org/x/term v0.30.0 // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: 🚀 Feature request 2 | description: Request a new feature or source for log. 3 | title: "[Feature]: " 4 | labels: ["enhancement"] 5 | body: 6 | - type: dropdown 7 | id: architecture 8 | attributes: 9 | label: Where improvement is needed? 10 | default: 0 11 | options: 12 | - Interface 13 | - System journals 14 | - File system 15 | - Containerization system 16 | - Filtering 17 | - Coloring 18 | - Output 19 | - New 20 | validations: 21 | required: true 22 | 23 | - type: textarea 24 | attributes: 25 | label: Describe what you would like to implement and how you see it 26 | validations: 27 | required: true 28 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # https://golangci-lint.run/docs/configuration/file 2 | # https://golangci-lint.run/docs/linters 3 | version: "2" 4 | linters: 5 | # default: none 6 | # enable: 7 | # - errcheck 8 | # - gosimple 9 | # - govet 10 | # - ineffassign 11 | # - staticcheck 12 | # - unused 13 | # - err113 14 | # - unparam 15 | # - gocritic 16 | # - misspell 17 | # - perfsprint 18 | default: all 19 | disable: 20 | - gosec 21 | - makezero 22 | - dupl 23 | - wsl 24 | - wsl_v5 25 | - goconst 26 | - gocognit 27 | - gocyclo 28 | - revive 29 | - maintidx 30 | - cyclop 31 | - depguard 32 | - forbidigo 33 | - funlen 34 | - godot 35 | - lll 36 | - mnd 37 | - nestif 38 | - nilerr 39 | - nlreturn 40 | - tagliatelle 41 | - varnamelen 42 | - wrapcheck 43 | - ireturn 44 | - nonamedreturns 45 | - exhaustruct 46 | - unconvert 47 | - copyloopvar 48 | - intrange 49 | - paralleltest 50 | - gochecknoglobals 51 | - prealloc -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2024 Lifailon 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the 8 | Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | lazyjournal: 3 | image: lifailon/lazyjournal:latest 4 | container_name: lazyjournal 5 | restart: unless-stopped 6 | privileged: true 7 | volumes: 8 | # Systemd 9 | - /run/dbus/system_bus_socket:/run/dbus/system_bus_socket:ro 10 | - /run/systemd/system:/run/systemd/system:ro 11 | - /run/systemd/journal/socket:/run/systemd/journal/socket:ro 12 | # Journald 13 | - /etc/machine-id:/etc/machine-id:ro 14 | # Filesystem 15 | - /var/log:/var/log:ro 16 | - /opt:/opt:ro 17 | - /home:/home:ro 18 | # Docker socket 19 | - /var/run/docker.sock:/var/run/docker.sock:ro 20 | # Read docker logs from filesystem 21 | - /var/lib/docker/containers:/var/lib/docker/containers:ro 22 | # Kubernetes configuration 23 | - $HOME/.kube/config:/root/.kube/config:ro 24 | # Custom lazyjournal configuration (optional) 25 | # - ./config.yml:/bin/config.yml:ro 26 | environment: 27 | # Enable Web mode 28 | - TTYD=false 29 | - PORT=5555 30 | # Credentials for accessing via Web browser (optional) 31 | - USERNAME= 32 | - PASSWORD= 33 | # Flags used (optional) 34 | - OPTIONS= 35 | # Web mode 36 | ports: 37 | - 5555:5555 38 | # TUI mode 39 | tty: true 40 | stdin_open: true -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker Build 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | Commit: 7 | description: "Commit for checkout" 8 | required: true 9 | default: "" 10 | Version: 11 | description: "Version for tag" 12 | required: true 13 | default: "" 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | 19 | name: Build on ubuntu-latest 20 | 21 | steps: 22 | - name: Clone main repository 23 | uses: actions/checkout@v4 24 | with: 25 | fetch-depth: 0 26 | ref: main 27 | 28 | - name: Checkout the specified commit 29 | run: git checkout ${{ github.event.inputs.Commit }} 30 | 31 | - name: Login to Docker Hub 32 | uses: docker/login-action@v3 33 | with: 34 | username: ${{ secrets.DOCKER_USERNAME }} 35 | password: ${{ secrets.DOCKER_PASSWORD }} 36 | 37 | - name: Install Docker Buildx 38 | uses: docker/setup-buildx-action@v3 39 | with: 40 | driver: docker-container 41 | install: true 42 | 43 | - name: Build and push Docker images for Linux on amd64 and arm64 44 | run: | 45 | version=${{ github.event.inputs.Version }} 46 | docker buildx build \ 47 | --platform linux/amd64,linux/arm64 \ 48 | -t lifailon/lazyjournal:$version \ 49 | --push . 50 | -------------------------------------------------------------------------------- /playground/foreground.sh: -------------------------------------------------------------------------------- 1 | echo -e "\033[32mInstalling k3s (lightweight Kubernetes cluster)...\033[0m" 2 | curl -sfL https://get.k3s.io | sh - 3 | 4 | echo -e "\033[32mInstalling docker-compose...\033[0m" 5 | curl -sSL "https://github.com/docker/compose/releases/download/v2.40.3/docker-compose-$(uname -s)-$(uname -m)" -o /usr/bin/docker-compose 6 | 7 | echo -e "\033[32mInstalling Docker Socket Proxy in the Podman...\033[0m" 8 | podman run -d -p 2375:2375 --name docker-socket-haproxy -v /var/run/docker.sock:/var/run/docker.sock lifailon/docker-socket-proxy:amd64 9 | while true; do curl -s http://localhost:2375/_ping > /dev/null; sleep 2; done & 10 | 11 | echo -e "\033[32mInstalling prometheus and logporter (lightweight alternative to cadvisor) in the compose stack...\033[0m" 12 | curl -sSL https://raw.githubusercontent.com/Lifailon/lazyjournal/refs/heads/main/playground/prometheus.yml -o prometheus.yml 13 | curl -sSL https://raw.githubusercontent.com/Lifailon/lazyjournal/refs/heads/main/playground/docker-compose.yml -o docker-compose.yml 14 | docker-compose up -d 15 | 16 | echo -e "\033[32mCreate test log for custom path in the file system...\033[0m" 17 | mkdir /test 18 | curl -sSL https://raw.githubusercontent.com/Lifailon/lazyjournal/refs/heads/main/color.log -o /test/color.log 19 | 20 | echo -e "\033[32mInstalling lazyjournal binary from the GitHub repository...\033[0m" 21 | curl -sS https://raw.githubusercontent.com/Lifailon/lazyjournal/main/install.sh | bash 22 | . /root/.bashrc 23 | 24 | lazyjournal -p /test -t 5000 -u 2 -------------------------------------------------------------------------------- /audit.yml: -------------------------------------------------------------------------------- 1 | system: 2 | date: 28.11.2025 11:21:32 3 | go: 1.23.2 4 | os: linux Ubuntu 25.04 (Plucky Puffin) 5 | arch: amd64 6 | username: lifailon 7 | privilege: user 8 | execType: source code 9 | execPath: /tmp/go-build4279785658/b001/exe/main 10 | systemd: 11 | journald: 12 | - installed: true 13 | journals: 14 | - name: Unit service list 15 | count: 232 16 | - name: System journals 17 | count: 83667 18 | - name: User journals 19 | count: 147 20 | - name: Kernel boot 21 | count: 36 22 | fileSystem: 23 | files: 24 | - name: System var logs 25 | path: /var/log/ 26 | count: 79 27 | - name: Custom path 28 | path: /opt 29 | count: 0 30 | - name: Users home logs 31 | path: /home/ 32 | count: 103 33 | - name: Process descriptor logs 34 | path: descriptor 35 | count: 0 36 | containerization: 37 | system: 38 | - name: docker 39 | installed: true 40 | version: 27.5.1 41 | containers: 38 42 | context: 43 | current: default 44 | count: 4 45 | - name: compose 46 | installed: true 47 | version: 2.39.2 48 | stacks: 11 49 | - name: podman 50 | installed: true 51 | version: 5.4.1 52 | containers: 0 53 | context: 54 | current: default 55 | count: 1 56 | - name: kubernetes 57 | installed: true 58 | version: 1.33.3+k3s1 59 | pods: 56 60 | context: 61 | current: default 62 | count: 1 63 | namespace: 64 | current: all 65 | count: 16 -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: ⚠ Bug report 2 | description: Report a bug or issue. 3 | title: '[Bug]: ' 4 | labels: ['bug'] 5 | body: 6 | - type: dropdown 7 | id: system 8 | attributes: 9 | label: System 10 | default: 0 11 | options: 12 | - linux 13 | - macOS 14 | - openbsd 15 | - freebsd 16 | - windows 17 | validations: 18 | required: true 19 | 20 | - type: dropdown 21 | id: architecture 22 | attributes: 23 | label: Architecture 24 | default: 0 25 | options: 26 | - amd64 27 | - arm64 28 | validations: 29 | required: true 30 | 31 | - type: input 32 | id: os-version 33 | attributes: 34 | label: OS version 35 | placeholder: Ubuntu 24.04, macOS 15.2, Windows 10 or other 36 | validations: 37 | required: true 38 | 39 | - type: markdown 40 | attributes: 41 | value: | 42 | Before opening an issue, please check the log source content manually, this will help understand the problem more accurately and resolve it faster. 43 | If you have an error running the executable, check running from source code (if you have the ability to install Go) and specify it in the installation method. 44 | 45 | - type: checkboxes 46 | id: install-method 47 | attributes: 48 | label: Installation method 49 | options: 50 | - label: Binary 51 | - label: Source code 52 | - label: Docker container 53 | 54 | - type: textarea 55 | id: description 56 | attributes: 57 | label: Description 58 | description: Briefly describe the problem and transmit the contents of the error 59 | validations: 60 | required: true -------------------------------------------------------------------------------- /config.yml: -------------------------------------------------------------------------------- 1 | # Default flag values (lower priority) 2 | settings: 3 | tailMode: 50000 4 | updateInterval: 5 5 | minSymbolFilter: 3 6 | disableAutoUpdate: false 7 | disableMouse: false 8 | disableTimestamp: false 9 | onlyStream: false 10 | dockerContext: default 11 | kubernetesContext: default 12 | kubernetesNamespace: all 13 | customPath: /opt 14 | colorMode: default 15 | disableFastMode: false 16 | 17 | # Available hotkeys: 18 | # a-z, 0-9, f1-f12 and any other symbols 19 | # enter, space, backspace, delete, escape, tab and shift+tab 20 | # shift+ 21 | # ctrl+ 22 | hotkeys: 23 | help: f1 24 | up: k 25 | quickUp: shift+k 26 | veryQuickUp: ctrl+k 27 | switchFilterMode: ctrl+k 28 | down: j 29 | quickDown: shift+j 30 | veryQuickDown: ctrl+j 31 | backSwitchFilterMode: ctrl+j 32 | left: h 33 | right: l 34 | switchWindow: tab 35 | backSwitchWindows: shift+tab 36 | loadJournal: enter 37 | goToFilter: / 38 | goToEnd: ctrl+e 39 | goToTop: ctrl+a 40 | tailModeMore: ctrl+x 41 | tailModeLess: ctrl+z 42 | updateIntervalMore: ctrl+p 43 | updateIntervalLess: ctrl+o 44 | autoUpdateJournal: ctrl+u 45 | updateJournal: ctrl+r 46 | updateLists: ctrl+q 47 | colorDisable: ctrl+w 48 | tailspinEnable: ctrl+n 49 | switchDockerMode: ctrl+d 50 | switchStreamMode: ctrl+s 51 | timestampShow: ctrl+t 52 | exit: ctrl+c 53 | 54 | # Available colors: default, green, black, yellow, red, blue, cyan, magenta, white 55 | interface: 56 | foregroundColor: default 57 | backgroundColor: default 58 | selectedForegroundColor: black 59 | selectedBackgroundColor: green 60 | frameColor: default 61 | titleColor: default 62 | selectedFrameColor: green 63 | selectedTitleColor: green 64 | statusColor: yellow 65 | errorColor: red -------------------------------------------------------------------------------- /install.ps1: -------------------------------------------------------------------------------- 1 | $OS = $([System.Environment]::OSVersion).VersionString 2 | $ARCH = $env:PROCESSOR_ARCHITECTURE.ToLower() 3 | 4 | Write-Host "System: " -NoNewline 5 | Write-Host $OS -ForegroundColor Green 6 | Write-Host "Architecture: " -NoNewline 7 | Write-Host $ARCH -ForegroundColor Green 8 | 9 | $binPath = "$env:LOCALAPPDATA\lazyjournal" 10 | $configPath = "$HOME\.config\lazyjournal" 11 | 12 | if (!(Test-Path $binPath)) { 13 | New-Item -Path $binPath -ItemType Directory | Out-Null 14 | Write-Host "Directory created: " -NoNewline 15 | Write-Host $binPath -ForegroundColor Blue 16 | } 17 | 18 | if (!(Test-Path $configPath)) { 19 | New-Item -Path $configPath -ItemType Directory | Out-Null 20 | Write-Host "Directory created: " -NoNewline 21 | Write-Host $configPath -ForegroundColor Blue 22 | } 23 | 24 | $beforeEnvPath = [Environment]::GetEnvironmentVariable("Path", "User") 25 | if (!($($beforeEnvPath).Split(";") -contains $binPath)) { 26 | $afterEnvPath = $beforeEnvPath + ";$binPath" 27 | [Environment]::SetEnvironmentVariable("Path", $afterEnvPath, "User") 28 | Write-Host "The path has been added to the Path environment variable for the current user." 29 | } 30 | 31 | $GITHUB_LATEST_VERSION = (Invoke-RestMethod "https://api.github.com/repos/Lifailon/lazyjournal/releases/latest").tag_name 32 | if ($null -ne $GITHUB_LATEST_VERSION) { 33 | $urlDownload = "https://github.com/Lifailon/lazyjournal/releases/download/$GITHUB_LATEST_VERSION/lazyjournal-$GITHUB_LATEST_VERSION-windows-$ARCH.exe" 34 | Invoke-RestMethod -Uri $urlDownload -OutFile "$binPath\lazyjournal.exe" 35 | Invoke-RestMethod -Uri https://raw.githubusercontent.com/Lifailon/lazyjournal/refs/heads/main/config.yml -OutFile "$configPath\config.yml" 36 | Write-Host "✔ Installation completed " -NoNewline 37 | Write-Host "successfully" -ForegroundColor Green -NoNewline 38 | Write-Host " in " -NoNewline 39 | Write-Host "$binPath\lazyjournal.exe" -ForegroundColor Blue -NoNewline 40 | Write-Host " (version:" -NoNewline 41 | Write-Host " $GITHUB_LATEST_VERSION" -ForegroundColor Green -NoNewline 42 | Write-Host ") and configuration in" -NoNewline 43 | Write-Host " $binPath\config.yml" -ForegroundColor Blue 44 | Write-Host "To launch the interface from anywhere, re-login to the current session" 45 | } else { 46 | Write-Host "Error. " -ForegroundColor Red -NoNewline 47 | Write-Host "Unable to get the latest version from GitHub repository, check your internet connection." 48 | } 49 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | OS=$(uname -s | tr '[:upper:]' '[:lower:]') 4 | 5 | ARCH=$(uname -m) 6 | case $ARCH in 7 | x86_64|amd64) ARCH="amd64" ;; 8 | aarch64|arm64) ARCH="arm64" ;; 9 | *) 10 | echo -e "\033[31mError.\033[0m Processor architecture not supported: $ARCH" 11 | echo -e "Create a request with a \033[31mproblem\033[0m: https://github.com/Lifailon/lazyjournal/issues" 12 | exit 1 13 | ;; 14 | esac 15 | 16 | echo -e "System: \033[32m$OS\033[0m" 17 | echo -e "Architecture: \033[32m$ARCH\033[0m" 18 | 19 | case "$SHELL" in 20 | */bash) shellRc="$HOME/.bashrc" ;; # Debian/RHEL 21 | */zsh) shellRc="$HOME/.zshrc" ;; # MacOS 22 | */ksh) shellRc="$HOME/.kshrc" ;; # OpenBSD 23 | */sh) shellRc="$HOME/.shrc" ;; # FreeBSD 24 | *) 25 | shellRc="$HOME/.profile" 26 | echo -e "Shell \033[34m$SHELL\033[0m not supported, \033[32mprofile\033[0m is used to add path to environment variables" 27 | ;; 28 | esac 29 | 30 | binPath=$HOME/.local/bin/lazyjournal 31 | configPath=$HOME/.config/lazyjournal/config.yml 32 | 33 | mkdir -p $(dirname "$binPath") 34 | mkdir -p $(dirname "$configPath") 35 | touch $shellRc 36 | 37 | grep -F 'export PATH=$PATH:$HOME/.local/bin' $shellRc > /dev/null || { 38 | echo 'export PATH=$PATH:$HOME/.local/bin' >> $shellRc 39 | source "$shellRc" 2> /dev/null || . "$shellRc" 40 | echo -e "Added environment variable \033[34m$HOME/.local/bin\033[0m in \033[34m$shellRc\033[0m profile" 41 | echo -e "To launch the interface from anywhere, re-login to the current session or run the command: \033[32m. $shellRc\033[0m" 42 | } 43 | 44 | GITHUB_LATEST_VERSION=$(curl -L -sS -H 'Accept: application/json' https://github.com/Lifailon/lazyjournal/releases/latest | sed -e 's/.*"tag_name":"\([^"]*\)".*/\1/') 45 | if [ -z "$GITHUB_LATEST_VERSION" ]; then 46 | echo -e "\033[31mError.\033[0m Unable to get the latest version from GitHub repository, check your internet connection." 47 | exit 1 48 | else 49 | BIN_URL="https://github.com/Lifailon/lazyjournal/releases/download/$GITHUB_LATEST_VERSION/lazyjournal-$GITHUB_LATEST_VERSION-$OS-$ARCH" 50 | curl -L -sS "$BIN_URL" -o "$binPath" 51 | chmod +x "$binPath" 52 | if [ $OS = "darwin" ]; then 53 | xattr -d com.apple.quarantine "$binPath" 54 | fi 55 | curl -L -sS https://raw.githubusercontent.com/Lifailon/lazyjournal/main/config.yml -o "$configPath" 56 | echo -e "✔ Installation completed \033[32msuccessfully\033[0m in \033[34m$binPath\033[0m (version: \033[32m$GITHUB_LATEST_VERSION\033[0m) and configuration in \033[34m$configPath\033[0m" 57 | exit 0 58 | fi -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build source code for different architectures 2 | FROM golang:1.23-alpine3.20 AS build 3 | WORKDIR /lazyjournal 4 | COPY go.mod go.sum ./ 5 | RUN go mod download 6 | COPY . . 7 | ARG TARGETOS TARGETARCH 8 | RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} CGO_ENABLED=0 go build -o /bin/lazyjournal 9 | # Download ttyd 10 | RUN apk add -U -q --progress --no-cache curl jq 11 | RUN ARCH=$(case ${TARGETARCH} in \ 12 | "amd64") echo "x86_64" ;; \ 13 | "arm64") echo "aarch64" ;; \ 14 | *) echo "${TARGETARCH}" ;; \ 15 | esac) && \ 16 | curl -fsSL "https://github.com/tsl0922/ttyd/releases/download/1.7.7/ttyd.${ARCH}" -o /bin/ttyd 17 | # Download compose 18 | RUN ARCH=$(case ${TARGETARCH} in \ 19 | "amd64") echo "x86_64" ;; \ 20 | "arm64") echo "aarch64" ;; \ 21 | *) echo "${TARGETARCH}" ;; \ 22 | esac) && \ 23 | latest=$(curl -s https://api.github.com/repos/docker/compose/releases/latest | jq -r .tag_name) && \ 24 | curl -sSL "https://github.com/docker/compose/releases/download/${latest}/docker-compose-linux-${ARCH}" -o /bin/docker-compose 25 | # Download kubectl 26 | RUN latest=$(curl -sL https://dl.k8s.io/release/stable.txt) && \ 27 | curl -fsSL https://cdn.dl.k8s.io/release/${latest}/bin/linux/${TARGETARCH}/kubectl -o /bin/kubectl 28 | 29 | # Build docker cli 30 | FROM golang:1.23-alpine3.20 AS docker-build 31 | RUN apk add -U -q --progress --no-cache git bash coreutils gcc musl-dev 32 | WORKDIR /go/src/github.com/docker/cli 33 | RUN git clone --branch v27.0.3 --single-branch --depth 1 https://github.com/docker/cli . 34 | ARG TARGETOS TARGETARCH 35 | ENV CGO_ENABLED=0 36 | ENV GOOS=${TARGETOS} 37 | ENV GOARCH=${TARGETARCH} 38 | ENV DISABLE_WARN_OUTSIDE_CONTAINER=1 39 | RUN ./scripts/build/binary 40 | RUN mv build/docker-${TARGETOS}-${TARGETARCH} build/docker 41 | 42 | # Final image 43 | FROM debian:bookworm-slim 44 | RUN apt-get update && \ 45 | DEBIAN_FRONTEND=noninteractive \ 46 | apt-get install -y --no-install-recommends systemd \ 47 | xz-utils bzip2 gzip && \ 48 | apt-get clean && \ 49 | rm -rf /var/lib/apt/lists/* 50 | RUN mkdir -p /usr/local/lib/docker/cli-plugins 51 | COPY --from=build /bin/lazyjournal /bin/lazyjournal 52 | COPY --from=build /bin/ttyd /bin/ttyd 53 | COPY --from=build /bin/docker-compose /usr/local/lib/docker/cli-plugins/docker-compose 54 | COPY --from=build /bin/kubectl /bin/kubectl 55 | COPY --from=docker-build /go/src/github.com/docker/cli/build/docker /bin/docker 56 | 57 | WORKDIR /lazyjournal 58 | COPY config.yml entrypoint.sh ./ 59 | RUN chmod +x /bin/lazyjournal /bin/ttyd /usr/local/lib/docker/cli-plugins/docker-compose /bin/kubectl /bin/docker /lazyjournal/entrypoint.sh 60 | 61 | ENTRYPOINT ["/lazyjournal/entrypoint.sh"] -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | # ROADMAP 2 | 3 | Backlog tasks after `0.8.3` release: 4 | 5 | 28 | 29 | - [ ] Change time and date filtering to dynamically change the value. 30 | - [ ] Add a list of all Kubernetes resources to display events. 31 | - [ ] Update lists in the background to keep statuses up-to-date. 32 | - [ ] Add a nested list of containers to the `compose` stack list. 33 | - [ ] Add size to the log file list and sorting mode. 34 | - [ ] Read multiple file logs (select by pressing `space`) in real time, without displaying history. 35 | - [ ] `_all` mode for reading all Linux system logs from `journald`, without filtering by unit. 36 | - [ ] Filter by priority for logs from `journald`. 37 | - [ ] Add `json` output in tabular format and filtering mode by `level`, like in `Dozzle`. 38 | - [ ] Enable context lines like in `VSCode` to output 1, 2, or more lines above and below the found word. 39 | - [ ] Wrap mode for output. 40 | - [ ] Context manager interface for Docker and Kubernetes. 41 | - [ ] SSH connection interface with a host list in the configuration (new `ssh.hosts` structure) on `F2`. 42 | - [ ] Rewrite log reading logic (download the entire log only the first time and append changes) and remove `disableFastMode`. 43 | - [ ] Manage Docker containers and `compose` (restart, stop, access the terminal, like `lazydocker`) and `systemd` services. 44 | - [ ] Modify `compose` and `env` (like `Dockge`), as well as unit files. 45 | - [ ] Filter log files by date and size with deletion support. 46 | - [ ] Editing Kubernetes resources and restarting pods. 47 | - [ ] Reading macOS system logs. 48 | - [ ] AI-powered log analysis. 49 | - [ ] Test coverage over 80%. -------------------------------------------------------------------------------- /Supfile.yml: -------------------------------------------------------------------------------- 1 | env: 2 | GIT_URL: https://github.com/Lifailon/lazyjournal 3 | 4 | networks: 5 | dev: 6 | hosts: 7 | - lifailon@192.168.3.101:2121 8 | bsd: 9 | hosts: 10 | - root@192.168.3.102:22 11 | - root@192.168.3.103:22 12 | rpi: 13 | hosts: 14 | - lifailon@192.168.3.105:2121 15 | - lifailon@192.168.3.106:2121 16 | 17 | commands: 18 | install: 19 | desc: Install Go 20 | run: | 21 | if command -v go > /dev/null 2>&1; then 22 | go version 23 | else 24 | if [ "$(uname -s)" = "FreeBSD" ]; then 25 | pkg update 26 | pkg install -y go 27 | elif [ "$(uname -s)" = "OpenBSD" ]; then 28 | pkg_add -u 29 | yes | pkg_add go 30 | fi 31 | fi 32 | 33 | creat: 34 | desc: Creat directory for upload files 35 | run: | 36 | rm -rf docker/lazyjournal 37 | mkdir -p docker/lazyjournal 38 | 39 | copy: 40 | desc: Upload all files to remote hosts 41 | upload: 42 | - src: \* 43 | dst: docker/lazyjournal 44 | 45 | ls: 46 | desc: Check directory 47 | run: ls -lh docker/lazyjournal 48 | 49 | prep: 50 | desc: Install dependencies and code preparation 51 | run: | 52 | cd docker/lazyjournal 53 | go fmt ./... 54 | go vet ./... 55 | go get ./... 56 | go mod tidy 57 | go mod verify 58 | go build -o /dev/null -v ./... 59 | 60 | test: 61 | desc: Run unit tests 62 | run: | 63 | cd docker/lazyjournal 64 | go test -v -cover --run "TestUnixFiles|TestColor|TestFilter|TestFlags|TestCommandColor|TestCommandFuzzyFilter|TestCommandRegexFilter|TestMainInterface|TestMockInterface" 65 | 66 | docker: 67 | desc: Run docker containers test 68 | run: | 69 | cd docker/lazyjournal 70 | go test -v -cover --run "TestDockerContainer" 71 | 72 | remove: 73 | desc: Remove files on remote hosts 74 | run: | 75 | rm -rf docker/lazyjournal 76 | ls -l docker/lazyjournal 2> /dev/null || echo "lazyjournal removed" 77 | 78 | bin: 79 | desc: Install binary from GitHub 80 | run: curl -sS https://raw.githubusercontent.com/Lifailon/lazyjournal/main/install.sh | bash 81 | 82 | flags: 83 | desc: Check flags from release 84 | run: | 85 | if [ "$(uname -s)" = "FreeBSD" ]; then 86 | . ~/.shrc 87 | elif [ "$(uname -s)" = "OpenBSD" ]; then 88 | . ~/.kshrc 89 | fi 90 | lazyjournal -v 91 | mkdir -p /opt 92 | echo test > /opt/test.log 93 | lazyjournal -a 94 | rm /opt/test.log 95 | lazyjournal -g 96 | curl -s https://raw.githubusercontent.com/Lifailon/lazyjournal/refs/heads/main/color.log | lazyjournal -c 97 | curl -s https://raw.githubusercontent.com/Lifailon/lazyjournal/refs/heads/main/color.log | lazyjournal -f "success" 98 | curl -s https://raw.githubusercontent.com/Lifailon/lazyjournal/refs/heads/main/color.log | lazyjournal -r "http|127" 99 | 100 | tmux: 101 | desc: Check run interface from release 102 | run: | 103 | if [ "$(uname -s)" = "FreeBSD" ]; then 104 | . /root/.shrc 105 | elif [ "$(uname -s)" = "OpenBSD" ]; then 106 | . /root/.kshrc 107 | fi 108 | tmux new-session -d -s test-session lazyjournal 109 | sleep 1 110 | tmux capture-pane -p 111 | tmux send-keys -t test-session "$(echo -e 'syslog\t\r')" 112 | sleep 2 113 | tmux capture-pane -p 114 | tmux send-keys -t test-session "$(echo -e '\t\t\trsyslogd')" 115 | sleep 2 116 | tmux capture-pane -p 117 | tmux kill-session -t test-session 118 | 119 | targets: 120 | testing: 121 | - install 122 | - creat 123 | - copy 124 | - ls 125 | - prep 126 | - test 127 | - remove 128 | release: 129 | - bin 130 | - flags 131 | - tmux -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Local debug", 5 | "type": "go", 6 | "request": "launch", 7 | "mode": "debug", 8 | "program": "${workspaceFolder}/main.go", 9 | "console": "integratedTerminal", 10 | "args": [] 11 | }, 12 | { 13 | "name": "Remote debug", 14 | "preLaunchTask": "Run remote debug", 15 | "type": "go", 16 | "request": "attach", 17 | "mode": "remote", 18 | "host": "192.168.3.101", 19 | "port": 12345 20 | }, 21 | { 22 | "name": "SSH mode", 23 | "type": "go", 24 | "request": "launch", 25 | "mode": "debug", 26 | "program": "${workspaceFolder}/main.go", 27 | "console": "integratedTerminal", 28 | "args": ["-s", "lifailon@192.168.3.101 -p 2121"] 29 | }, 30 | { 31 | "name": "sudo SSH mode", 32 | "type": "go", 33 | "request": "launch", 34 | "mode": "debug", 35 | "program": "${workspaceFolder}/main.go", 36 | "console": "integratedTerminal", 37 | "args": ["-s", "lifailon@192.168.3.101 -p 2121 sudo"] 38 | }, 39 | { 40 | "name": "Docker remote context", 41 | "type": "go", 42 | "request": "launch", 43 | "mode": "debug", 44 | "program": "${workspaceFolder}/main.go", 45 | "console": "integratedTerminal", 46 | "args": ["-C", "hv-us-101"] 47 | }, 48 | { 49 | "name": "Kubernetes [default/default]", 50 | "type": "go", 51 | "request": "launch", 52 | "mode": "debug", 53 | "program": "${workspaceFolder}/main.go", 54 | "console": "integratedTerminal", 55 | "args": ["-k", "default", "-n", "default"] 56 | }, 57 | { 58 | "name": "Custom path [/var/lib]", 59 | "type": "go", 60 | "request": "launch", 61 | "mode": "debug", 62 | "program": "${workspaceFolder}/main.go", 63 | "console": "integratedTerminal", 64 | "args": ["-p", "/var/lib"] 65 | }, 66 | { 67 | "name": "Fast [5K/2s]", 68 | "type": "go", 69 | "request": "launch", 70 | "mode": "debug", 71 | "program": "${workspaceFolder}/main.go", 72 | "console": "integratedTerminal", 73 | "args": ["-t", "5000", "-u", "2"] 74 | }, 75 | { 76 | "name": "Max [200K/10s]", 77 | "type": "go", 78 | "request": "launch", 79 | "mode": "debug", 80 | "program": "${workspaceFolder}/main.go", 81 | "console": "integratedTerminal", 82 | "args": ["-t", "200000", "-u", "10"] 83 | }, 84 | { 85 | "name": "Disable autoupdate", 86 | "type": "go", 87 | "request": "launch", 88 | "mode": "debug", 89 | "program": "${workspaceFolder}/main.go", 90 | "console": "integratedTerminal", 91 | "args": ["-e"] 92 | }, 93 | { 94 | "name": "Disable color", 95 | "type": "go", 96 | "request": "launch", 97 | "mode": "debug", 98 | "program": "${workspaceFolder}/main.go", 99 | "console": "integratedTerminal", 100 | "args": ["-d"] 101 | }, 102 | { 103 | "name": "Disable mouse control", 104 | "type": "go", 105 | "request": "launch", 106 | "mode": "debug", 107 | "program": "${workspaceFolder}/main.go", 108 | "console": "integratedTerminal", 109 | "args": ["-m"] 110 | }, 111 | { 112 | "name": "Docker disable timestamp and only stream", 113 | "type": "go", 114 | "request": "launch", 115 | "mode": "debug", 116 | "program": "${workspaceFolder}/main.go", 117 | "console": "integratedTerminal", 118 | "args": ["-i", "-o"] 119 | }, 120 | { 121 | "name": "Audit", 122 | "type": "go", 123 | "request": "launch", 124 | "mode": "debug", 125 | "program": "${workspaceFolder}/main.go", 126 | "console": "integratedTerminal", 127 | "args": ["-a"] 128 | }, 129 | ] 130 | } 131 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | prep: 2 | @go fmt ./... 3 | @go vet ./... 4 | @go get ./... 5 | @go mod tidy 6 | @go mod verify 7 | @go build -o /dev/null -v ./... 8 | 9 | clear-cache: 10 | go clean -cache -modcache -testcache 11 | 12 | update-dependencies: 13 | go get -u ./... 14 | 15 | run: prep 16 | @go run main.go 17 | 18 | # Remote 19 | 20 | SSH_OPTIONS := lifailon@192.168.3.101 -p 2121 21 | ROOT_PATH := docker/lazyjournal 22 | GO_PATH := /usr/local/go/bin/go 23 | DLV_PATH := /home/lifailon/go/bin/dlv 24 | 25 | copy: 26 | @tar czf - . | dd status=progress | ssh $(SSH_OPTIONS) "mkdir -p $(ROOT_PATH) && rm -rf $(ROOT_PATH)/* && cd $(ROOT_PATH) && tar xzf -" 27 | 28 | run-remote: copy 29 | @ssh -t $(SSH_OPTIONS) "cd $(ROOT_PATH) && $(GO_PATH) run main.go" 30 | 31 | build-debug: 32 | @ssh $(SSH_OPTIONS) "cd $(ROOT_PATH) && $(GO_PATH) build -gcflags='all=-N -l' -o bin/debug" 33 | 34 | run-debug: copy build-debug 35 | @ssh -t $(SSH_OPTIONS) "killall dlv || true && cd $(ROOT_PATH) && $(DLV_PATH) exec bin/debug --headless --listen=:12345 --api-version=2 --log" 36 | 37 | # Linters 38 | 39 | lint-install: 40 | go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest 41 | go install github.com/go-critic/go-critic/cmd/gocritic@latest 42 | go install github.com/securego/gosec/v2/cmd/gosec@latest 43 | 44 | lint: 45 | golangci-lint run -v ./main.go 46 | gocritic check -v -enableAll ./main.go 47 | gosec -severity=high ./... 48 | 49 | lint-fix: 50 | golangci-lint run --fix ./main.go 51 | 52 | lint-all: 53 | golangci-lint run -v ./main.go --no-config --enable-all 54 | 55 | # Tests 56 | 57 | test-list: 58 | @go test -list . ./... 59 | @echo "\nTo run the selected test: \033[32mmake test n=TestMain*\033[0m\n" 60 | 61 | test: 62 | go test -v -cover --run $(n) ./... 63 | 64 | test-all: 65 | go test -v -cover ./... 66 | 67 | # Build 68 | 69 | VERSION := $(shell go run main.go -v) 70 | 71 | build-clear: 72 | @rm -rf bin 73 | 74 | build-linux-amd64: 75 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/lazyjournal-$(VERSION)-linux-amd64 76 | 77 | build-linux-arm64: 78 | CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o bin/lazyjournal-$(VERSION)-linux-arm64 79 | 80 | build-darwin-amd64: 81 | CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o bin/lazyjournal-$(VERSION)-darwin-amd64 82 | 83 | build-darwin-arm64: 84 | CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o bin/lazyjournal-$(VERSION)-darwin-arm64 85 | 86 | build-openbsd-amd64: 87 | CGO_ENABLED=0 GOOS=openbsd GOARCH=amd64 go build -o bin/lazyjournal-$(VERSION)-openbsd-amd64 88 | 89 | build-openbsd-arm64: 90 | CGO_ENABLED=0 GOOS=openbsd GOARCH=arm64 go build -o bin/lazyjournal-$(VERSION)-openbsd-arm64 91 | 92 | build-freebsd-amd64: 93 | CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -o bin/lazyjournal-$(VERSION)-freebsd-amd64 94 | 95 | build-freebsd-arm64: 96 | CGO_ENABLED=0 GOOS=freebsd GOARCH=arm64 go build -o bin/lazyjournal-$(VERSION)-freebsd-arm64 97 | 98 | build-windows-amd64: 99 | CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o bin/lazyjournal-$(VERSION)-windows-amd64.exe 100 | 101 | build-windows-arm64: 102 | CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -o bin/lazyjournal-$(VERSION)-windows-arm64.exe 103 | 104 | build-all-amd64: build-linux-amd64 build-darwin-amd64 build-openbsd-amd64 build-freebsd-amd64 build-windows-amd64 105 | 106 | build-all-arm64: build-linux-arm64 build-darwin-arm64 build-openbsd-arm64 build-freebsd-arm64 build-windows-arm64 107 | 108 | build-all: build-clear 109 | @make -j 10 build-all-amd64 build-all-arm64 110 | @ls -lh bin 111 | 112 | OS := $(shell uname -s | tr '[:upper:]' '[:lower:]') 113 | ARCH := $(shell uname -m) 114 | ifeq ($(ARCH),x86_64) 115 | ARCH := amd64 116 | else ifeq ($(ARCH),aarch64) 117 | ARCH := arm64 118 | endif 119 | 120 | build: 121 | @CGO_ENABLED=0 GOOS=$(OS) GOARCH=$(ARCH) go build -o lazyjournal 122 | 123 | run-bin: build 124 | @./lazyjournal 125 | @rm ./lazyjournal 126 | 127 | # Install 128 | 129 | BIN_PATH := $(HOME)/.local/bin 130 | 131 | install: build 132 | @mkdir -p $(BIN_PATH) 133 | @mv ./lazyjournal $(BIN_PATH)/lazyjournal 134 | 135 | uninstall: 136 | rm -f $(shell which lazyjournal) 137 | 138 | # Docker 139 | 140 | docker-build: 141 | @docker build -t lifailon/lazyjournal:latest . 142 | 143 | compose-up: 144 | @docker compose up -d 145 | @docker exec -it lazyjournal lazyjournal 146 | 147 | compose-down: 148 | @docker compose down 149 | @docker rmi lifailon/lazyjournal -------------------------------------------------------------------------------- /color.log: -------------------------------------------------------------------------------- 1 | ⎯⎯⎯ URL ⎯⎯⎯ 2 | 3 | http://127.0.0.1 4 | http://127.0.0.1:8443 5 | http://127.0.0.1:8443/api 6 | https://github.com/Lifailon/lazyjournal 7 | https://torapi.vercel.app/api/search/title/kinozal?query=The+Rookie&category=0&page=0&year=0&format=0 8 | 9 | ⎯⎯⎯ JSON (double quotes and braces) ⎯⎯⎯ 10 | 11 | {"level":"info","version":"v8.14.6","time":"2025-12-05T03:23:46Z","message":"Dozzle version v8.14.6"} 12 | {"level":"error","version":"v8.14.6","host":"rpi-106","error":"unexpected EOF","time":"2025-12-06T08:32:37Z","message":"unexpected error while listening to docker events"} 13 | 14 | ⎯⎯⎯ UNIX file paths ⎯⎯⎯ 15 | 16 | ~/.local/bin/lazyjournal 17 | ./lazyjournal 18 | /var/log/syslog 19 | 20 | ⎯⎯⎯ UNIX processes ⎯⎯⎯ 21 | 22 | systemd[1]: system started 23 | dockerd[123]: container stopped 24 | kernel: system running 25 | rsyslogd: rsyslog running 26 | sudo: service was restarted 27 | 28 | ⎯⎯⎯ HTTP methods ⎯⎯⎯ 29 | 30 | GET 31 | POST 32 | PUT 33 | PATCH 34 | TRACE 35 | DELETE 36 | CONNECT 37 | 38 | ⎯⎯⎯ Statuses ⎯⎯⎯ 39 | 40 | OK, DONE, DEBUG, INFO, NOTICE, WARN, WARNING 41 | ERR, ERROR, CRIT, ALERT, EMERG, FAIL, FATAL 42 | 43 | ⎯⎯⎯ Numbers ⎯⎯⎯ 44 | 45 | Byte: 0x04 46 | DateTime: 2025-02-26T21:38:35.956968+03:00 47 | Timestamp: 2025-04-07T14:10:09.028022923Z 48 | Date: 20.03.2025, 2025.03.20, 2025-03-20, 20-03-2025, 20/03/2025, 2025/03/20 49 | Time: 11:11, 11:11:11 50 | MAC address: 11:11:11:11:11:11, 11-11-11-11-11-11 51 | IP address: 127.0.0.1, 127.0.0.1:8443, 255.255.255.255/24 52 | Versions: 1.0, 1.0.7 53 | Percentage: 1%, 50%, 100% 54 | Integers: 1, 10, 100, (1), [10], {100}, "1000" 55 | 56 | ⎯⎯⎯ Warnings and known names (yellow) ⎯⎯⎯ 57 | 58 | lifailon, root, daemon 59 | warnings, warning, warn 60 | 61 | ⎯⎯⎯ Success (green) ⎯⎯⎯ 62 | 63 | unblocking, unblocked, unblock 64 | successfully, successful, succeeded, succeed, success 65 | completed, completing, completion, completes, complete 66 | accepted, accepting, acception, acceptance, acceptable, acceptably, accepte, accepts, accept 67 | connected, connecting, connection, connects, connect 68 | finished, finishing, finish 69 | started, starting, startup, start 70 | enabled, enables, enable 71 | allowed, allowing, allow 72 | passed, passing 73 | ready, available, running, installed 74 | true, ok, done, yes 75 | 76 | ⎯⎯⎯ Errors (red) ⎯⎯⎯ 77 | 78 | stderr, errors, error, erro, err 79 | disconnected, disconnection, disconnects, disconnect, disabled, disabling, disable 80 | crashed, crashing, crash 81 | deletion, deleted, deleting, deletes, delete 82 | removing, removed, removes, remove 83 | stopping, stopped, stoped, stops, stop 84 | invalidation, invalidating, invalidated, invalidate, invalid 85 | aborted, aborting, abort 86 | blocked, blocker, blocking, blocks, block 87 | inactive, deactivated, deactivating, deactivate 88 | exited, exiting, exits, exit 89 | critical, critic, crit 90 | failed, failure, failing, fails, fail 91 | fatality, fataling, fatals, fatal 92 | closed, closing, close 93 | dropped, droping, drops, drop 94 | panicked, panics, panic 95 | emergency, emerg 96 | rejecting, rejection, rejected, reject 97 | refusing, refused, refuses, refuse 98 | denied, unavailable, unknown, unsuccessful, unauthorized, forbidden, conflict, severe 99 | false, null, nil, none, no 100 | Not found 101 | Bad request 102 | 103 | ⎯⎯⎯ Actions (blue) ⎯⎯⎯ 104 | 105 | session 106 | logged, login 107 | registered, registration 108 | authenticating, authentication, authenticate, authorization 109 | return, returne, returned 110 | listening, listener, listen 111 | opening, opened, open 112 | created, creating, creates, create 113 | resolved, resolving, resolve, restarting, restarted, restart 114 | reboot, booting, boot 115 | overloading, overloaded, overload, uploading, uploaded, uploads, upload 116 | downloading, downloaded, downloads, download, loading, loaded, load 117 | updates, updated, updating, update 118 | upgrades, upgraded, upgrading, upgrade, setup 119 | synchronization, synchronize, sync, syn 120 | launched, launching, launch 121 | changed, changing, change 122 | cleaning, cleaner, clearing, cleared, clear 123 | skipping, skipped, skip 124 | missing, missed 125 | mountpoint, mounted, mounting, mount 126 | configurations, configuration, configuring, configured, configure, config 127 | reading, readed, read 128 | writing, writed, write 129 | saved, saving, save 130 | paused, pausing, pause 131 | normal, norm 132 | alerting, alert 133 | notifications, notification, notify, noting, notice 134 | informations, information, informing, informed, info 135 | installation, installing, install, initialization, initial 136 | stdout, timeout 137 | debug, verbose, level, status 138 | shutdown, protocol -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/awesome-gocui/gocui v1.1.0 h1:db2j7yFEoHZjpQFeE2xqiatS8bm1lO3THeLwE6MzOII= 2 | github.com/awesome-gocui/gocui v1.1.0/go.mod h1:M2BXkrp7PR97CKnPRT7Rk0+rtswChPtksw/vRAESGpg= 3 | github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= 4 | github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw= 5 | github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo= 6 | github.com/gdamore/tcell/v2 v2.4.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU= 7 | github.com/gdamore/tcell/v2 v2.8.1 h1:KPNxyqclpWpWQlPLx6Xui1pMk8S+7+R37h3g07997NU= 8 | github.com/gdamore/tcell/v2 v2.8.1/go.mod h1:bj8ori1BG3OYMjmb3IklZVWfZUJ1UBQt9JXrOCOhGWw= 9 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 10 | github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 11 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 12 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 13 | github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 14 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 15 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 16 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 17 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 18 | github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 19 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 20 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 21 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 22 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 23 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 24 | golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 25 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 26 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 27 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 28 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 29 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 30 | golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 31 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 32 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 33 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 34 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 35 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 36 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 37 | golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= 38 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 39 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 40 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 41 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 42 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 43 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 44 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 45 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 46 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 47 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 48 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 49 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 50 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 51 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 52 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 53 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 54 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 55 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 56 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 57 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 58 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 59 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 60 | golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= 61 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 62 | golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 63 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 64 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 65 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 66 | golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= 67 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 68 | golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= 69 | golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= 70 | golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= 71 | golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= 72 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 73 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 74 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 75 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 76 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 77 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 78 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 79 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 80 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 81 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 82 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 83 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 84 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 85 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 86 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 87 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 88 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 89 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 90 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 91 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 92 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 93 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 94 | -------------------------------------------------------------------------------- /.github/workflows/merge-coverage.yml: -------------------------------------------------------------------------------- 1 | name: Merge Coverage 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | Debug: 7 | description: 'Advanced summary report from tests' 8 | default: false 9 | type: boolean 10 | Linter: 11 | description: 'Check linters for final report' 12 | default: false 13 | type: boolean 14 | Coverage: 15 | description: 'Update html and svg reports on Wiki' 16 | default: false 17 | type: boolean 18 | Markdown: 19 | description: 'Update markdown report on Wiki' 20 | default: false 21 | type: boolean 22 | 23 | permissions: 24 | contents: write 25 | 26 | jobs: 27 | Linux: 28 | runs-on: ubuntu-24.04 29 | 30 | steps: 31 | - name: Clone main repository 32 | uses: actions/checkout@v4 33 | 34 | - name: Install Go 35 | uses: actions/setup-go@v5 36 | with: 37 | go-version: 1.25 38 | 39 | - name: Install dependencies 40 | run: | 41 | go fmt ./... 42 | go vet ./... 43 | go get ./... 44 | go mod tidy 45 | go mod verify 46 | go build -v ./... 47 | 48 | - name: Start docker container for test in Ubuntu 49 | run: | 50 | version=$(curl -s https://api.github.com/repos/docker/compose/releases/latest | jq -r .tag_name) 51 | curl -L "https://github.com/docker/compose/releases/download/$version/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose 52 | sudo chmod +x /usr/local/bin/docker-compose 53 | docker-compose --version 54 | sed "s/TTYD=false/TTYD=true/" docker-compose.yml -i 55 | docker-compose up -d 56 | docker run -d --name pinguem -p 8085:8085 -p 3005:3005 lifailon/pinguem:latest 57 | 58 | - name: Create pcap files for Linux 59 | run: | 60 | sudo tcpdump -i any -c 1 -w test.pcap 61 | gzip -c test.pcap > test.pcap.gz 62 | sudo tcpdump -i any -c 1 -w test.pcapng 63 | gzip -c test.pcapng > test.pcapng.gz 64 | ls -lh 65 | continue-on-error: true 66 | 67 | - name: Install tailspin 68 | run: sudo apt install -y tailspin 69 | 70 | - name: Run all unit test in Linux 71 | run: sudo go test -v -cover -coverprofile linux-coverage.out 72 | continue-on-error: true 73 | timeout-minutes: 10 74 | 75 | - name: Create markdown report for Linux 76 | run: | 77 | mv test-report.md test-linux-report.md 78 | if [[ "${{ inputs.Debug }}" == "true" ]]; then 79 | cat test-linux-report.md >> $GITHUB_STEP_SUMMARY 80 | fi 81 | continue-on-error: true 82 | 83 | - name: Upload test summary report 84 | uses: actions/upload-artifact@v4 85 | with: 86 | name: linux-report 87 | path: test-linux-report.md 88 | 89 | - name: Upload coverage report 90 | uses: actions/upload-artifact@v4 91 | with: 92 | name: linux-coverage 93 | path: linux-coverage.out 94 | 95 | Docker: 96 | runs-on: ubuntu-24.04 97 | 98 | steps: 99 | - name: Clone main repository 100 | uses: actions/checkout@v4 101 | 102 | - name: Install Go 103 | uses: actions/setup-go@v5 104 | with: 105 | go-version: 1.25 106 | 107 | - name: Install dependencies 108 | run: | 109 | go fmt ./... 110 | go vet ./... 111 | go get ./... 112 | go mod tidy 113 | go mod verify 114 | go build -v ./... 115 | 116 | - name: Start docker container for test in Ubuntu 117 | run: | 118 | version=$(curl -s https://api.github.com/repos/docker/compose/releases/latest | jq -r .tag_name) 119 | curl -L "https://github.com/docker/compose/releases/download/$version/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose 120 | sudo chmod +x /usr/local/bin/docker-compose 121 | docker-compose --version 122 | sed "s/TTYD=false/TTYD=true/" docker-compose.yml -i 123 | docker-compose up -d 124 | docker run -d --name pinguem -p 8085:8085 -p 3005:3005 lifailon/pinguem:latest 125 | 126 | - name: Run unit test for docker in Linux without root 127 | run: go test -v --run "TestDockerContainer" -cover -coverprofile docker-coverage.out 128 | continue-on-error: true 129 | timeout-minutes: 5 130 | 131 | - name: Upload coverage report 132 | uses: actions/upload-artifact@v4 133 | with: 134 | name: docker-coverage 135 | path: docker-coverage.out 136 | 137 | macOS: 138 | runs-on: macos-15 139 | 140 | steps: 141 | - name: Clone main repository 142 | uses: actions/checkout@v4 143 | 144 | - name: Install Go 145 | uses: actions/setup-go@v5 146 | with: 147 | go-version: 1.25 148 | 149 | - name: Install dependencies 150 | run: | 151 | go fmt ./... 152 | go vet ./... 153 | go get ./... 154 | go mod tidy 155 | go mod verify 156 | go build -v ./... 157 | 158 | - name: Run unit test in macOS 159 | run: sudo go test -v --run "TestUnixFiles|TestFlags|TestCommandColor|TestCommandFuzzyFilter|TestCommandRegexFilter|TestMainInterface" -cover -coverprofile macos-coverage.out 160 | continue-on-error: true 161 | timeout-minutes: 5 162 | 163 | - name: Create markdown report for macOS 164 | run: | 165 | mv test-report.md test-macos-report.md 166 | if [[ "${{ inputs.Debug }}" == "true" ]]; then 167 | cat test-macos-report.md >> $GITHUB_STEP_SUMMARY 168 | fi 169 | continue-on-error: true 170 | 171 | - name: Upload test summary report 172 | uses: actions/upload-artifact@v4 173 | with: 174 | name: macos-report 175 | path: test-macos-report.md 176 | 177 | - name: Upload coverage report 178 | uses: actions/upload-artifact@v4 179 | with: 180 | name: macos-coverage 181 | path: macos-coverage.out 182 | 183 | Windows: 184 | runs-on: windows-2022 185 | steps: 186 | - name: Clone main repository 187 | uses: actions/checkout@v4 188 | 189 | - name: Install Go 190 | uses: actions/setup-go@v5 191 | with: 192 | go-version: 1.25 193 | 194 | - name: Install dependencies 195 | run: | 196 | go fmt ./... 197 | go vet ./... 198 | go get ./... 199 | go mod tidy 200 | go mod verify 201 | go build -v ./... 202 | 203 | - name: Create log file for Windows 204 | run: | 205 | New-Item -Path "$env:APPDATA\test" -Type Directory 206 | "line test" | Out-File -FilePath "$env:APPDATA\test\test.log" 207 | shell: pwsh 208 | continue-on-error: true 209 | 210 | - name: Run unit test in Windows 211 | run: go test -v --run "TestWin.*|TestFlags|TestCommandColor|TestCommandFuzzyFilter|TestCommandRegexFilter|TestMainInterface|TestMockInterface" -cover -coverprofile windows-coverage.out 212 | continue-on-error: true 213 | timeout-minutes: 5 214 | 215 | - name: Create markdown report for Windows 216 | run: | 217 | Rename-Item -Path test-report.md -NewName test-windows-report.md 218 | if ($env:DEBUG -eq "true") { 219 | Get-Content test-windows-report.md | Out-File -Append -FilePath $env:GITHUB_STEP_SUMMARY 220 | } 221 | shell: pwsh 222 | continue-on-error: true 223 | env: 224 | DEBUG: ${{ inputs.Debug }} 225 | 226 | - name: Upload test summary report 227 | uses: actions/upload-artifact@v4 228 | with: 229 | name: windows-report 230 | path: test-windows-report.md 231 | 232 | - name: Upload coverage report 233 | uses: actions/upload-artifact@v4 234 | with: 235 | name: windows-coverage 236 | path: windows-coverage.out 237 | 238 | Merge: 239 | runs-on: ubuntu-latest 240 | needs: [Linux, Docker, macOS, Windows] 241 | steps: 242 | - name: Clone main repository 243 | uses: actions/checkout@v4 244 | 245 | - name: Install Go 246 | uses: actions/setup-go@v5 247 | with: 248 | go-version: 1.25 249 | 250 | - name: Download coverage report for Linux (with root) 251 | uses: actions/download-artifact@v4 252 | with: 253 | name: linux-coverage 254 | path: . 255 | 256 | - name: Download coverage report for Docker without root 257 | uses: actions/download-artifact@v4 258 | with: 259 | name: docker-coverage 260 | path: . 261 | 262 | - name: Download coverage report for macOS 263 | uses: actions/download-artifact@v4 264 | with: 265 | name: macos-coverage 266 | path: . 267 | 268 | - name: Download coverage report for Windows 269 | uses: actions/download-artifact@v4 270 | with: 271 | name: windows-coverage 272 | path: . 273 | 274 | - name: Check coverage report for all tests on Linux with root 275 | run: | 276 | out=$(go tool cover -func linux-coverage.out) 277 | echo "# 🧪 Test Coverage" >> $GITHUB_STEP_SUMMARY 278 | coverTotal=$(echo "$out" | tail -n 1 | sed -E "s/.+\)\s+//") 279 | echo "Linux coverage: $coverTotal" 280 | echo "- **Linux**: $coverTotal" >> $GITHUB_STEP_SUMMARY 281 | cat "$GITHUB_STEP_SUMMARY" > all-report.md 282 | 283 | - name: Check coverage report for Docker without root 284 | run: | 285 | out=$(go tool cover -func docker-coverage.out) 286 | coverTotal=$(echo "$out" | tail -n 1 | sed -E "s/.+\)\s+//") 287 | echo "Docker coverage: $coverTotal" 288 | 289 | - name: Check coverage report for macOS 290 | run: | 291 | out=$(go tool cover -func macos-coverage.out) 292 | coverTotal=$(echo "$out" | tail -n 1 | sed -E "s/.+\)\s+//") 293 | echo "macOS coverage: $coverTotal" 294 | echo "- **macOS**: $coverTotal" >> $GITHUB_STEP_SUMMARY 295 | cat "$GITHUB_STEP_SUMMARY" >> all-report.md 296 | 297 | - name: Check coverage report for Windows 298 | run: | 299 | out=$(go tool cover -func windows-coverage.out) 300 | coverTotal=$(echo "$out" | tail -n 1 | sed -E "s/.+\)\s+//") 301 | echo "Windows coverage: $coverTotal" 302 | echo "- **Windows**: $coverTotal" >> $GITHUB_STEP_SUMMARY 303 | cat "$GITHUB_STEP_SUMMARY" >> all-report.md 304 | 305 | - name: Install go coverage merge 306 | run: | 307 | go install github.com/wadey/gocovmerge@latest 308 | echo "$HOME/go/bin" >> $GITHUB_PATH 309 | export PATH=$HOME/go/bin:$PATH 310 | which gocovmerge 311 | 312 | - name: Merge coverage reports 313 | run: | 314 | gocovmerge linux-coverage.out docker-coverage.out macos-coverage.out windows-coverage.out > merge-coverage.out 315 | out=$(go tool cover -func merge-coverage.out) 316 | coverTotal=$(echo "$out" | tail -n 1 | sed -E "s/.+\)\s+//") 317 | echo "Total after merge coverage: $coverTotal" 318 | echo "- **Total after merge**: $coverTotal" >> $GITHUB_STEP_SUMMARY 319 | mdReport=$(echo "$out" | sed -E 's/.+\///' | sed "s/:/ /g" | head -n -1) 320 | title="File Line Function Coverage" 321 | separator="- - - -" 322 | echo "# 📚 Merge test results" >> $GITHUB_STEP_SUMMARY 323 | mdReport=$(echo -e "$title\n$separator\n$mdReport") 324 | echo "$mdReport" | awk '{print "|" $1 "|" $2 "|" $3 "|" $4 "|"}' >> $GITHUB_STEP_SUMMARY 325 | cat "$GITHUB_STEP_SUMMARY" >> all-report.md 326 | 327 | - name: Linters reports 328 | if: ${{ github.event.inputs.Linter == 'true' }} 329 | run: | 330 | echo "# 👮‍♂️ Linters check" >> $GITHUB_STEP_SUMMARY 331 | echo "## GolangCI" >> $GITHUB_STEP_SUMMARY 332 | go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest 333 | echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY 334 | golangci-lint run -v ./main.go 2>&1 | tee -a $GITHUB_STEP_SUMMARY 335 | echo "\`\`\`" >> $GITHUB_STEP_SUMMARY 336 | echo "## Go Critic" >> $GITHUB_STEP_SUMMARY 337 | go install github.com/go-critic/go-critic/cmd/gocritic@latest 338 | echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY 339 | gocritic check -v -enableAll ./main.go 2>&1 | tee -a $GITHUB_STEP_SUMMARY 340 | echo "\`\`\`" >> $GITHUB_STEP_SUMMARY 341 | echo "## Go Security" >> $GITHUB_STEP_SUMMARY 342 | go install github.com/securego/gosec/v2/cmd/gosec@latest 343 | echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY 344 | gosec -severity=high ./... 2>&1 | tee -a $GITHUB_STEP_SUMMARY 345 | echo "\`\`\`" >> $GITHUB_STEP_SUMMARY 346 | cat "$GITHUB_STEP_SUMMARY" >> all-report.md 347 | 348 | - name: Upload merge coverage report 349 | uses: actions/upload-artifact@v4 350 | with: 351 | name: merge-coverage 352 | path: merge-coverage.out 353 | 354 | - name: Update html and svg reports on Wiki 355 | if: ${{ github.event.inputs.Coverage == 'true' }} 356 | uses: ncruces/go-coverage-report@v0 357 | with: 358 | coverage-file: merge-coverage.out 359 | report: true 360 | chart: true 361 | amend: true 362 | continue-on-error: true 363 | 364 | - name: Download test summary report for Linux 365 | uses: actions/download-artifact@v4 366 | with: 367 | name: linux-report 368 | path: . 369 | 370 | - name: Download test summary report for macOS 371 | uses: actions/download-artifact@v4 372 | with: 373 | name: macos-report 374 | path: . 375 | 376 | - name: Download test summary report for Windows 377 | uses: actions/download-artifact@v4 378 | with: 379 | name: windows-report 380 | path: . 381 | 382 | - name: Merge all reports 383 | run: | 384 | echo "# 🐧 Linux" >> all-report.md 385 | cat "test-linux-report.md" >> all-report.md 386 | echo "# 🍏 macOS" >> all-report.md 387 | cat "test-macos-report.md" >> all-report.md 388 | echo "# 🏠 Windows" >> all-report.md 389 | cat "test-windows-report.md" >> all-report.md 390 | 391 | - name: Upload final report 392 | uses: actions/upload-artifact@v4 393 | with: 394 | name: all-report 395 | path: all-report.md 396 | 397 | - name: Clone Wiki repository 398 | if: ${{ github.event.inputs.Markdown == 'true' }} 399 | run: git clone https://github.com/Lifailon/lazyjournal.wiki wiki 400 | 401 | - name: Update markdown report on Wiki 402 | if: ${{ github.event.inputs.Markdown == 'true' }} 403 | run: | 404 | echo -e '# Test report\n' > wiki/home.md 405 | echo -e 'The test coverage report in `html` format using [go-coverage-report](https://github.com/marketplace/actions/go-coverage-report) is available [here](https://raw.githubusercontent.com/wiki/Lifailon/lazyjournal/coverage.html).\n' >> wiki/home.md 406 | echo -e 'During testing, the interface operation is checked in the disabled `tcell` mode, as well as the reading and coloring time of all available logs in Linux, macOS and Windows systems.\n' >> wiki/home.md 407 | echo -e 'To combine test results from different systems [gosovmerge](https://github.com/wadey/gocovmerge) is used.\n' >> wiki/home.md 408 | echo -e '# 📈 Coverage history\n' >> wiki/home.md 409 | echo -e '![](https://raw.githubusercontent.com/wiki/Lifailon/lazyjournal/coverage-chart.svg)\n' >> wiki/home.md 410 | cat all-report.md >> wiki/home.md 411 | cd wiki 412 | git config --global user.name 'GitHub Actions' 413 | git config --global user.email 'actions@github.com' 414 | git add home.md 415 | if ! git diff --staged --quiet; then 416 | git diff 417 | git commit -m "update markdown report" 418 | git push https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/Lifailon/lazyjournal.wiki.git master 419 | else 420 | echo "No changes to commit" 421 | fi -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | Distro: 7 | description: 'Select runner image' # https://github.com/actions/runner-images 8 | required: true 9 | default: 'ubuntu-24.04' 10 | type: choice 11 | options: 12 | - 'ubuntu-24.04' 13 | - 'macos-15' 14 | - 'windows-2022' 15 | Update: 16 | description: 'Update dependencies' 17 | default: false 18 | type: boolean 19 | GoLangCI: 20 | description: 'GoLangCI linters check' 21 | default: false 22 | type: boolean 23 | GoCritic: 24 | description: 'GoCritic linters check' 25 | default: false 26 | type: boolean 27 | GoSec: 28 | description: 'GoSec linters check' 29 | default: false 30 | type: boolean 31 | Test: 32 | description: 'Go unit testing' 33 | default: false 34 | type: boolean 35 | Release: 36 | description: 'Release check' 37 | default: false 38 | type: boolean 39 | Binary: 40 | description: 'Build binary' 41 | default: false 42 | type: boolean 43 | Docker: 44 | description: 'Build and publish on Docker Hub' 45 | default: false 46 | type: boolean 47 | 48 | run-name: CI on ${{ github.event.inputs.Distro }} [#${{ github.run_number }}] 49 | 50 | jobs: 51 | test: 52 | runs-on: ${{ github.event.inputs.Distro }} 53 | 54 | name: Tests on ${{ github.event.inputs.Distro }} 55 | 56 | steps: 57 | - name: Clone main repository 58 | uses: actions/checkout@v4 59 | 60 | - name: Install Go 61 | uses: actions/setup-go@v5 62 | with: 63 | go-version: 1.25 64 | 65 | - name: Install dependencies 66 | run: | 67 | go fmt ./... 68 | go vet ./... 69 | go get ./... 70 | go mod tidy 71 | go mod verify 72 | go build -v ./... 73 | 74 | - name: Get build information 75 | shell: bash 76 | run: | 77 | version=$(go run main.go -v) 78 | echo "## 🚀 Build Information" >> $GITHUB_STEP_SUMMARY 79 | echo "- **Build number**: #${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY 80 | echo "- **OS**: ${{ github.event.inputs.Distro }}" >> $GITHUB_STEP_SUMMARY 81 | echo "- **Version**: $version" >> $GITHUB_STEP_SUMMARY 82 | echo "- **Update dependencies**: ${{ github.event.inputs.Update }}" >> $GITHUB_STEP_SUMMARY 83 | echo "- **GoLangCI linters check**: ${{ github.event.inputs.GoLangCI }}" >> $GITHUB_STEP_SUMMARY 84 | echo "- **Go critic linters check**: ${{ github.event.inputs.GoCritic }}" >> $GITHUB_STEP_SUMMARY 85 | echo "- **Go security linters check**: ${{ github.event.inputs.GoSec }}" >> $GITHUB_STEP_SUMMARY 86 | echo "- **Go unit testing**: ${{ github.event.inputs.Test }}" >> $GITHUB_STEP_SUMMARY 87 | echo "- **Release Check**: ${{ github.event.inputs.Release }}" >> $GITHUB_STEP_SUMMARY 88 | echo "- **Build binary**: ${{ github.event.inputs.Binary }}" >> $GITHUB_STEP_SUMMARY 89 | echo "- **Build and publish on Docker Hub**: ${{ github.event.inputs.Docker }}" >> $GITHUB_STEP_SUMMARY 90 | 91 | - name: Update dependencies 92 | if: ${{ github.event.inputs.Update == 'true' }} 93 | run: go get -u ./... 94 | 95 | # Linters 96 | 97 | - name: Main linters check 98 | if: ${{ github.event.inputs.GoLangCI == 'true' }} 99 | continue-on-error: true 100 | run: | 101 | go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest 102 | golangci-lint run -v ./main.go 103 | 104 | - name: Critic linters check 105 | if: ${{ github.event.inputs.GoCritic == 'true' }} 106 | continue-on-error: true 107 | run: | 108 | go install github.com/go-critic/go-critic/cmd/gocritic@latest 109 | gocritic check -v -enableAll ./main.go 110 | 111 | - name: Security linters check 112 | if: ${{ github.event.inputs.GoSec == 'true' }} 113 | continue-on-error: true 114 | run: | 115 | go install github.com/securego/gosec/v2/cmd/gosec@latest 116 | gosec -severity=high ./... 117 | 118 | # Linux 119 | 120 | - name: Start docker container for test in Ubuntu 121 | if: ${{ (github.event.inputs.Release == 'true' || github.event.inputs.Test == 'true') && github.event.inputs.Distro != 'macos-15' && github.event.inputs.Distro != 'windows-2022' }} 122 | run: | 123 | version=$(curl -s https://api.github.com/repos/docker/compose/releases/latest | jq -r .tag_name) 124 | curl -L "https://github.com/docker/compose/releases/download/$version/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose 125 | sudo chmod +x /usr/local/bin/docker-compose 126 | docker-compose --version 127 | sed "s/TTYD=false/TTYD=true/" docker-compose.yml -i 128 | docker-compose up -d 129 | docker run -d --name pinguem -p 8085:8085 -p 3005:3005 lifailon/pinguem:latest 130 | 131 | - name: Create pcap files for Linux 132 | if: ${{ github.event.inputs.Test == 'true' && github.event.inputs.Distro != 'macos-15' && github.event.inputs.Distro != 'windows-2022' }} 133 | run: | 134 | sudo tcpdump -i any -c 1 -w test.pcap 135 | gzip -c test.pcap > test.pcap.gz 136 | sudo tcpdump -i any -c 1 -w test.pcapng 137 | gzip -c test.pcapng > test.pcapng.gz 138 | ls -lh 139 | 140 | - name: Install bat and tailspin 141 | if: ${{ github.event.inputs.Test == 'true' && github.event.inputs.Distro != 'macos-15' && github.event.inputs.Distro != 'windows-2022' }} 142 | run: | 143 | sudo apt install bat 144 | bat --version || batcat --version || true 145 | version=$(curl -s https://api.github.com/repos/bensadeh/tailspin/releases/latest | jq -r .tag_name) 146 | curl -L "https://github.com/bensadeh/tailspin/releases/download/$version/tailspin-$(uname -m)-unknown-$(uname -s | tr '[:upper:]' '[:lower:]')-musl.tar.gz" -o /usr/local/bin/tailspin.tar.gz 147 | tar -xzf /usr/local/bin/tailspin.tar.gz -C /usr/local/bin 148 | mv /usr/local/bin/tspin /usr/local/bin/tailspin 149 | chmod +x /usr/local/bin/tailspin 150 | rm /usr/local/bin/*.tar.gz 151 | tailspin --version 152 | 153 | - name: Unit testing (all functions and mock interface) in Linux 154 | if: ${{ github.event.inputs.Test == 'true' && github.event.inputs.Distro != 'macos-15' && github.event.inputs.Distro != 'windows-2022' }} 155 | continue-on-error: true 156 | timeout-minutes: 10 157 | run: | 158 | sudo env "PATH=$PATH" go test -v -cover | tee test.log 159 | echo "## 🧪 Test Coverage" >> $GITHUB_STEP_SUMMARY 160 | cat test.log | tail -n 2 | head -n 1 | sed "s/coverage: //" | sed -E "s/of.+//g" >> $GITHUB_STEP_SUMMARY 161 | 162 | # + Docker without root 163 | 164 | - name: Unit testing for docker without root in Linux 165 | if: ${{ github.event.inputs.Test == 'true' && github.event.inputs.Distro != 'macos-15' && github.event.inputs.Distro != 'windows-2022' }} 166 | continue-on-error: true 167 | timeout-minutes: 5 168 | run: env "PATH=$PATH" go test -v -cover --run "TestDockerContainer" 169 | 170 | - name: Check cli mode (color and filter) for container 171 | if: ${{ github.event.inputs.Test == 'true' && github.event.inputs.Distro != 'macos-15' && github.event.inputs.Distro != 'windows-2022' }} 172 | continue-on-error: true 173 | run: | 174 | docker logs lazyjournal | go run main.go -c 175 | docker logs lazyjournal | go run main.go -f "start" 176 | docker logs lazyjournal | go run main.go -r "start|close" 177 | 178 | - name: Create markdown report in Linux 179 | if: ${{ github.event.inputs.Test == 'true' && github.event.inputs.Distro != 'macos-15' && github.event.inputs.Distro != 'windows-2022' }} 180 | continue-on-error: true 181 | run: cat test-report.md >> $GITHUB_STEP_SUMMARY 182 | 183 | # macOS 184 | 185 | - name: Unit testing (files, flags, cli and run main interface) in macOS 186 | if: ${{ github.event.inputs.Test == 'true' && github.event.inputs.Distro == 'macos-15' }} 187 | continue-on-error: true 188 | timeout-minutes: 5 189 | shell: bash 190 | run: | 191 | sudo go test -v -cover --run "TestUnixFiles|TestFlags|TestCommandColor|TestCommandFuzzyFilter|TestCommandRegexFilter|TestMainInterface" | tee test.log 192 | echo "## 🧪 Test Coverage" >> $GITHUB_STEP_SUMMARY 193 | cat test.log | tail -n 2 | head -n 1 | sed "s/coverage: //" | sed -E "s/of.+//g" >> $GITHUB_STEP_SUMMARY 194 | 195 | - name: Create markdown report in macOS 196 | if: ${{ github.event.inputs.Test == 'true' && github.event.inputs.Distro == 'macos-15' }} 197 | continue-on-error: true 198 | shell: bash 199 | run: cat test-report.md >> $GITHUB_STEP_SUMMARY 200 | 201 | # Windows 202 | 203 | - name: Create log file for Windows 204 | if: ${{ github.event.inputs.Test == 'true' && github.event.inputs.Distro == 'windows-2022' }} 205 | continue-on-error: true 206 | shell: pwsh 207 | run: | 208 | New-Item -Path "$env:APPDATA\test" -Type Directory 209 | "line test" | Out-File -FilePath "$env:APPDATA\test\test.log" 210 | 211 | - name: Unit testing (events, files, flags, cli, main and mock interface) in Windows 212 | if: ${{ github.event.inputs.Test == 'true' && github.event.inputs.Distro == 'windows-2022' }} 213 | continue-on-error: true 214 | timeout-minutes: 5 215 | shell: bash 216 | run: | 217 | go test -v -cover --run "TestWin.*|TestFlags|TestCommandColor|TestCommandFuzzyFilter|TestCommandRegexFilter|TestMainInterface|TestMockInterface" | tee test.log 218 | echo "## 🧪 Test Coverage" >> $GITHUB_STEP_SUMMARY 219 | cat test.log | tail -n 2 | head -n 1 | sed "s/coverage: //" | sed -E "s/of.+//g" >> $GITHUB_STEP_SUMMARY 220 | 221 | - name: Create markdown report in Windows 222 | if: ${{ github.event.inputs.Test == 'true' && github.event.inputs.Distro == 'windows-2022' }} 223 | continue-on-error: true 224 | shell: pwsh 225 | run: Get-Content test-report.md | Out-File -Append -FilePath $env:GITHUB_STEP_SUMMARY 226 | 227 | # Release (Flags and TMUX) 228 | 229 | - name: Install binary from last commit on GitHub 230 | if: ${{ github.event.inputs.Release == 'true' }} 231 | run: | 232 | LAST_COMMIT_HASH=$(git ls-remote https://github.com/lifailon/lazyjournal HEAD | awk '{print $1}') 233 | go install github.com/Lifailon/lazyjournal@$LAST_COMMIT_HASH 234 | 235 | - name: Check flags (cli mode) 236 | if: ${{ github.event.inputs.Release == 'true' }} 237 | run: | 238 | echo -e "\n\033[32mVersion:\033[0m\n" 239 | lazyjournal -v 240 | echo -e "\n\033[32mConfig:\033[0m\n" 241 | lazyjournal -g 242 | echo -e "\n\033[32mAudit:\033[0m\n" 243 | lazyjournal -a 244 | echo -e "\n\033[32mColoring:\033[0m\n" 245 | curl -s https://raw.githubusercontent.com/Lifailon/lazyjournal/refs/heads/main/color.log | lazyjournal -c 246 | echo -e "\n\033[32mFiltering in fuzzy mode:\033[0m\n" 247 | curl -s https://raw.githubusercontent.com/Lifailon/lazyjournal/refs/heads/main/color.log | lazyjournal -f "error" 248 | echo -e "\n\033[32mFiltering in fuzzy mode with output coloring:\033[0m\n" 249 | curl -s https://raw.githubusercontent.com/Lifailon/lazyjournal/refs/heads/main/color.log | lazyjournal -f "error" -c 250 | echo -e "\n\033[32mFiltering in regex mode:\033[0m\n" 251 | curl -s https://raw.githubusercontent.com/Lifailon/lazyjournal/refs/heads/main/color.log | lazyjournal -r "error|fatal|fail|crash" 252 | echo -e "\n\033[32mFiltering in regex mode with output coloring:\033[0m\n" 253 | curl -s https://raw.githubusercontent.com/Lifailon/lazyjournal/refs/heads/main/color.log | lazyjournal -r "error|fatal|fail|crash" -c 254 | 255 | - name: Check interface in TMUX 256 | if: ${{ github.event.inputs.Release == 'true' }} 257 | run: | 258 | tmux new-session -d -s test-session "lazyjournal" 259 | sleep 1 260 | tmux capture-pane -p 261 | tmux send-keys -t test-session "$(echo -e 'cron')" 262 | tmux send-keys -t test-session "$(echo -e '\t')" # Tab 263 | tmux send-keys -t test-session "$(echo -e '\r')" # Enter 264 | sleep 3 265 | tmux capture-pane -p 266 | tmux send-keys -t test-session "$(echo -e '\t\t\t\t\t')" 267 | for i in {1..4}; do tmux send-keys -t test-session "$(echo -e '\x7f')"; done 268 | tmux send-keys -t test-session "$(echo -e 'lazy')" 269 | tmux send-keys -t test-session "$(echo -e '\t\t\t')" 270 | tmux send-keys -t test-session "$(echo -e '\x1b[C')" # Right 271 | sleep 1 272 | tmux send-keys -t test-session "$(echo -e '\x1b[D')" # Left 273 | sleep 1 274 | tmux send-keys -t test-session "$(echo -e '\r')" 275 | sleep 3 276 | tmux capture-pane -p 277 | tmux send-keys -t test-session "$(echo -e '\t')" 278 | tmux send-keys -t test-session "$(echo -e '\x1b[A')" # Up 279 | tmux send-keys -t test-session "$(echo -e '\x1b[B')" # Down 280 | tmux send-keys -t test-session "$(echo -e '\x1b[B')" 281 | tmux send-keys -t test-session "$(echo -e '\x1b[B')" 282 | tmux send-keys -t test-session "$(echo -e 'tty|port')" 283 | sleep 3 284 | tmux capture-pane -p 285 | tmux kill-session -t test-session 286 | 287 | build: 288 | runs-on: ubuntu-latest 289 | 290 | name: Build on ubuntu-latest 291 | 292 | steps: 293 | - name: Clone repository 294 | uses: actions/checkout@v4 295 | 296 | - name: Clone main repository 297 | uses: actions/checkout@v4 298 | 299 | - name: Install Go 300 | uses: actions/setup-go@v5 301 | with: 302 | go-version: 1.25 303 | 304 | - name: Install dependencies 305 | run: | 306 | go fmt ./... 307 | go vet ./... 308 | go get ./... 309 | go mod tidy 310 | go mod verify 311 | go build -v ./... 312 | 313 | - name: Get version in env for build 314 | run: | 315 | version=$(go run main.go -v) 316 | echo "VERSION=$version" >> $GITHUB_ENV 317 | 318 | - name: Build binaries 319 | if: ${{ github.event.inputs.Binary == 'true' }} 320 | run: | 321 | version=$VERSION 322 | mkdir -p bin 323 | architectures=("amd64" "arm64") 324 | oss=("linux" "darwin" "openbsd" "freebsd" "windows") 325 | for arch in "${architectures[@]}"; do 326 | for os in "${oss[@]}"; do 327 | binName="bin/lazyjournal-$version-$os-$arch" 328 | if [[ "$os" == "windows" ]]; then 329 | binName="$binName.exe" 330 | fi 331 | CGO_ENABLED=0 GOOS=$os GOARCH=$arch go build -o "$binName" 332 | done 333 | done 334 | ls -lh bin 335 | echo "ARTIFACT_NAME=lazyjournal-$version" >> $GITHUB_ENV 336 | 337 | - name: Build deb package 338 | if: ${{ github.event.inputs.Binary == 'true' }} 339 | run: | 340 | version=$VERSION 341 | rm -f lazyjournal 342 | mkdir -p lazyjournal/DEBIAN lazyjournal/usr/local/bin 343 | architectures=("amd64" "arm64") 344 | for arch in "${architectures[@]}"; do 345 | rm -f lazyjournal/usr/local/bin/lazyjournal 346 | cp bin/lazyjournal-$version-linux-$arch lazyjournal/usr/local/bin/lazyjournal 347 | rm -f lazyjournal/DEBIAN/control 348 | echo "Package: lazyjournal" > lazyjournal/DEBIAN/control 349 | echo "Version: $version" >> lazyjournal/DEBIAN/control 350 | echo "Architecture: $arch" >> lazyjournal/DEBIAN/control 351 | echo "Maintainer: https://github.com/Lifailon" >> lazyjournal/DEBIAN/control 352 | echo "Description: A TUI for reading logs from journald, auditd, file system, Docker and Podman containers, as well Kubernetes pods." >> lazyjournal/DEBIAN/control 353 | dpkg-deb --build lazyjournal lazyjournal-$version-$arch.deb 354 | dpkg-deb --contents lazyjournal-$version-$arch.deb 355 | mv lazyjournal-$version-$arch.deb bin/lazyjournal-$version-$arch.deb 356 | done 357 | ls -lh bin/*.deb 358 | sudo dpkg -i bin/lazyjournal-$version-amd64.deb 359 | sudo dpkg -r lazyjournal 360 | 361 | - name: Upload binaries 362 | if: ${{ github.event.inputs.Binary == 'true' }} 363 | uses: actions/upload-artifact@v4 364 | with: 365 | name: ${{ env.ARTIFACT_NAME }} 366 | path: bin/ 367 | env: 368 | ARTIFACT_NAME: ${{ env.ARTIFACT_NAME }} 369 | 370 | - name: Login to Docker Hub 371 | if: ${{ github.event.inputs.Docker == 'true' }} 372 | uses: docker/login-action@v3 373 | with: 374 | username: ${{ secrets.DOCKER_USERNAME }} 375 | password: ${{ secrets.DOCKER_PASSWORD }} 376 | 377 | - name: Install Docker Buildx 378 | if: ${{ github.event.inputs.Docker == 'true' }} 379 | uses: docker/setup-buildx-action@v3 380 | with: 381 | driver: docker-container 382 | install: true 383 | 384 | - name: Build and push Docker images for Linux on amd64 and arm64 385 | if: ${{ github.event.inputs.Docker == 'true' }} 386 | run: | 387 | # docker build -t lifailon/lazyjournal:latest . 388 | # docker push lifailon/lazyjournal:latest 389 | docker buildx build \ 390 | --platform linux/amd64,linux/arm64 \ 391 | -t lifailon/lazyjournal:latest \ 392 | --push . 393 | version=$VERSION 394 | docker buildx build \ 395 | --platform linux/amd64,linux/arm64 \ 396 | -t lifailon/lazyjournal:$version \ 397 | --push . -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

6 | 7 | 8 | Go Report 9 | 10 | Mentioned in Awesome Go 11 | Go Reference 12 |
13 | 14 | 15 | 16 | 17 | 18 |

19 | 20 | Terminal user interface for reading logs from `journald`, `auditd`, file system, Docker (including Swarm) containers, Compose stacks, Podman and Kubernetes pods with support for output coloring and multiple filtering modes. Written in Go with the [awesome-gocui](https://github.com/awesome-gocui/gocui) (fork [gocui](https://github.com/jroimartin/gocui)) library. 21 | 22 | This tool is inspired by and with love for [LazyDocker](https://github.com/jesseduffield/lazydocker) and [LazyGit](https://github.com/jesseduffield/lazygit). It is also included in [Awesome-Go](https://github.com/avelino/awesome-go?tab=readme-ov-file#logging), [Awesome-TUIs](https://github.com/rothgar/awesome-tuis?tab=readme-ov-file#development) and [Awesome-Docker](https://github.com/veggiemonk/awesome-docker?tab=readme-ov-file#terminal-ui), check out other useful projects on the repository pages. 23 | 24 | > [!NOTE] 25 | > You can try it out on the [Killercoda](https://killercoda.com/lazyjournal/scenario/playground) playground. 26 | 27 | ![Regex filtering](/img/regex.png) 28 | 29 |
30 | Screenshots 31 |
  • Filtering the file log using fuzzy search:
  • 32 | Filtering the file log using fuzzy search 33 |
  • Filtering by timestamp for ssh log from systemd:
  • 34 | Filtering by timestamp for ssh log from systemd 35 |
  • Disabling built-in timestamp in docker logs and filtering by stream:
  • 36 | Disabling built-in timestamp in docker logs and filtering by stream 37 |
  • Demo file of built-in output coloring for the log:
  • 38 | Demo file of built-in output coloring for the log 39 |
    40 | 41 | ## Features 42 | 43 | - Simple installation, to run download one executable file without dependencies and settings. 44 | - Centralized search for the required journal by filtering all lists (log sources). 45 | - Streaming output of new events from the selected journal (like `tail`). 46 | - List of all services (including disabled unit files) with current state from `systemd` to access their logs. 47 | - View all system and user journals via `journalctl` (tool for reading logs from [journald](https://github.com/systemd/systemd/tree/main/src/journal)). 48 | - List of all system boots for kernel log output. 49 | - List of audit rules from `auditd` for filtering by keys and viewing in `interpret` format. 50 | - File system logs such as for `Apache` or `Nginx`, as well as `syslog`, `messages`, etc. from `/var/log`. 51 | - Lists all log files in users home directories, as well as descriptor log files used by processes. 52 | - Reading archive logs truncated during rotation (`gz`, `xz` and `bz2` formats) and Packet Capture (`pcap` format). 53 | - Apple System Logs support (`asl` format). 54 | - Docker and Swarm logs from the file system or stream, including build-in timestamps and filtering by stream. 55 | - Logs of all containers in Docker Compose stacks, sorted by time for all entries. 56 | - Podman logs, without the need to run a background process (socket). 57 | - Kubernetes pods logs (you must first configure a connection to the cluster via `kubectl`). 58 | - Logs of [k3s](https://github.com/k3s-io/k3s) pods and containers from the file system on any nodes (including workers). 59 | - Windows Event Logs via `PowerShell` and `wevtutil`, as well as application logs from Windows file system. 60 | - Search and analyze all logs from remote hosts in one interface using [rsyslog](https://www.rsyslog.com) configuration. 61 | - Access to logs on a remote system via ssh protocol. 62 | 63 | ### Filtering 64 | 65 | Supports 4 filtering modes: 66 | 67 | - **Default** - case sensitive exact search. 68 | - **Fuzzy** (like `fzf`) - custom inexact case-insensitive search (searches for all phrases separated by a space anywhere on a line). 69 | - **Regex** (like `grep`) - search with regular expression support, based on the built-in [regexp](https://pkg.go.dev/regexp) library, case-insensitive by default (in case a regular expression syntax error occurs, the input field will be highlighted in red). 70 | - **Timestamp** - filter `since` and/or `until` by date and time for `journald` and docker or podman logs (only in stream mode). This mode affects the loading of the log (thereby increasing performance) and can be used in conjunction with other filtering modes, so the current log should be reloaded by pressing `Enter` in the current input field. 71 | 72 | Supported formats for filtering by timestamp: 73 | 74 | - `00:00` 75 | - `00:00:00` 76 | - `2025-04-14` 77 | - `2025-04-14 00:00` 78 | - `2025-04-14 00:00:00` 79 | 80 | Examples of short format: 81 | 82 | - Since `-48h` until `-24h` for container logs from journald (logs for the previous day). 83 | - Since `+1h` until `+30m` for system journals from docker or podman. 84 | 85 | ### Coloring 86 | 87 | Several log output coloring modes are supported: 88 | 89 | - **default** - built-in output coloring, requires no dependencies and is several times faster than other tools (including command-line mode). 90 | - **tailspin** - uses [tailspin](https://github.com/bensadeh/tailspin) (requires the tool to be installed on the system). 91 | - **bat** - uses [bat](https://github.com/sharkdp/bat) in ansi mode and log language (requires the tool to be installed on the system). 92 | 93 | It is also possible to disable coloring, this is useful if your terminal already has output coloring built in, such as [WindTerm](https://github.com/kingToolbox/WindTerm). 94 | 95 | The built-in coloring by default supports several color groups: 96 | 97 | - **Custom** - URLs, HTTP methods (GET, POST, etc), double quotes and braces for json, file paths and processes in UNIX. 98 | - **Yellow** - warnings and known names (host name and system users). 99 | - **Green** - keywords indicating success. 100 | - **Red** - keywords indicating error. 101 | - **Blue** - statuses and actions (restart, update, etc). 102 | - **Light blue** - numbers (date, time, timestamp, bytes, versions, percentage, integers, IP and MAC addresses). 103 | 104 | A full list of all keywords can be found in the [color.log](/color.log) file (used for testing only). If you have suggestions for improving coloring (e.g. adding new words), you can open an [issue](https://github.com/Lifailon/lazyjournal/issues) for a new feature. 105 | 106 | Coloring directly affects the loading time of the log, to increase the performance of reading large logs, it is possible to disable coloring using the `Ctrl+Q`. 107 | 108 | ## Install 109 | 110 | Binaries are available for download on the [releases](https://github.com/Lifailon/lazyjournal/releases) page. 111 | 112 | List of supported systems and architectures in which functionality is checked: 113 | 114 | | OS | amd64 | arm64 | Systems | 115 | | - | - | - | - | 116 | | Linux | ✔ | ✔ | Raspberry Pi (`aarch64`), Oracle Linux (RHEL-based), Arch Linux, Rocky Linux, Ubuntu Server 20.04 and above. | 117 | | Darwin | ✔ | ✔ | macOS Sequoia 15.2 `x64` on MacBook and the `arm64` in GitHub Actions. | 118 | | BSD | ✔ | | OpenBSD 7.6 and FreeBSD 14.2. | 119 | | Windows | ✔ | | Windows 10 and 11. | 120 | 121 | ### Unix-based 122 | 123 | Run the command in the console to quickly install or update the stable version for Linux, macOS or the BSD-based system: 124 | 125 | ```shell 126 | curl -sS https://raw.githubusercontent.com/Lifailon/lazyjournal/main/install.sh | bash 127 | ``` 128 | 129 | > [!NOTE] 130 | > This command will run a script that will download the latest executable binary (auto-detect OS and arch) from the GitHub repository to your home directory along with other executables (default path is `~/.local/bin/`) and configurations (`~/.config/lazyjournal/`) for the current user, and also grant execute permission. 131 | 132 | ### Debian-based 133 | 134 | If you are using Ubuntu or any other Debian-based system, you can also download the `deb` package to manage installation and removal: 135 | 136 | ```bash 137 | VERSION=$(curl -sSL -H 'Accept: application/json' https://github.com/Lifailon/lazyjournal/releases/latest | sed -e 's/.*"tag_name":"\([^"]*\)".*/\1/') 138 | curl -L -sS https://github.com/Lifailon/lazyjournal/releases/download/$VERSION/lazyjournal-$VERSION-$(dpkg --print-architecture).deb -o /tmp/lazyjournal.deb 139 | sudo apt install /tmp/lazyjournal.deb && rm /tmp/lazyjournal.deb 140 | ``` 141 | 142 | ### Homebrew (macOS / Linux) 143 | 144 | Use the following command to install `lazyjournal` using [Homebrew](https://formulae.brew.sh/formula/lazyjournal): 145 | 146 | ```shell 147 | brew install lazyjournal 148 | ``` 149 | 150 | ### Conda / mamba / pixi (Linux / macOS / Windows) 151 | 152 | If you use package managers like conda or mamba, you can install `lazyjournal` from [conda-forge](https://anaconda.org/conda-forge/lazyjournal): 153 | 154 | ```shell 155 | conda install -c conda-forge lazyjournal 156 | mamba install -c conda-forge lazyjournal 157 | ``` 158 | 159 | You can install `lazyjournal` user-globally using [pixi](https://prefix.dev): 160 | 161 | ```shell 162 | pixi global install lazyjournal 163 | ``` 164 | 165 | ### Arch Linux 166 | 167 | If you an Arch Linux user you can also install from the [AUR](https://aur.archlinux.org/packages/lazyjournal): 168 | 169 | ```shell 170 | paru -S lazyjournal 171 | ``` 172 | 173 | ### Docker (Debian-based) 174 | 175 | Download the [docker-compose](/docker-compose.yml) file and run the container using the image from [Docker Hub](https://hub.docker.com/r/lifailon/lazyjournal): 176 | 177 | ```shell 178 | mkdir lazyjournal && cd lazyjournal 179 | curl https://raw.githubusercontent.com/Lifailon/lazyjournal/refs/heads/main/docker-compose.yml -o docker-compose.yml 180 | docker compose up -d 181 | docker exec -it lazyjournal lazyjournal 182 | ``` 183 | 184 | The image is based on Debian with `systemd`, docker cli, `docker-compose` and `kubectl` pre-installed. The necessary permissions are already pre-set in the file to support all log sources from the host system. 185 | 186 | To access Kubernetes logs, you need to forward the configuration to the container. If you're using a local cluster (e.g., k3s), change the cluster server address in the configuration to the host address on the local network. 187 | 188 | ### Web mode 189 | 190 | Supports running in a container with a Web interface, using [ttyd](https://github.com/tsl0922/ttyd) to access logs via a browser. To do this, edit the variables: 191 | 192 | ```env 193 | # Enable Web mode 194 | TTYD=true 195 | PORT=5555 196 | # Credentials for accessing via Web browser (optional) 197 | USERNAME=admin 198 | PASSWORD=admin 199 | # Flags used (optional) 200 | OPTIONS=-t 5000 -u 2 201 | ``` 202 | 203 | ### Windows 204 | 205 | Use the following command to quickly install in your PowerShell console: 206 | 207 | ```PowerShell 208 | irm https://raw.githubusercontent.com/Lifailon/lazyjournal/main/install.ps1 | iex 209 | ``` 210 | 211 | The following directories are used to search for logs in the file system: 212 | 213 | - `Program Files` 214 | - `Program Files (x86)` 215 | - `ProgramData` 216 | - `AppData\Local` and `AppData\Roamin` for current user 217 | 218 | To read logs, automatic detection of the following encodings is supported: 219 | 220 | - `UTF-8` 221 | - `UTF-16 with BOM` 222 | - `UTF-16 without BOM` 223 | - `Windows-1251` by default 224 | 225 | ### Go package 226 | 227 | You can also use Go to install ([Go](https://go.dev/doc/install) must be installed on the system): 228 | 229 | ```shell 230 | go install github.com/Lifailon/lazyjournal@latest 231 | ``` 232 | 233 | ## Usage 234 | 235 | You can run the interface from anywhere: `lazyjournal` 236 | 237 | Access to all system logs and containers may require elevated privileges for the current user. For example, if a user does not have read permission to the directory `/var/lib/docker/containers`, he will not be able to access all archived logs from the moment the container is started, but only from the moment the containerization system is started, so the process of reading logs is different. However, reading in streaming mode is faster than parsing json logs from the file system. 238 | 239 | Information in the subtitle of the `Logs` window (overridden by flags and hotkeys): 240 | 241 | - `tail` - maximum number of log lines to output (affects log loading performance). 242 | - `auto-update (interval)` - current mode of operation for automatic display of new events (disabled by manually scrolling the log output or using the `Ctrl+U` keyboard shortcut) and update interval (file logs are updated only when there are changes). 243 | - `docker` - displays the current mode for loading the container log (stream mode from the docker api or in json format from the file system) and stream display mode (all, stdout or stderr only). 244 | - `color` - displays the status (enabled or disabled) of the output coloring for the log. 245 | 246 | Hotkeys and settings values ​​can be overridden using the [config](/config.yml) file (see issue [#23](https://github.com/Lifailon/lazyjournal/issues/23) and [#27](https://github.com/Lifailon/lazyjournal/issues/27)), which can be in `~/.config/lazydocker/config.yml`, as well as next to the executable or in the current startup directory (has high priority). 247 | 248 | ### Flags 249 | 250 | `lazyjournal -h` 251 | 252 | ``` 253 | --help, -h Show help 254 | --version, -v Show version 255 | --config, -g Show configuration of hotkeys and settings (check values) 256 | --audit, -a Show audit information 257 | --tail, -t Change the number of log lines to output (range: 200-200000, default: 50000) 258 | --update, -u Change the auto refresh interval of the log output (range: 2-10, default: 5) 259 | --filter-symbols, -F Minimum number of symbols for filtering output (range: 1-10, default: 3) 260 | --disable-autoupdate, -d Disable streaming of new events (log is loaded once without automatic update) 261 | --disable-mouse, -m Disable mouse control support 262 | --disable-timestamp, -i Disable timestamp for Docker logs 263 | --only-stream, -o Force reading of Docker container logs in stream mode (by default from the file system) 264 | --docker-context, -D Use the specified Docker context (default: default) 265 | --kubernetes-context, -K Use the specified Kubernetes context (default: default) 266 | --namespace, -n Use the specified Kubernetes namespace (default: all) 267 | --path, -p Custom path to logs in the file system (e.g. "$(pwd)", default: /opt) 268 | --color, -C Color mode (available values: default, tailspin, bat or disable) 269 | --command-color, -c ANSI coloring in command line mode 270 | --command-fuzzy, -f Filtering using fuzzy search in command line mode 271 | --command-regex, -r Filtering using regular expression (regexp) in command line mode 272 | --ssh, -s Connect to remote host (use standard ssh options, separated by spaces in quotes) 273 | Example: lazyjournal --ssh "lifailon@192.168.3.101 -p 22" 274 | ``` 275 | 276 | ### Hotkeys 277 | 278 | List of all used keys and hotkeys (default values): 279 | 280 | - `F1` - show help on hotkeys. 281 | - `Up`/`PgUp`/`k` and `Down`/`PgDown`/`j` - move up and down through all journal lists and log output, as well as changing the filtering mode in the filter window. 282 | - `Shift`/`Alt`+`Up`/`Down` - quickly move up and down through all journal lists and log output every `10` or `100` lines (`500` for log output). 283 | - `Shift`/`Ctrl`+`k`/`j` - quickly move up and down (like Vim and alternative for macOS from config). 284 | - `Left`/`[`/`h` and `Right`/`]`/`l` - switch between journal lists in the selected window. 285 | - `Tab` - switch to next window. 286 | - `Shift+Tab` - return to previous window. 287 | - `Enter` - load a log from the list window or return to the previous window from the filter window. 288 | - `/` - go to the filter window from the current list window or logs window. 289 | - `End`/`Ctrl+E` - go to the end of the log. 290 | - `Home`/`Ctrl+A` - go to the top of the log. 291 | - `Ctrl`+`X`/`Z` - change the number of log lines to output (range: `200-200000`, default: `50000`). 292 | - `Ctrl`+`P`/`O` - change the auto refresh interval of the log output (range: `2-10`, default: `5`). 293 | - `Ctrl`+`U` - disable streaming of new events (log is loaded once without automatic update). 294 | - `Ctrl`+`R` - update the current log output manually (relevant in disable streaming mode). 295 | - `Ctrl`+`Q` - update all log lists. 296 | - `Ctrl`+`W` - switch color mode between `default`, `tailspin`, `bat` or `disable`. 297 | - `Ctrl`+`D` - change read mode for docker logs (stream only or json from file system). 298 | - `Ctrl`+`S` - change stream display mode for docker logs (all, stdout or stderr only). 299 | - `Ctrl`+`T` - enable or disable built-in timestamp and stream type for docker logs. 300 | - `Ctrl`+`C` - clear input text in the filter window or exit. 301 | 302 | Mouse control is supported (but can also be disabled with the `-m` flag or configuration) for selecting window and the log from list, as well as lists and log scrolling. To copy text, use the `Alt+Shift` key combination while selecting. 303 | 304 | ### Remote mode 305 | 306 | Supports access to logs on a remote system (no client installation required). 307 | 308 | Standard `ssh` arguments are used to configure the connection (passed as a single argument in quotes), for example: 309 | 310 | ```bash 311 | lazyjournal --ssh "lifailon@192.168.3.101" 312 | # Passing arguments 313 | lazyjournal --ssh "lifailon@192.168.3.101 -p 21 -o Compression=yes" 314 | # If sudo is supported without entering a password 315 | lazyjournal --ssh "lifailon@192.168.3.101 sudo" 316 | ``` 317 | 318 | > [!IMPORTANT] 319 | > Remote access is only possible using an ssh key (password access is **not supported**, as each function request will require entering a password). 320 | 321 | ### Command-line mode 322 | 323 | Coloring and/or filtering of output is supported in command line mode: 324 | 325 | ```shell 326 | alias lj=lazyjournal # >> $HOME/.bashrc 327 | 328 | # Coloring the output from stdin 329 | cat /var/log/syslog | lj -c 330 | 331 | # Filtering in fuzzy search 332 | cat /var/log/syslog | lj -f "error" 333 | 334 | # Filtering using regular expressions 335 | cat /var/log/syslog | lj -r "error|fatal|fail|crash" 336 | 337 | # Filtering with subsequent coloring of the output 338 | cat /var/log/syslog | lj -r "error|fatal|fail|crash" -c 339 | ``` 340 | 341 | ## Build 342 | 343 | Clone the repository and use Make to run or build the binary: 344 | 345 | ```shell 346 | git clone https://github.com/Lifailon/lazyjournal 347 | cd lazyjournal 348 | make run 349 | # or 350 | make build 351 | ``` 352 | 353 | ## Testing 354 | 355 | Unit tests cover all main functions and interface operation. 356 | 357 | ```shell 358 | # Get a list of all tests 359 | make list 360 | # Run selected or all tests 361 | make test n=TestMockInterface 362 | make test-all 363 | ``` 364 | 365 | > [!NOTE] 366 | > A detailed report on test coverage using CI Actions for Linux, macOS and Windows systems is available on the [Wiki](https://github.com/Lifailon/lazyjournal/wiki) page. 367 | 368 | Check the source code on the base linters using [golangci-lint](https://github.com/golangci/golangci-lint) (including all [critic](https://github.com/go-critic/go-critic) and severity high in [security](https://github.com/securego/gosec)): 369 | 370 | ```shell 371 | make lint-install 372 | make lint 373 | ``` 374 | 375 | ## Contributing 376 | 377 | Since this is my first Go project, there may be some bad practices, BUT I want to make `lazyjournal` better. Any contribution will be appreciated! If you want to implement any new feature or fix something, please [open an issue](https://github.com/Lifailon/lazyjournal/issues) first. 378 | 379 | Thanks to all participants for their contributions: 380 | 381 | - [Matteo Giordano](https://github.com/malteo) for upload and update the package in `AUR`. 382 | - [Ueno M.](https://github.com/eunos-1128) for upload and update the package in `Homebrew` and `Conda`. 383 | 384 | You can also upload the package yourself to any package manager you use and make [Pull Requests](https://github.com/Lifailon/lazyjournal/pulls). 385 | 386 | ## Alternatives 387 | 388 | - [Lnav](https://github.com/tstack/lnav) - The Logfile Navigator is a log file viewer for the terminal. 389 | - [Toolong](https://github.com/Textualize/toolong) - A terminal application to view, tail, merge, and search log files. 390 | - [Nerdlog](https://github.com/dimonomid/nerdlog) - A remote, multi-host TUI syslog viewer with timeline histogram and no central server. 391 | - [Gonzo](https://github.com/control-theory/gonzo) - A log analysis terminal UI with beautiful charts and AI-powered insights. 392 | - [Dozzle](https://github.com/amir20/dozzle) - A small lightweight application with a Web based interface to monitor Docker logs. 393 | 394 | ## License 395 | 396 | This project is licensed under the **MIT License**. See the [LICENSE](LICENSE) file for details. 397 | 398 | Copyright (C) 2024 Lifailon (Alex Kup) -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "log" 8 | "os" 9 | "os/exec" 10 | "os/user" 11 | "regexp" 12 | "runtime" 13 | "strings" 14 | "testing" 15 | "time" 16 | 17 | "github.com/awesome-gocui/gocui" 18 | ) 19 | 20 | func TestCreatReport(t *testing.T) { 21 | file, _ := os.Create("test-report.md") 22 | defer file.Close() 23 | } 24 | 25 | func TestWinFiles(t *testing.T) { 26 | // Пропускаем тест целиком для Linux/macOS/bsd 27 | if runtime.GOOS != "windows" { 28 | t.Skip("Skip Windows test") 29 | } 30 | 31 | // Создаем файл отчета 32 | file, _ := os.OpenFile("test-report.md", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 33 | defer file.Close() 34 | file.WriteString("## Windows File Logs\n") 35 | file.WriteString("| Lines | Read | Color | Path |\n") 36 | file.WriteString("|-------|------|-------|------|\n") 37 | 38 | // Тестируемые параметры для функции 39 | testCases := []struct { 40 | name string 41 | selectPath string 42 | }{ 43 | {"Program Files", "ProgramFiles"}, 44 | {"Program Files 86", "ProgramFiles86"}, 45 | {"ProgramData", "ProgramData"}, 46 | {"AppData/Local", "AppDataLocal"}, 47 | {"AppData/Roaming", "AppDataRoaming"}, 48 | } 49 | 50 | for _, tc := range testCases { 51 | t.Run(tc.name, func(t *testing.T) { 52 | // Заполняем базовые параметры структуры 53 | app := &App{ 54 | selectPath: tc.selectPath, 55 | testMode: true, 56 | logViewCount: "50000", 57 | logUpdateSeconds: 5, 58 | getOS: "windows", 59 | // Режим и текст для фильтрации 60 | selectFilterMode: "fuzzy", 61 | filterText: "", 62 | // Инициализируем переменные с регулярными выражениями 63 | trimHttpRegex: trimHttpRegex, 64 | trimHttpsRegex: trimHttpsRegex, 65 | trimPrefixPathRegex: trimPrefixPathRegex, 66 | trimPostfixPathRegex: trimPostfixPathRegex, 67 | hexByteRegex: hexByteRegex, 68 | dateTimeRegex: dateTimeRegex, 69 | integersInputRegex: integersInputRegex, 70 | syslogUnitRegex: syslogUnitRegex, 71 | } 72 | 73 | currentUser, _ := user.Current() 74 | app.userName = currentUser.Username 75 | if strings.Contains(app.userName, "\\") { 76 | app.userName = strings.Split(app.userName, "\\")[1] 77 | } 78 | app.systemDisk = os.Getenv("SystemDrive") 79 | if len(app.systemDisk) >= 1 { 80 | app.systemDisk = string(app.systemDisk[0]) 81 | } else { 82 | app.systemDisk = "C" 83 | } 84 | 85 | // Пропускаем тесты для CI 86 | if app.userName == "runneradmin" && tc.selectPath != "AppDataRoaming" { 87 | t.Skip("Skip test for", app.userName, "in CI") 88 | } 89 | 90 | // (1) Заполняем массив из названий файлов и путей к ним 91 | app.loadWinFiles(app.selectPath) 92 | // Если список файлов пустой, тест будет провален 93 | if len(app.logfiles) == 0 { 94 | t.Errorf("File list is null") 95 | } else { 96 | t.Log("Log files count:", len(app.logfiles)) 97 | } 98 | 99 | var ansiEscape = regexp.MustCompile(`\x1b\[[0-9;]*m`) 100 | // Проходимся по всем путям в массиве 101 | for _, logfile := range app.logfiles { 102 | // Удаляем покраску из названия файла в массиве (в интерфейсе строка читается без покраски при выборе) 103 | logFileName := ansiEscape.ReplaceAllString(logfile.name, "") 104 | // Фиксируем время запуска функции 105 | startTime := time.Now() 106 | // (2) Читаем журнал 107 | app.loadFileLogs(strings.TrimSpace(logFileName), true) 108 | endTime := time.Since(startTime) 109 | // (3) Фильтруем и красим 110 | startTime2 := time.Now() 111 | app.applyFilter(true) 112 | endTime2 := time.Since(startTime2) 113 | // Записываем в отчет путь, количество строк в массиве прочитанных из файла, время чтения и фильтрации + покраски 114 | fmt.Fprintf(file, "| %d | %s | %s | %s |\n", len(app.currentLogLines), endTime, endTime2, app.lastLogPath) 115 | } 116 | }) 117 | } 118 | } 119 | 120 | func TestWinEvents(t *testing.T) { 121 | if runtime.GOOS != "windows" { 122 | t.Skip("Skip Windows test") 123 | } 124 | 125 | file, _ := os.OpenFile("test-report.md", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 126 | defer file.Close() 127 | file.WriteString("## Windows Event Logs\n") 128 | file.WriteString("| Lines | Read | Color | Event Name |\n") 129 | file.WriteString("|-------|------|-------|------------|\n") 130 | 131 | app := &App{ 132 | testMode: true, 133 | logViewCount: "50000", 134 | logUpdateSeconds: 5, 135 | getOS: "windows", 136 | systemDisk: "C", 137 | userName: "lifailon", 138 | selectFilterMode: "fuzzy", 139 | filterText: "", 140 | trimHttpRegex: trimHttpRegex, 141 | trimHttpsRegex: trimHttpsRegex, 142 | trimPrefixPathRegex: trimPrefixPathRegex, 143 | trimPostfixPathRegex: trimPostfixPathRegex, 144 | hexByteRegex: hexByteRegex, 145 | dateTimeRegex: dateTimeRegex, 146 | integersInputRegex: integersInputRegex, 147 | syslogUnitRegex: syslogUnitRegex, 148 | } 149 | 150 | app.loadWinEvents() 151 | if len(app.journals) == 0 { 152 | t.Errorf("File list is null") 153 | } else { 154 | t.Log("Windows Event Logs count:", len(app.journals)) 155 | } 156 | 157 | var ansiEscape = regexp.MustCompile(`\x1b\[[0-9;]*m`) 158 | for _, journal := range app.journals { 159 | app.updateFile = true 160 | serviceName := ansiEscape.ReplaceAllString(journal.name, "") 161 | startTime := time.Now() 162 | app.loadJournalLogs(strings.TrimSpace(serviceName), true) 163 | endTime := time.Since(startTime) 164 | 165 | startTime2 := time.Now() 166 | app.applyFilter(true) 167 | endTime2 := time.Since(startTime2) 168 | 169 | fmt.Fprintf(file, "| %d | %s | %s | %s |\n", len(app.currentLogLines), endTime, endTime2, serviceName) 170 | } 171 | } 172 | 173 | func TestUnixFiles(t *testing.T) { 174 | if runtime.GOOS == "windows" { 175 | t.Skip("Skip Linux test") 176 | } 177 | 178 | file, _ := os.OpenFile("test-report.md", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 179 | defer file.Close() 180 | file.WriteString("## Unix File Logs\n") 181 | file.WriteString("| Lines | Read | Color | Path |\n") 182 | file.WriteString("|-------|------|-------|------|\n") 183 | 184 | testCases := []struct { 185 | name string 186 | selectPath string 187 | }{ 188 | {"System var logs", "/var/log/"}, 189 | {"Optional package logs", "/opt/"}, 190 | {"Users home logs", "/home/"}, 191 | {"Process descriptor logs", "descriptor"}, 192 | } 193 | 194 | for _, tc := range testCases { 195 | t.Run(tc.name, func(t *testing.T) { 196 | app := &App{ 197 | selectPath: tc.selectPath, 198 | testMode: true, 199 | logViewCount: "50000", 200 | logUpdateSeconds: 5, 201 | getOS: "linux", 202 | userName: "lifailon", 203 | selectFilterMode: "fuzzy", 204 | filterText: "", 205 | trimHttpRegex: trimHttpRegex, 206 | trimHttpsRegex: trimHttpsRegex, 207 | trimPrefixPathRegex: trimPrefixPathRegex, 208 | trimPostfixPathRegex: trimPostfixPathRegex, 209 | hexByteRegex: hexByteRegex, 210 | dateTimeRegex: dateTimeRegex, 211 | integersInputRegex: integersInputRegex, 212 | syslogUnitRegex: syslogUnitRegex, 213 | customPath: "/opt", 214 | } 215 | 216 | // Пропускаем тесты в macOS/BSD 217 | if runtime.GOOS != "linux" && tc.selectPath != "/var/log/" { 218 | t.Skip("Skip test for macOS in CI") 219 | } 220 | 221 | app.loadFiles(app.selectPath) 222 | if len(app.logfiles) == 0 { 223 | t.Errorf("File list is null") 224 | } else { 225 | t.Log("Log files count:", len(app.logfiles)) 226 | } 227 | 228 | var ansiEscape = regexp.MustCompile(`\x1b\[[0-9;]*m`) 229 | for _, logfile := range app.logfiles { 230 | logFileName := ansiEscape.ReplaceAllString(logfile.name, "") 231 | startTime := time.Now() 232 | app.loadFileLogs(strings.TrimSpace(logFileName), true) 233 | endTime := time.Since(startTime) 234 | 235 | startTime2 := time.Now() 236 | app.applyFilter(true) 237 | endTime2 := time.Since(startTime2) 238 | 239 | fmt.Fprintf(file, "| %d | %s | %s | %s |\n", len(app.currentLogLines), endTime, endTime2, app.lastLogPath) 240 | } 241 | }) 242 | } 243 | } 244 | 245 | func TestLinuxJournal(t *testing.T) { 246 | if runtime.GOOS != "linux" { 247 | t.Skip("Skip Linux test") 248 | } 249 | 250 | file, _ := os.OpenFile("test-report.md", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 251 | defer file.Close() 252 | file.WriteString("## Linux journals\n") 253 | file.WriteString("| Lines | Read | Color | Journal Name |\n") 254 | file.WriteString("|-------|------|-------|--------------|\n") 255 | 256 | testCases := []struct { 257 | name string 258 | journalName string 259 | }{ 260 | {"Unit service list", "services"}, 261 | {"System journals", "UNIT"}, 262 | {"User journals", "USER_UNIT"}, 263 | {"Kernel boot", "kernel"}, 264 | } 265 | 266 | for _, tc := range testCases { 267 | t.Run(tc.name, func(t *testing.T) { 268 | app := &App{ 269 | selectUnits: tc.journalName, 270 | testMode: true, 271 | logViewCount: "50000", 272 | logUpdateSeconds: 5, 273 | getOS: "linux", 274 | selectFilterMode: "fuzzy", 275 | filterText: "", 276 | trimHttpRegex: trimHttpRegex, 277 | trimHttpsRegex: trimHttpsRegex, 278 | trimPrefixPathRegex: trimPrefixPathRegex, 279 | trimPostfixPathRegex: trimPostfixPathRegex, 280 | hexByteRegex: hexByteRegex, 281 | dateTimeRegex: dateTimeRegex, 282 | integersInputRegex: integersInputRegex, 283 | syslogUnitRegex: syslogUnitRegex, 284 | } 285 | 286 | app.loadServices(app.selectUnits) 287 | if len(app.journals) == 0 { 288 | t.Errorf("File list is null") 289 | } else { 290 | t.Log("Journal count:", len(app.journals)) 291 | } 292 | 293 | var ansiEscape = regexp.MustCompile(`\x1b\[[0-9;]*m`) 294 | for _, journal := range app.journals { 295 | serviceName := ansiEscape.ReplaceAllString(journal.name, "") 296 | startTime := time.Now() 297 | app.loadJournalLogs(strings.TrimSpace(serviceName), true) 298 | endTime := time.Since(startTime) 299 | 300 | startTime2 := time.Now() 301 | app.applyFilter(true) 302 | endTime2 := time.Since(startTime2) 303 | 304 | fmt.Fprintf(file, "| %d | %s | %s | %s |\n", len(app.currentLogLines), endTime, endTime2, serviceName) 305 | } 306 | }) 307 | } 308 | } 309 | 310 | func TestDockerContainer(t *testing.T) { 311 | file, _ := os.OpenFile("test-report.md", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 312 | defer file.Close() 313 | file.WriteString("## Containers\n") 314 | file.WriteString("| Lines | Read | Color | Container Name |\n") 315 | file.WriteString("|-------|------|-------|----------------|\n") 316 | 317 | testCases := []struct { 318 | name string 319 | selectContainerizationSystem string 320 | }{ 321 | {"Docker", "docker"}, 322 | // {"Compose", "compose"}, 323 | // {"Podman", "podman"}, 324 | // {"Kubernetes", "kubectl"}, 325 | } 326 | 327 | for _, tc := range testCases { 328 | t.Run(tc.name, func(t *testing.T) { 329 | // Пропускаем не установленые системы 330 | _, err := exec.LookPath(tc.selectContainerizationSystem) 331 | if err != nil { 332 | t.Skip("Skip: ", tc.selectContainerizationSystem, " not installed (environment not found)") 333 | } 334 | app := &App{ 335 | selectContainerizationSystem: tc.selectContainerizationSystem, 336 | testMode: true, 337 | logViewCount: "50000", 338 | logUpdateSeconds: 5, 339 | selectFilterMode: "fuzzy", 340 | filterText: "", 341 | trimHttpRegex: trimHttpRegex, 342 | trimHttpsRegex: trimHttpsRegex, 343 | trimPrefixPathRegex: trimPrefixPathRegex, 344 | trimPostfixPathRegex: trimPostfixPathRegex, 345 | hexByteRegex: hexByteRegex, 346 | dateTimeRegex: dateTimeRegex, 347 | integersInputRegex: integersInputRegex, 348 | syslogUnitRegex: syslogUnitRegex, 349 | uniquePrefixColorMap: make(map[string]string), 350 | } 351 | 352 | app.loadDockerContainer(app.selectContainerizationSystem) 353 | if len(app.dockerContainers) == 0 { 354 | t.Errorf("Container list is null") 355 | } else { 356 | t.Log("Container count:", len(app.dockerContainers)) 357 | } 358 | 359 | var ansiEscape = regexp.MustCompile(`\x1b\[[0-9;]*m`) 360 | for _, dockerContainer := range app.dockerContainers { 361 | containerName := ansiEscape.ReplaceAllString(dockerContainer.name, "") 362 | startTime := time.Now() 363 | app.loadDockerLogs(strings.TrimSpace(containerName), true) 364 | endTime := time.Since(startTime) 365 | 366 | startTime2 := time.Now() 367 | app.applyFilter(true) 368 | endTime2 := time.Since(startTime2) 369 | 370 | fmt.Fprintf(file, "| %d | %s | %s | %s |\n", len(app.currentLogLines), endTime, endTime2, containerName) 371 | } 372 | }) 373 | } 374 | } 375 | 376 | func TestColor(t *testing.T) { 377 | if runtime.GOOS == "windows" { 378 | t.Skip("Skip unix test") 379 | } 380 | 381 | app := &App{ 382 | testMode: true, 383 | logViewCount: "50000", 384 | logUpdateSeconds: 5, 385 | selectPath: "/home/", 386 | filterListText: "color", 387 | trimHttpRegex: trimHttpRegex, 388 | trimHttpsRegex: trimHttpsRegex, 389 | trimPrefixPathRegex: trimPrefixPathRegex, 390 | trimPostfixPathRegex: trimPostfixPathRegex, 391 | hexByteRegex: hexByteRegex, 392 | dateTimeRegex: dateTimeRegex, 393 | integersInputRegex: integersInputRegex, 394 | syslogUnitRegex: syslogUnitRegex, 395 | } 396 | 397 | // Определяем переменные для покраски 398 | app.hostName, _ = os.Hostname() 399 | if strings.Contains(app.hostName, ".") { 400 | app.hostName = strings.Split(app.hostName, ".")[0] 401 | } 402 | currentUser, _ := user.Current() 403 | app.userName = currentUser.Username 404 | if strings.Contains(app.userName, "\\") { 405 | app.userName = strings.Split(app.userName, "\\")[1] 406 | } 407 | passwd, _ := os.Open("/etc/passwd") 408 | scanner := bufio.NewScanner(passwd) 409 | for scanner.Scan() { 410 | line := scanner.Text() 411 | userName := strings.Split(line, ":") 412 | if len(userName) > 0 { 413 | app.userNameArray = append(app.userNameArray, userName[0]) 414 | } 415 | } 416 | files, _ := os.ReadDir("/") 417 | for _, file := range files { 418 | if file.IsDir() { 419 | app.rootDirArray = append(app.rootDirArray, "/"+file.Name()) 420 | } 421 | } 422 | 423 | // Загружаем файловые журналы и фильтруем вывод (находим тестовый лог-файл) 424 | app.loadFiles(app.selectPath) 425 | app.logfilesNotFilter = app.logfiles 426 | app.applyFilterList() 427 | 428 | if len(app.logfiles) == 0 { 429 | t.Errorf("File list is null") 430 | } else { 431 | t.Log("Log files count:", len(app.logfiles)) 432 | } 433 | 434 | // Загружаем журнал 435 | var ansiEscape = regexp.MustCompile(`\x1b\[[0-9;]*m`) 436 | logFileName := ansiEscape.ReplaceAllString(app.logfiles[0].name, "") 437 | app.loadFileLogs(strings.TrimSpace(logFileName), true) 438 | 439 | // Выводим содержимое с покраской 440 | app.applyFilter(true) 441 | t.Log("Lines: ", len(app.filteredLogLines)) 442 | for _, line := range app.filteredLogLines { 443 | t.Log(line) 444 | } 445 | } 446 | 447 | func TestExtColor(t *testing.T) { 448 | if runtime.GOOS == "windows" { 449 | t.Skip("Skip unix test") 450 | } 451 | 452 | app := &App{ 453 | testMode: true, 454 | colorMode: "tailspin", 455 | logViewCount: "50000", 456 | logUpdateSeconds: 5, 457 | selectPath: "/home/", 458 | filterListText: "color", 459 | } 460 | 461 | app.loadFiles(app.selectPath) 462 | app.logfilesNotFilter = app.logfiles 463 | app.applyFilterList() 464 | 465 | if len(app.logfiles) == 0 { 466 | t.Errorf("File list is null") 467 | } else { 468 | t.Log("Log files count:", len(app.logfiles)) 469 | } 470 | 471 | var ansiEscape = regexp.MustCompile(`\x1b\[[0-9;]*m`) 472 | logFileName := ansiEscape.ReplaceAllString(app.logfiles[0].name, "") 473 | app.loadFileLogs(strings.TrimSpace(logFileName), true) 474 | 475 | app.applyFilter(true) 476 | t.Log("Lines: ", len(app.filteredLogLines)) 477 | for _, line := range app.filteredLogLines { 478 | t.Log(line) 479 | } 480 | 481 | app.colorMode = "bat" 482 | app.applyFilter(true) 483 | t.Log("Lines: ", len(app.filteredLogLines)) 484 | for _, line := range app.filteredLogLines { 485 | t.Log(line) 486 | } 487 | } 488 | 489 | func TestFilter(t *testing.T) { 490 | if runtime.GOOS == "windows" { 491 | t.Skip("Skip unix test") 492 | } 493 | 494 | testCases := []struct { 495 | name string 496 | selectFilterMode string 497 | }{ 498 | {"Default filter mode", "default"}, 499 | {"Fuzzy filter mode", "fuzzy"}, 500 | {"Regex filter mode", "regex"}, 501 | } 502 | for _, tc := range testCases { 503 | t.Run(tc.name, func(t *testing.T) { 504 | app := &App{ 505 | testMode: true, 506 | logViewCount: "50000", 507 | logUpdateSeconds: 5, 508 | selectPath: "/home/", 509 | filterListText: "color", 510 | selectFilterMode: tc.selectFilterMode, 511 | filterText: "true", 512 | trimHttpRegex: trimHttpRegex, 513 | trimHttpsRegex: trimHttpsRegex, 514 | trimPrefixPathRegex: trimPrefixPathRegex, 515 | trimPostfixPathRegex: trimPostfixPathRegex, 516 | hexByteRegex: hexByteRegex, 517 | dateTimeRegex: dateTimeRegex, 518 | integersInputRegex: integersInputRegex, 519 | syslogUnitRegex: syslogUnitRegex, 520 | } 521 | 522 | if tc.selectFilterMode == "regex" { 523 | app.filterText = "^true$" 524 | } 525 | 526 | app.hostName, _ = os.Hostname() 527 | if strings.Contains(app.hostName, ".") { 528 | app.hostName = strings.Split(app.hostName, ".")[0] 529 | } 530 | currentUser, _ := user.Current() 531 | app.userName = currentUser.Username 532 | if strings.Contains(app.userName, "\\") { 533 | app.userName = strings.Split(app.userName, "\\")[1] 534 | } 535 | passwd, _ := os.Open("/etc/passwd") 536 | scanner := bufio.NewScanner(passwd) 537 | for scanner.Scan() { 538 | line := scanner.Text() 539 | userName := strings.Split(line, ":") 540 | if len(userName) > 0 { 541 | app.userNameArray = append(app.userNameArray, userName[0]) 542 | } 543 | } 544 | files, _ := os.ReadDir("/") 545 | for _, file := range files { 546 | if file.IsDir() { 547 | app.rootDirArray = append(app.rootDirArray, "/"+file.Name()) 548 | } 549 | } 550 | 551 | app.loadFiles(app.selectPath) 552 | app.logfilesNotFilter = app.logfiles 553 | app.applyFilterList() 554 | 555 | if len(app.logfiles) == 0 { 556 | t.Errorf("File list is null") 557 | } else { 558 | t.Log("Log files count:", len(app.logfiles)) 559 | } 560 | 561 | var ansiEscape = regexp.MustCompile(`\x1b\[[0-9;]*m`) 562 | logFileName := ansiEscape.ReplaceAllString(app.logfiles[0].name, "") 563 | app.loadFileLogs(strings.TrimSpace(logFileName), true) 564 | 565 | app.applyFilter(true) 566 | t.Log("Lines: ", len(app.filteredLogLines)) 567 | for _, line := range app.filteredLogLines { 568 | t.Log(line) 569 | } 570 | }) 571 | } 572 | } 573 | 574 | func TestFlags(t *testing.T) { 575 | app := &App{} 576 | app.uniquePrefixColorMap = make(map[string]string) 577 | showHelp() 578 | showConfig() 579 | app.showAudit() 580 | } 581 | 582 | func TestCommandColor(t *testing.T) { 583 | app := &App{ 584 | testMode: false, 585 | startServices: 0, 586 | selectedJournal: 0, 587 | startFiles: 0, 588 | selectedFile: 0, 589 | startDockerContainers: 0, 590 | selectedDockerContainer: 0, 591 | selectUnits: "services", 592 | selectPath: "/var/log/", 593 | selectContainerizationSystem: "docker", 594 | selectFilterMode: "default", 595 | logViewCount: "50000", 596 | logUpdateSeconds: 5, 597 | journalListFrameColor: gocui.ColorDefault, 598 | fileSystemFrameColor: gocui.ColorDefault, 599 | dockerFrameColor: gocui.ColorDefault, 600 | autoScroll: true, 601 | trimHttpRegex: trimHttpRegex, 602 | trimHttpsRegex: trimHttpsRegex, 603 | trimPrefixPathRegex: trimPrefixPathRegex, 604 | trimPostfixPathRegex: trimPostfixPathRegex, 605 | hexByteRegex: hexByteRegex, 606 | dateTimeRegex: dateTimeRegex, 607 | integersInputRegex: integersInputRegex, 608 | syslogUnitRegex: syslogUnitRegex, 609 | keybindingsEnabled: true, 610 | uniquePrefixColorMap: make(map[string]string), 611 | } 612 | 613 | // Читаем содержимое тестируемого файла 614 | data, err := os.ReadFile("color.log") 615 | if err != nil { 616 | t.Fatalf("Error read test file: %v", err) 617 | } 618 | // Подменяем stdin содержимым из файла 619 | bytes.NewReader(data) 620 | // Захватываем stdin 621 | originalStdin := os.Stdin 622 | // Создаем pipe, чтобы перенаправить stdin 623 | pr, pw, _ := os.Pipe() 624 | os.Stdin = pr 625 | // Записываем данные в "pipe" (это имитирует передачу данных в stdin) 626 | go func() { 627 | _, _ = pw.Write(data) 628 | pw.Close() 629 | }() 630 | 631 | // Текущее имя хоста 632 | app.hostName, _ = os.Hostname() 633 | // Удаляем доменную часть, если она есть 634 | if strings.Contains(app.hostName, ".") { 635 | app.hostName = strings.Split(app.hostName, ".")[0] 636 | } 637 | // Текущее имя пользователя 638 | currentUser, _ := user.Current() 639 | app.userName = currentUser.Username 640 | // Удаляем доменную часть, если она есть 641 | if strings.Contains(app.userName, "\\") { 642 | app.userName = strings.Split(app.userName, "\\")[1] 643 | } 644 | // Определяем букву системного диска с установленной ОС Windows 645 | app.systemDisk = os.Getenv("SystemDrive") 646 | if len(app.systemDisk) >= 1 { 647 | app.systemDisk = string(app.systemDisk[0]) 648 | } else { 649 | app.systemDisk = "C" 650 | } 651 | // Имена пользователей 652 | passwd, _ := os.Open("/etc/passwd") 653 | scanner := bufio.NewScanner(passwd) 654 | for scanner.Scan() { 655 | line := scanner.Text() 656 | userName := strings.Split(line, ":") 657 | if len(userName) > 0 { 658 | app.userNameArray = append(app.userNameArray, userName[0]) 659 | } 660 | } 661 | // Список корневых каталогов (ls -d /*/) с приставкой "/" 662 | files, _ := os.ReadDir("/") 663 | for _, file := range files { 664 | if file.IsDir() { 665 | app.rootDirArray = append(app.rootDirArray, "/"+file.Name()) 666 | } 667 | } 668 | 669 | app.commandLineColor(false) 670 | // Восстанавливаем оригинальный stdin 671 | os.Stdin = originalStdin 672 | } 673 | 674 | func TestCommandFuzzyFilter(t *testing.T) { 675 | app := &App{ 676 | testMode: false, 677 | startServices: 0, 678 | selectedJournal: 0, 679 | startFiles: 0, 680 | selectedFile: 0, 681 | startDockerContainers: 0, 682 | selectedDockerContainer: 0, 683 | selectUnits: "services", 684 | selectPath: "/var/log/", 685 | selectContainerizationSystem: "docker", 686 | selectFilterMode: "default", 687 | logViewCount: "50000", 688 | logUpdateSeconds: 5, 689 | journalListFrameColor: gocui.ColorDefault, 690 | fileSystemFrameColor: gocui.ColorDefault, 691 | dockerFrameColor: gocui.ColorDefault, 692 | autoScroll: true, 693 | trimHttpRegex: trimHttpRegex, 694 | trimHttpsRegex: trimHttpsRegex, 695 | trimPrefixPathRegex: trimPrefixPathRegex, 696 | trimPostfixPathRegex: trimPostfixPathRegex, 697 | hexByteRegex: hexByteRegex, 698 | dateTimeRegex: dateTimeRegex, 699 | integersInputRegex: integersInputRegex, 700 | syslogUnitRegex: syslogUnitRegex, 701 | keybindingsEnabled: true, 702 | uniquePrefixColorMap: make(map[string]string), 703 | } 704 | 705 | data, err := os.ReadFile("color.log") 706 | if err != nil { 707 | t.Fatalf("Error read test file: %v", err) 708 | } 709 | bytes.NewReader(data) 710 | pr, pw, _ := os.Pipe() 711 | os.Stdin = pr 712 | go func() { 713 | _, _ = pw.Write(data) 714 | pw.Close() 715 | }() 716 | 717 | var filter = "success" 718 | app.commandLineFuzzy(filter, false) 719 | app.commandLineFuzzy(filter, true) 720 | } 721 | 722 | func TestCommandRegexFilter(t *testing.T) { 723 | app := &App{ 724 | testMode: false, 725 | startServices: 0, 726 | selectedJournal: 0, 727 | startFiles: 0, 728 | selectedFile: 0, 729 | startDockerContainers: 0, 730 | selectedDockerContainer: 0, 731 | selectUnits: "services", 732 | selectPath: "/var/log/", 733 | selectContainerizationSystem: "docker", 734 | selectFilterMode: "default", 735 | logViewCount: "50000", 736 | logUpdateSeconds: 5, 737 | journalListFrameColor: gocui.ColorDefault, 738 | fileSystemFrameColor: gocui.ColorDefault, 739 | dockerFrameColor: gocui.ColorDefault, 740 | autoScroll: true, 741 | trimHttpRegex: trimHttpRegex, 742 | trimHttpsRegex: trimHttpsRegex, 743 | trimPrefixPathRegex: trimPrefixPathRegex, 744 | trimPostfixPathRegex: trimPostfixPathRegex, 745 | hexByteRegex: hexByteRegex, 746 | dateTimeRegex: dateTimeRegex, 747 | integersInputRegex: integersInputRegex, 748 | syslogUnitRegex: syslogUnitRegex, 749 | keybindingsEnabled: true, 750 | uniquePrefixColorMap: make(map[string]string), 751 | } 752 | 753 | data, err := os.ReadFile("color.log") 754 | if err != nil { 755 | t.Fatalf("Error read test file: %v", err) 756 | } 757 | bytes.NewReader(data) 758 | pr, pw, _ := os.Pipe() 759 | os.Stdin = pr 760 | go func() { 761 | _, _ = pw.Write(data) 762 | pw.Close() 763 | }() 764 | 765 | var filter = "http|127" 766 | filter = "(?i)" + filter 767 | regex, err := regexp.Compile(filter) 768 | if err != nil { 769 | fmt.Println("Regular expression syntax error") 770 | } 771 | app.commandLineRegex(regex, false) 772 | app.commandLineRegex(regex, true) 773 | } 774 | 775 | func TestMainInterface(t *testing.T) { 776 | go runGoCui(true) 777 | time.Sleep(3 * time.Second) 778 | } 779 | 780 | func TestMockInterface(t *testing.T) { 781 | app := &App{ 782 | testMode: false, 783 | startServices: 0, 784 | selectedJournal: 0, 785 | startFiles: 0, 786 | selectedFile: 0, 787 | startDockerContainers: 0, 788 | selectedDockerContainer: 0, 789 | selectUnits: "services", 790 | selectPath: "/var/log/", 791 | selectContainerizationSystem: "docker", 792 | selectFilterMode: "default", 793 | logViewCount: "50000", 794 | logUpdateSeconds: 5, 795 | journalListFrameColor: gocui.ColorDefault, 796 | fileSystemFrameColor: gocui.ColorDefault, 797 | dockerFrameColor: gocui.ColorDefault, 798 | autoScroll: true, 799 | trimHttpRegex: trimHttpRegex, 800 | trimHttpsRegex: trimHttpsRegex, 801 | trimPrefixPathRegex: trimPrefixPathRegex, 802 | trimPostfixPathRegex: trimPostfixPathRegex, 803 | hexByteRegex: hexByteRegex, 804 | dateTimeRegex: dateTimeRegex, 805 | integersInputRegex: integersInputRegex, 806 | syslogUnitRegex: syslogUnitRegex, 807 | keybindingsEnabled: true, 808 | uniquePrefixColorMap: make(map[string]string), 809 | } 810 | 811 | app.getOS = runtime.GOOS 812 | app.getArch = runtime.GOARCH 813 | 814 | var err error 815 | 816 | // Отключение tcell для CI 817 | g, err = gocui.NewGui(gocui.OutputSimulator, true) 818 | var debug = true 819 | 820 | // Debug mode (включить отображение интерфейса и отключить логирование) 821 | // g, err = gocui.NewGui(gocui.OutputNormal, true) 822 | // debug = false 823 | 824 | if err != nil { 825 | log.Panicln(err) 826 | } 827 | 828 | app.gui = g 829 | g.SetManagerFunc(app.layout) 830 | g.Mouse = false 831 | 832 | g.FgColor = gocui.ColorDefault 833 | g.BgColor = gocui.ColorDefault 834 | 835 | if err := app.setupKeybindings(); err != nil { 836 | log.Panicln("Error key bindings", err) 837 | } 838 | 839 | if err := app.layout(g); err != nil { 840 | log.Panicln(err) 841 | } 842 | 843 | app.hostName, _ = os.Hostname() 844 | if strings.Contains(app.hostName, ".") { 845 | app.hostName = strings.Split(app.hostName, ".")[0] 846 | } 847 | currentUser, _ := user.Current() 848 | app.userName = currentUser.Username 849 | if strings.Contains(app.userName, "\\") { 850 | app.userName = strings.Split(app.userName, "\\")[1] 851 | } 852 | app.systemDisk = os.Getenv("SystemDrive") 853 | if len(app.systemDisk) >= 1 { 854 | app.systemDisk = string(app.systemDisk[0]) 855 | } else { 856 | app.systemDisk = "C" 857 | } 858 | passwd, _ := os.Open("/etc/passwd") 859 | scanner := bufio.NewScanner(passwd) 860 | for scanner.Scan() { 861 | line := scanner.Text() 862 | userName := strings.Split(line, ":") 863 | if len(userName) > 0 { 864 | app.userNameArray = append(app.userNameArray, userName[0]) 865 | } 866 | } 867 | files, _ := os.ReadDir("/") 868 | for _, file := range files { 869 | if file.IsDir() { 870 | app.rootDirArray = append(app.rootDirArray, file.Name()) 871 | } 872 | } 873 | 874 | if v, err := g.View("services"); err == nil { 875 | _, viewHeight := v.Size() 876 | app.maxVisibleServices = viewHeight 877 | } 878 | if app.getOS == "windows" { 879 | v, err := g.View("services") 880 | if err != nil { 881 | log.Panicln(err) 882 | } 883 | v.Title = " < Windows Event Logs (0) > " 884 | go func() { 885 | app.loadWinEvents() 886 | }() 887 | } else { 888 | app.loadServices(app.selectUnits) 889 | } 890 | 891 | if v, err := g.View("varLogs"); err == nil { 892 | _, viewHeight := v.Size() 893 | app.maxVisibleFiles = viewHeight 894 | } 895 | 896 | if app.getOS == "windows" { 897 | selectedVarLog, err := g.View("varLogs") 898 | if err != nil { 899 | log.Panicln(err) 900 | } 901 | g.Update(func(g *gocui.Gui) error { 902 | selectedVarLog.Clear() 903 | fmt.Fprintln(selectedVarLog, "Searching log files...") 904 | selectedVarLog.Highlight = false 905 | return nil 906 | }) 907 | selectedVarLog.Title = " < Program Files (0) > " 908 | app.selectPath = "ProgramFiles" 909 | go func() { 910 | app.loadWinFiles(app.selectPath) 911 | }() 912 | } else { 913 | app.loadFiles(app.selectPath) 914 | } 915 | 916 | if v, err := g.View("docker"); err == nil { 917 | _, viewHeight := v.Size() 918 | app.maxVisibleDockerContainers = viewHeight 919 | } 920 | app.loadDockerContainer(app.selectContainerizationSystem) 921 | 922 | if _, err := g.SetCurrentView("filterList"); err != nil { 923 | return 924 | } 925 | 926 | app.secondsChan = make(chan int, app.logUpdateSeconds) 927 | go func() { 928 | app.updateLogBackground(app.secondsChan, false) 929 | }() 930 | 931 | go func() { 932 | app.updateWindowSize(1) 933 | }() 934 | 935 | // Отображение GUI в режиме OutputNormal 936 | go g.MainLoop() 937 | 938 | time.Sleep(3 * time.Second) 939 | 940 | // F1 941 | app.showInterfaceHelp(g) 942 | app.closeHelp(g) 943 | 944 | // Проверяем покраску 945 | app.currentLogLines = []string{ 946 | "http://127.0.0.1:8443", 947 | "https://github.com/Lifailon/lazyjournal", 948 | "/dev/null", 949 | "root", 950 | "warning", 951 | "stderr", 952 | "success", 953 | "restart", 954 | "0x04", 955 | "2025-02-26T21:38:35.956968+03:00", 956 | "127.0.0.1, 127.0.0.1:8443", 957 | } 958 | app.updateDelimiter(true) 959 | app.applyFilter(true) 960 | time.Sleep(3 * time.Second) 961 | if debug { 962 | t.Log("\033[32mPASS\033[0m: test coloring") 963 | } 964 | 965 | // Обновить вывод лога 966 | app.updateLogOutput(false) 967 | if debug { 968 | t.Log("\033[32mPASS\033[0m: update log (Ctrl+R)") 969 | } 970 | 971 | // Проверяем фильтрацию текста для списков 972 | app.filterListText = "a" 973 | app.createFilterEditor("lists") 974 | time.Sleep(1 * time.Second) 975 | // app.filterListText = "" 976 | app.applyFilterList() 977 | time.Sleep(1 * time.Second) 978 | if debug { 979 | t.Log("\033[32mPASS\033[0m: test filter lists") 980 | } 981 | 982 | // Очистка фильтров 983 | app.clearFilterListEditor(g) 984 | app.clearFilterEditor(g) 985 | if debug { 986 | t.Log("\033[32mPASS\033[0m: clear filters before exit (Ctrl+C)") 987 | } 988 | 989 | // Проверяем фильтрацию по timestamp 990 | app.timestampFilterEditor("sinceFilter") 991 | app.timestampFilterEditor("untilFilter") 992 | time.Sleep(1 * time.Second) 993 | if debug { 994 | t.Log("\033[32mPASS\033[0m: test fiter timestamp") 995 | } 996 | 997 | // TAB journals 998 | if debug { 999 | t.Log("\033[33mDEBUG\033[0m: Tab to journals") 1000 | } 1001 | app.nextView(g, nil) 1002 | time.Sleep(1 * time.Second) 1003 | 1004 | // Journals (services) 1005 | if v, err := g.View("services"); err == nil { 1006 | // Перемещаемся по списку вниз 1007 | app.nextService(v, 100) 1008 | time.Sleep(1 * time.Second) 1009 | // Загружаем журнал 1010 | app.selectService(g, v) 1011 | time.Sleep(3 * time.Second) 1012 | // Перемещаемся по списку вверх 1013 | app.prevService(v, 100) 1014 | time.Sleep(1 * time.Second) 1015 | // Переключаем списки (только для Linux) 1016 | if runtime.GOOS != "windows" { 1017 | // Вправо 1018 | if debug { 1019 | t.Log("\033[33mDEBUG\033[0m: List next (right)") 1020 | } 1021 | app.setUnitListRight(g, v) 1022 | time.Sleep(3 * time.Second) 1023 | if debug { 1024 | t.Log("\033[32mPASS\033[0m: System journals (UNIT)") 1025 | } 1026 | app.setUnitListRight(g, v) 1027 | time.Sleep(3 * time.Second) 1028 | if debug { 1029 | t.Log("\033[32mPASS\033[0m: User journals (USER_UNIT)") 1030 | } 1031 | app.setUnitListRight(g, v) 1032 | time.Sleep(3 * time.Second) 1033 | if debug { 1034 | t.Log("\033[32mPASS\033[0m: Kernel boot") 1035 | } 1036 | app.setUnitListRight(g, v) 1037 | time.Sleep(3 * time.Second) 1038 | if debug { 1039 | t.Log("\033[32mPASS\033[0m: Audit rules keys (auditd)") 1040 | } 1041 | app.setUnitListRight(g, v) 1042 | time.Sleep(3 * time.Second) 1043 | if debug { 1044 | t.Log("\033[32mPASS\033[0m: Unit service list (services)") 1045 | } 1046 | // Влево 1047 | if debug { 1048 | t.Log("\033[33mDEBUG\033[0m: List back (left)") 1049 | } 1050 | app.setUnitListLeft(g, v) 1051 | time.Sleep(3 * time.Second) 1052 | if debug { 1053 | t.Log("\033[32mPASS\033[0m: Audit rules keys") 1054 | } 1055 | app.setUnitListLeft(g, v) 1056 | time.Sleep(3 * time.Second) 1057 | if debug { 1058 | t.Log("\033[32mPASS\033[0m: Kernel boot") 1059 | } 1060 | app.setUnitListLeft(g, v) 1061 | time.Sleep(3 * time.Second) 1062 | if debug { 1063 | t.Log("\033[32mPASS\033[0m: User journals (USER_UNIT)") 1064 | } 1065 | app.setUnitListLeft(g, v) 1066 | time.Sleep(3 * time.Second) 1067 | if debug { 1068 | t.Log("\033[32mPASS\033[0m: System journals (UNIT)") 1069 | } 1070 | app.setUnitListLeft(g, v) 1071 | time.Sleep(3 * time.Second) 1072 | if debug { 1073 | t.Log("\033[32mPASS\033[0m: Unit service list (services)") 1074 | } 1075 | } 1076 | } 1077 | if debug { 1078 | t.Log("\033[32mPASS\033[0m: test journals") 1079 | } 1080 | 1081 | // TAB filesystem 1082 | if debug { 1083 | t.Log("\033[33mDEBUG\033[0m: Tab to filesystem") 1084 | } 1085 | app.nextView(g, nil) 1086 | time.Sleep(1 * time.Second) 1087 | 1088 | // File System (varLogs) 1089 | if v, err := g.View("varLogs"); err == nil { 1090 | // Перемещаемся по списку вниз 1091 | app.nextFileName(v, 100) 1092 | time.Sleep(1 * time.Second) 1093 | // Загружаем журнал 1094 | app.selectFile(g, v) 1095 | time.Sleep(3 * time.Second) 1096 | // Перемещаемся по списку вверх 1097 | app.prevFileName(v, 100) 1098 | time.Sleep(1 * time.Second) 1099 | if runtime.GOOS != "windows" { 1100 | // Вправо 1101 | if debug { 1102 | t.Log("\033[33mDEBUG\033[0m: List next (right)") 1103 | } 1104 | app.setLogFilesListRight(g, v) 1105 | time.Sleep(10 * time.Second) 1106 | if debug { 1107 | t.Log("\033[32mPASS\033[0m: Optional package logs (/opt/log)") 1108 | } 1109 | app.setLogFilesListRight(g, v) 1110 | time.Sleep(10 * time.Second) 1111 | if debug { 1112 | t.Log("\033[32mPASS\033[0m: Users home logs (/home)") 1113 | } 1114 | app.setLogFilesListRight(g, v) 1115 | time.Sleep(10 * time.Second) 1116 | if debug { 1117 | t.Log("\033[32mPASS\033[0m: Process descriptor logs") 1118 | } 1119 | app.setLogFilesListRight(g, v) 1120 | time.Sleep(10 * time.Second) 1121 | if debug { 1122 | t.Log("\033[32mPASS\033[0m: System var logs (/var/log)") 1123 | } 1124 | // Влево 1125 | if debug { 1126 | t.Log("\033[33mDEBUG\033[0m: List back (left)") 1127 | } 1128 | app.setLogFilesListLeft(g, v) 1129 | time.Sleep(10 * time.Second) 1130 | if debug { 1131 | t.Log("\033[32mPASS\033[0m: Process descriptor logs") 1132 | } 1133 | app.setLogFilesListLeft(g, v) 1134 | time.Sleep(10 * time.Second) 1135 | if debug { 1136 | t.Log("\033[32mPASS\033[0m: Users home logs (/home)") 1137 | } 1138 | app.setLogFilesListLeft(g, v) 1139 | time.Sleep(10 * time.Second) 1140 | if debug { 1141 | t.Log("\033[32mPASS\033[0m: Optional package logs (/opt/log)") 1142 | } 1143 | app.setLogFilesListLeft(g, v) 1144 | time.Sleep(10 * time.Second) 1145 | if debug { 1146 | t.Log("\033[32mPASS\033[0m: System var logs (/var/log)") 1147 | } 1148 | } 1149 | } 1150 | if debug { 1151 | t.Log("\033[32mPASS\033[0m: test filesystem") 1152 | } 1153 | 1154 | // TAB containerization system 1155 | if debug { 1156 | t.Log("\033[33mDEBUG\033[0m: Tab to containerization system") 1157 | } 1158 | app.nextView(g, nil) 1159 | time.Sleep(1 * time.Second) 1160 | 1161 | // Containerization System (docker) 1162 | if v, err := g.View("docker"); err == nil { 1163 | // Перемещаемся по списку вниз 1164 | app.nextDockerContainer(v, 100) 1165 | time.Sleep(1 * time.Second) 1166 | // Загружаем журнал (ВРЕМЕННО ОТКЛЮЧЕНО) 1167 | app.selectDocker(g, v) 1168 | time.Sleep(3 * time.Second) 1169 | // Перемещаемся по списку вверх 1170 | app.prevDockerContainer(v, 100) 1171 | time.Sleep(1 * time.Second) 1172 | if runtime.GOOS != "windows" { 1173 | // Вправо 1174 | if debug { 1175 | t.Log("\033[33mDEBUG\033[0m: List next (right)") 1176 | } 1177 | app.setContainersListRight(g, v) 1178 | time.Sleep(2 * time.Second) 1179 | if debug { 1180 | t.Log("\033[32mPASS\033[0m: Compose") 1181 | } 1182 | app.setContainersListRight(g, v) 1183 | time.Sleep(2 * time.Second) 1184 | if debug { 1185 | t.Log("\033[32mPASS\033[0m: Podman") 1186 | } 1187 | app.setContainersListRight(g, v) 1188 | time.Sleep(2 * time.Second) 1189 | if debug { 1190 | t.Log("\033[32mPASS\033[0m: Kubernetes") 1191 | } 1192 | app.setContainersListRight(g, v) 1193 | time.Sleep(2 * time.Second) 1194 | if debug { 1195 | t.Log("\033[32mPASS\033[0m: Docker") 1196 | } 1197 | // Влево 1198 | if debug { 1199 | t.Log("\033[33mDEBUG\033[0m: List back (left)") 1200 | } 1201 | app.setContainersListLeft(g, v) 1202 | time.Sleep(2 * time.Second) 1203 | if debug { 1204 | t.Log("\033[32mPASS\033[0m: Kubernetes") 1205 | } 1206 | app.setContainersListLeft(g, v) 1207 | time.Sleep(2 * time.Second) 1208 | if debug { 1209 | t.Log("\033[32mPASS\033[0m: Podman") 1210 | } 1211 | app.setContainersListLeft(g, v) 1212 | time.Sleep(2 * time.Second) 1213 | if debug { 1214 | t.Log("\033[32mPASS\033[0m: Compose") 1215 | } 1216 | app.setContainersListLeft(g, v) 1217 | time.Sleep(2 * time.Second) 1218 | if debug { 1219 | t.Log("\033[32mPASS\033[0m: Docker") 1220 | } 1221 | } 1222 | } 1223 | if debug { 1224 | t.Log("\033[32mPASS\033[0m: test containers") 1225 | } 1226 | 1227 | // TAB filter logs 1228 | if debug { 1229 | t.Log("\033[33mDEBUG\033[0m: Tab to filter logs") 1230 | } 1231 | app.nextView(g, nil) 1232 | 1233 | // Проверяем фильтрацию текста для вывода журнала 1234 | app.filterText = "a" 1235 | app.applyFilter(true) 1236 | time.Sleep(3 * time.Second) 1237 | // Ctrl+W 1238 | app.clearFilterEditor(g) 1239 | app.applyFilter(true) 1240 | time.Sleep(3 * time.Second) 1241 | if debug { 1242 | t.Log("\033[32mPASS\033[0m: test filter logs output") 1243 | } 1244 | 1245 | // Проверяем режимы фильтрации 1246 | if v, err := g.View("filter"); err == nil { 1247 | // Вверх 1248 | if debug { 1249 | t.Log("\033[33mDEBUG\033[0m: Filter mode next (up)") 1250 | } 1251 | app.setFilterModeRight(g, v) 1252 | time.Sleep(1 * time.Second) 1253 | if debug { 1254 | t.Log("\033[32mPASS\033[0m: Filter fuzzy") 1255 | } 1256 | app.setFilterModeRight(g, v) 1257 | time.Sleep(1 * time.Second) 1258 | if debug { 1259 | t.Log("\033[32mPASS\033[0m: Filter regex") 1260 | } 1261 | app.setFilterModeRight(g, v) 1262 | time.Sleep(1 * time.Second) 1263 | if debug { 1264 | t.Log("\033[32mPASS\033[0m: Filter timestamp") 1265 | } 1266 | app.setFilterModeRight(g, v) 1267 | time.Sleep(1 * time.Second) 1268 | if debug { 1269 | t.Log("\033[32mPASS\033[0m: Filter default") 1270 | } 1271 | // Вниз 1272 | if debug { 1273 | t.Log("\033[33mDEBUG\033[0m: Filter mode back (down)") 1274 | } 1275 | app.setFilterModeLeft(g, v) 1276 | time.Sleep(1 * time.Second) 1277 | if debug { 1278 | t.Log("\033[32mPASS\033[0m: Filter timestamp") 1279 | } 1280 | app.setFilterModeLeft(g, v) 1281 | time.Sleep(1 * time.Second) 1282 | if debug { 1283 | t.Log("\033[32mPASS\033[0m: Filter regex") 1284 | } 1285 | app.setFilterModeLeft(g, v) 1286 | time.Sleep(1 * time.Second) 1287 | if debug { 1288 | t.Log("\033[32mPASS\033[0m: Filter fuzzy") 1289 | } 1290 | app.setFilterModeLeft(g, v) 1291 | time.Sleep(1 * time.Second) 1292 | if debug { 1293 | t.Log("\033[32mPASS\033[0m: Filter default") 1294 | } 1295 | } 1296 | if debug { 1297 | t.Log("\033[32mPASS\033[0m: test filter modes") 1298 | } 1299 | 1300 | // TAB logs output 1301 | if debug { 1302 | t.Log("\033[33mDEBUG\033[0m: Tab to logs output") 1303 | } 1304 | app.nextView(g, nil) 1305 | time.Sleep(1 * time.Second) 1306 | if v, err := g.View("logs"); err == nil { 1307 | // Default: 50000 1308 | // Вверх 1309 | if debug { 1310 | t.Log("\033[33mDEBUG\033[0m: Up tail (Ctrl+X)") 1311 | } 1312 | app.setCountLogViewUp(g, v) 1313 | time.Sleep(1 * time.Second) 1314 | if debug { 1315 | t.Log("\033[32mPASS\033[0m: Tail 100000") 1316 | } 1317 | app.setCountLogViewUp(g, v) 1318 | time.Sleep(1 * time.Second) 1319 | if debug { 1320 | t.Log("\033[32mPASS\033[0m: Tail 150000") 1321 | } 1322 | app.setCountLogViewUp(g, v) 1323 | time.Sleep(1 * time.Second) 1324 | app.setCountLogViewUp(g, v) 1325 | time.Sleep(1 * time.Second) 1326 | if debug { 1327 | t.Log("\033[32mPASS\033[0m: Tail 200000") 1328 | } 1329 | // Вниз 1330 | if debug { 1331 | t.Log("\033[33mDEBUG\033[0m: Down tail (Ctrl+Z)") 1332 | } 1333 | app.setCountLogViewDown(g, v) 1334 | time.Sleep(1 * time.Second) 1335 | if debug { 1336 | t.Log("\033[32mPASS\033[0m: Tail 150000") 1337 | } 1338 | app.setCountLogViewDown(g, v) 1339 | time.Sleep(1 * time.Second) 1340 | if debug { 1341 | t.Log("\033[32mPASS\033[0m: Tail 100000") 1342 | } 1343 | app.setCountLogViewDown(g, v) 1344 | time.Sleep(1 * time.Second) 1345 | if debug { 1346 | t.Log("\033[32mPASS\033[0m: Tail 50000") 1347 | } 1348 | app.setCountLogViewDown(g, v) 1349 | time.Sleep(1 * time.Second) 1350 | if debug { 1351 | t.Log("\033[32mPASS\033[0m: Tail 30000") 1352 | } 1353 | app.setCountLogViewDown(g, v) 1354 | time.Sleep(1 * time.Second) 1355 | if debug { 1356 | t.Log("\033[32mPASS\033[0m: Tail 20000") 1357 | } 1358 | app.setCountLogViewDown(g, v) 1359 | time.Sleep(1 * time.Second) 1360 | if debug { 1361 | t.Log("\033[32mPASS\033[0m: Tail 10000") 1362 | } 1363 | app.setCountLogViewDown(g, v) 1364 | time.Sleep(1 * time.Second) 1365 | if debug { 1366 | t.Log("\033[32mPASS\033[0m: Tail 5000") 1367 | } 1368 | app.setCountLogViewDown(g, v) 1369 | time.Sleep(1 * time.Second) 1370 | if debug { 1371 | t.Log("\033[32mPASS\033[0m: Tail 1000") 1372 | } 1373 | app.setCountLogViewDown(g, v) 1374 | time.Sleep(1 * time.Second) 1375 | if debug { 1376 | t.Log("\033[32mPASS\033[0m: Tail 500") 1377 | } 1378 | app.setCountLogViewDown(g, v) 1379 | time.Sleep(1 * time.Second) 1380 | app.setCountLogViewDown(g, v) 1381 | time.Sleep(1 * time.Second) 1382 | if debug { 1383 | t.Log("\033[32mPASS\033[0m: Tail 200") 1384 | } 1385 | // Вверх 1386 | if debug { 1387 | t.Log("\033[33mDEBUG\033[0m: Up tail (Ctrl+X)") 1388 | } 1389 | app.setCountLogViewUp(g, v) 1390 | time.Sleep(1 * time.Second) 1391 | if debug { 1392 | t.Log("\033[32mPASS\033[0m: Tail 500") 1393 | } 1394 | app.setCountLogViewUp(g, v) 1395 | time.Sleep(1 * time.Second) 1396 | if debug { 1397 | t.Log("\033[32mPASS\033[0m: Tail 1000") 1398 | } 1399 | app.setCountLogViewUp(g, v) 1400 | time.Sleep(1 * time.Second) 1401 | if debug { 1402 | t.Log("\033[32mPASS\033[0m: Tail 5000") 1403 | } 1404 | app.setCountLogViewUp(g, v) 1405 | time.Sleep(1 * time.Second) 1406 | if debug { 1407 | t.Log("\033[32mPASS\033[0m: Tail 10000") 1408 | } 1409 | app.setCountLogViewUp(g, v) 1410 | time.Sleep(1 * time.Second) 1411 | if debug { 1412 | t.Log("\033[32mPASS\033[0m: Tail 20000") 1413 | } 1414 | app.setCountLogViewUp(g, v) 1415 | time.Sleep(1 * time.Second) 1416 | if debug { 1417 | t.Log("\033[32mPASS\033[0m: Tail 30000") 1418 | } 1419 | app.setCountLogViewUp(g, v) 1420 | time.Sleep(1 * time.Second) 1421 | if debug { 1422 | t.Log("\033[32mPASS\033[0m: Tail 50000") 1423 | } 1424 | // Поднять журнал на 1 1425 | if debug { 1426 | t.Log("\033[33mDEBUG\033[0m: Up logs output on 1") 1427 | } 1428 | app.scrollUpLogs(1) 1429 | time.Sleep(1 * time.Second) 1430 | // Поднять журнал на 10 1431 | if debug { 1432 | t.Log("\033[33mDEBUG\033[0m: Up logs output on 10 (Shift+Up)") 1433 | } 1434 | app.scrollUpLogs(10) 1435 | time.Sleep(1 * time.Second) 1436 | // Поднять журнал на 500 1437 | if debug { 1438 | t.Log("\033[33mDEBUG\033[0m: Up logs output on 500 (Alt+Up)") 1439 | } 1440 | app.scrollUpLogs(500) 1441 | time.Sleep(1 * time.Second) 1442 | // Поднять журнал еще на 500 1443 | app.scrollUpLogs(500) 1444 | time.Sleep(1 * time.Second) 1445 | // Опустить журнал на 1 1446 | if debug { 1447 | t.Log("\033[33mDEBUG\033[0m: Down logs output on 1") 1448 | } 1449 | app.scrollDownLogs(1) 1450 | time.Sleep(1 * time.Second) 1451 | // Опустить журнал на 10 1452 | if debug { 1453 | t.Log("\033[33mDEBUG\033[0m: Down logs output on 10 (Shift+Down)") 1454 | } 1455 | app.scrollDownLogs(10) 1456 | time.Sleep(1 * time.Second) 1457 | // Опустить журнал на 500 1458 | if debug { 1459 | t.Log("\033[33mDEBUG\033[0m: Down logs output on 10 (Alt+Down)") 1460 | } 1461 | app.scrollDownLogs(500) 1462 | time.Sleep(1 * time.Second) 1463 | // Поднять журнал в самый вверх 1464 | if debug { 1465 | t.Log("\033[33mDEBUG\033[0m: Up logs output (Ctrl+A/Home)") 1466 | } 1467 | app.pageUpLogs() 1468 | time.Sleep(1 * time.Second) 1469 | // Опустить журнал в самый низ 1470 | if debug { 1471 | t.Log("\033[33mDEBUG\033[0m: Down logs output (Ctrl+E/End)") 1472 | } 1473 | app.updateLogsView(true) 1474 | time.Sleep(1 * time.Second) 1475 | } 1476 | if debug { 1477 | t.Log("\033[32mPASS\033[0m: test log output") 1478 | } 1479 | 1480 | // TAB filter lists 1481 | app.nextView(g, nil) 1482 | time.Sleep(1 * time.Second) 1483 | 1484 | // Back Tab 1485 | app.backView(g, nil) 1486 | time.Sleep(1 * time.Second) 1487 | app.backView(g, nil) 1488 | time.Sleep(1 * time.Second) 1489 | app.backView(g, nil) 1490 | time.Sleep(1 * time.Second) 1491 | app.backView(g, nil) 1492 | time.Sleep(1 * time.Second) 1493 | app.backView(g, nil) 1494 | time.Sleep(1 * time.Second) 1495 | app.backView(g, nil) 1496 | time.Sleep(1 * time.Second) 1497 | app.backView(g, nil) 1498 | time.Sleep(1 * time.Second) 1499 | if debug { 1500 | t.Log("\033[32mPASS\033[0m: test back tab (Shift+Tab)") 1501 | } 1502 | 1503 | // Проверяем переключение окон с помощью мыши 1504 | app.setSelectView(g, "filterList") 1505 | time.Sleep(1 * time.Second) 1506 | app.setSelectView(g, "services") 1507 | time.Sleep(1 * time.Second) 1508 | app.setSelectView(g, "varLogs") 1509 | time.Sleep(1 * time.Second) 1510 | app.setSelectView(g, "docker") 1511 | time.Sleep(1 * time.Second) 1512 | app.setSelectView(g, "filter") 1513 | time.Sleep(1 * time.Second) 1514 | app.setSelectView(g, "logs") 1515 | time.Sleep(1 * time.Second) 1516 | if debug { 1517 | t.Log("\033[32mPASS\033[0m: test mouse") 1518 | } 1519 | 1520 | // Переключаем режим фильтрации на timestamp 1521 | g.SetCurrentView("filter") 1522 | if v, err := g.View("filter"); err == nil { 1523 | app.setFilterModeRight(g, v) 1524 | time.Sleep(1 * time.Second) 1525 | app.setFilterModeRight(g, v) 1526 | time.Sleep(1 * time.Second) 1527 | app.setFilterModeRight(g, v) 1528 | time.Sleep(1 * time.Second) 1529 | } 1530 | 1531 | // Проверяем переключение окон в режиме timestamp 1532 | app.nextView(g, nil) 1533 | time.Sleep(1 * time.Second) 1534 | app.nextView(g, nil) 1535 | time.Sleep(1 * time.Second) 1536 | app.setSelectView(g, "logs") 1537 | time.Sleep(1 * time.Second) 1538 | app.setSelectView(g, "logs") 1539 | time.Sleep(1 * time.Second) 1540 | app.setSelectView(g, "logs") 1541 | time.Sleep(1 * time.Second) 1542 | app.nextView(g, nil) 1543 | time.Sleep(1 * time.Second) 1544 | 1545 | quit(g, nil) 1546 | } 1547 | --------------------------------------------------------------------------------