Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Add logs command to view/tail Nginx log files #337

Merged
merged 2 commits into from
Dec 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,13 +141,17 @@ Supported commands so far:
| `galaxy` | Commands for Ansible Galaxy |
| `info` | Displays information about this Trellis project |
| `init` | Initializes an existing Trellis project |
| `key` | Commands for managing SSH keys |
| `logs` | Tails the Nginx log files |
| `new` | Creates a new Trellis project |
| `open` | Opens user-defined URLs (and more) which can act as shortcuts/bookmarks specific to your Trellis projects |
| `provision` | Provisions the specified environment |
| `rollback` | Rollsback the last deploy of the site on the specified environment |
| `ssh` | Connects to host via SSH |
| `up` | Starts and provisions the Vagrant environment by running `vagrant up` |
| `valet` | Commands for Laravel Valet |
| `vault` | Commands for Ansible Vault |
| `xdebug-tunnel` | Commands for managing Xdebug tunnels |
## Configuration
There are three ways to set configuration settings for trellis-cli and they are
Expand Down
231 changes: 231 additions & 0 deletions cmd/logs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
package cmd

import (
"flag"
"fmt"
"os/exec"
"strings"

"github.com/mitchellh/cli"
"github.com/posener/complete"
"github.com/roots/trellis-cli/command"
"github.com/roots/trellis-cli/trellis"
)

func NewLogsCommand(ui cli.Ui, trellis *trellis.Trellis) *LogsCommand {
c := &LogsCommand{UI: ui, Trellis: trellis}
c.init()
return c
}

type LogsCommand struct {
UI cli.Ui
flags *flag.FlagSet
access bool
error bool
goaccess bool
goaccessFlags string
number string
Trellis *trellis.Trellis
}

func (c *LogsCommand) init() {
c.flags = flag.NewFlagSet("", flag.ContinueOnError)
c.flags.Usage = func() { c.UI.Info(c.Help()) }
c.flags.BoolVar(&c.access, "access", false, "Show access logs only")
c.flags.BoolVar(&c.error, "error", false, "Show error logs only")
c.flags.StringVar(&c.goaccessFlags, "goaccess-flags", "", "Flags to pass to the goaccess command (in quotes)")
c.flags.BoolVar(&c.goaccess, "g", false, "Uses goaccess as the log viewer instead of tail")
c.flags.BoolVar(&c.goaccess, "goaccess", false, "Uses goaccess as the log viewer instead of tail")
c.flags.StringVar(&c.number, "n", "", "Location (number lines) corresponding to tail's '-n' option")
c.flags.StringVar(&c.number, "number", "", "Location (number lines) corresponding to tail's '-n' option")
}

