Skip to content

Commit

Permalink
Merge pull request #337 from roots/logs-command
Browse files Browse the repository at this point in the history
Add logs command to view/tail Nginx log files
  • Loading branch information
swalkinshaw authored Dec 28, 2022
2 parents d887b76 + e831482 commit b4244a4
Show file tree
Hide file tree
Showing 8 changed files with 516 additions and 31 deletions.
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

0 comments on commit b4244a4

Please # to comment.