func (c *LogsCommand) Run(args []string) int {
if err := c.Trellis.LoadProject(); err != nil {
c.UI.Error(err.Error())
return 1
}

c.Trellis.CheckVirtualenv(c.UI)

if err := c.flags.Parse(args); err != nil {
return 1
}

args = c.flags.Args()

commandArgumentValidator := &CommandArgumentValidator{required: 1, optional: 1}
commandArgumentErr := commandArgumentValidator.validate(args)
if commandArgumentErr != nil {
c.UI.Error(commandArgumentErr.Error())
c.UI.Output(c.Help())
return 1
}

environment := args[0]
environmentErr := c.Trellis.ValidateEnvironment(environment)
if environmentErr != nil {
c.UI.Error(environmentErr.Error())
return 1
}

siteNameArg := ""
if len(args) == 2 {
siteNameArg = args[1]
}
siteName, siteNameErr := c.Trellis.FindSiteNameFromEnvironment(environment, siteNameArg)
if siteNameErr != nil {
c.UI.Error(siteNameErr.Error())
return 1
}

sshHost := c.Trellis.SshHost(environment, siteName, "web")

_, err := exec.LookPath("goaccess")

if (c.goaccess || c.goaccessFlags != "") && err == nil {
tailCmd := c.tailCmd(siteName, "goaccess")
logArgs := []string{sshHost, tailCmd}

ssh := command.Cmd("ssh", logArgs)
goaccessArgs := []string{"--log-format=COMBINED"}

if c.goaccessFlags != "" {
goaccessArgs = append(goaccessArgs, strings.Split(c.goaccessFlags, " ")...)
}

goaccess := command.WithOptions(
command.WithTermOutput(),
).Cmd("goaccess", goaccessArgs)

goaccess.Stdin, _ = ssh.StdoutPipe()

if err := ssh.Start(); err != nil {
c.UI.Error(fmt.Sprintf("Error starting SSH command: %s", err))
return 1
}

if err := goaccess.Start(); err != nil {
c.UI.Error(fmt.Sprintf("Error starting goaccess command: %s", err))
return 1
}

if err := goaccess.Wait(); err != nil {
c.UI.Error(fmt.Sprintf("Error running goaccess command: %s", err))
return 1
}
if err := ssh.Wait(); err != nil {
c.UI.Error(fmt.Sprintf("Error running SSH command: %s", err))
return 1
}
} else {
logArgs := []string{sshHost, c.tailCmd(siteName, "tail")}

ssh := command.WithOptions(
command.WithTermOutput(),
command.WithLogging(c.UI),
).Cmd("ssh", logArgs)

if err := ssh.Run(); err != nil {
c.UI.Error(fmt.Sprintf("Error running ssh: %s", err))
return 1
}
}

return 0
}

func (c *LogsCommand) Synopsis() string {
return "Tails the Nginx log files for an environment"
}

func (c *LogsCommand) Help() string {
helpText := `
Usage: trellis logs [options] ENVIRONMENT [SITE]
Tails the Nginx log files for an environment.
Automatically integrates with https://goaccess.io/ when the --goaccess option is used.
Note: this command relies on an SSH connection to the environment's hostname to remotely
tail the log files. It depends on SSH keys being setup properly for a passwordless SSH connection.
If the 'trellis ssh' command does not work, this logs command won't work either.
View production logs:
$ trellis logs production
View access logs only:
$ trellis logs --access production
View error logs only:
$ trellis logs --error production
View logs in goaccess:
$ trellis logs --goaccess production
Pass flags to goaccess:
$ trellis logs --goaccess-flags="-a -m" production
View the last 50 log lines (-n corresponds to tail's -n option):
$ trellis logs -n 50 production
Arguments:
ENVIRONMENT Name of environment (ie: production)
SITE Name of site (ie: example.com)
Options:
--access Show access logs only
--error Show error logs only
-g, --goaccess Uses goaccess as the log viewer instead of tail
--goaccess-flags Flags to pass to the goaccess command (in quotes)
-n, --number Location (number lines) corresponding to tail's '-n' argument
-h, --help Show this help
`

return strings.TrimSpace(helpText)
}

func (c *LogsCommand) AutocompleteArgs() complete.Predictor {
return c.Trellis.AutocompleteSite(flag.NewFlagSet("", flag.ContinueOnError))
}

func (c *LogsCommand) AutocompleteFlags() complete.Flags {
return complete.Flags{
"--access": complete.PredictNothing,
"--error": complete.PredictNothing,
"--goaccess": complete.PredictNothing,
"--goaccess-flags": complete.PredictNothing,
"--number": complete.PredictNothing,
}
}

func (c *LogsCommand) tailCmd(siteName string, mode string) string {
file := "*[^gz]?" // exclude gzipped log files by default

if c.access {
file = "access.log"
} else if c.error {
file = "error.log"
} else if mode == "goaccess" {
// goaccess supports gzipped log files so we can include them
file = "*"
}

n := ""
if c.number == "" && mode == "goaccess" {
// default to load entire file in Goaccess
// this makes more sense in this context than for viewing in terminal
n = "-n +0 "
} else if c.number != "" {
n = fmt.Sprintf("-n %s ", c.number)
}

return fmt.Sprintf("tail %s-f /srv/www/%s/logs/%s", n, siteName, file)
}
Loading