Add support for getting a server's log file via the API
This commit is contained in:
parent
b6bc9adf29
commit
7d67be8382
2
go.mod
2
go.mod
|
@ -21,7 +21,7 @@ require (
|
|||
github.com/onsi/gomega v1.5.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.1 // indirect
|
||||
github.com/pkg/errors v0.8.0 // indirect
|
||||
github.com/pkg/errors v0.8.0
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/remeh/sizedwaitgroup v0.0.0-20180822144253-5e7302b12cce
|
||||
github.com/sirupsen/logrus v1.0.5 // indirect
|
||||
|
|
22
http.go
22
http.go
|
@ -9,6 +9,7 @@ import (
|
|||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
@ -169,6 +170,25 @@ func (rt *Router) routeServerPower(w http.ResponseWriter, r *http.Request, ps ht
|
|||
w.WriteHeader(http.StatusAccepted)
|
||||
}
|
||||
|
||||
// Return the last 1Kb of the server log file.
|
||||
func (rt *Router) routeServerLogs(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
s := rt.Servers.Get(ps.ByName("server"))
|
||||
|
||||
l, _ := strconv.ParseInt(r.URL.Query().Get("size"), 10, 64)
|
||||
if l <= 0 {
|
||||
l = 2048
|
||||
}
|
||||
|
||||
out, err := s.ReadLogfile(l)
|
||||
if err != nil {
|
||||
zap.S().Errorw("failed to read server log file", zap.Error(err))
|
||||
http.Error(w, "failed to read log", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(struct{Data []string `json:"data"`}{Data: out })
|
||||
}
|
||||
|
||||
// Configures the router and all of the associated routes.
|
||||
func (rt *Router) ConfigureRouter() *httprouter.Router {
|
||||
router := httprouter.New()
|
||||
|
@ -176,6 +196,8 @@ func (rt *Router) ConfigureRouter() *httprouter.Router {
|
|||
router.GET("/", rt.routeIndex)
|
||||
router.GET("/api/servers", rt.AuthenticateToken("i:servers", rt.routeAllServers))
|
||||
router.GET("/api/servers/:server", rt.AuthenticateToken("s:view", rt.AuthenticateServer(rt.routeServer)))
|
||||
router.GET("/api/servers/:server/logs", rt.AuthenticateToken("s:logs", rt.AuthenticateServer(rt.routeServerLogs)))
|
||||
|
||||
router.POST("/api/servers/:server/power", rt.AuthenticateToken("s:power", rt.AuthenticateServer(rt.routeServerPower)))
|
||||
|
||||
return router
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Defines the basic interface that all environments need to implement so that
|
||||
// a server can be properly controlled.
|
||||
type Environment interface {
|
||||
// Returns the name of the environment.
|
||||
Type() string
|
||||
|
||||
// Starts a server instance. If the server instance is not in a state where it
|
||||
// can be started an error should be returned.
|
||||
Start() error
|
||||
|
@ -28,4 +32,12 @@ type Environment interface {
|
|||
// in the Docker environment create will create a new container instance for the
|
||||
// server.
|
||||
Create() error
|
||||
|
||||
// Attaches to the server console environment and allows piping the output to a
|
||||
// websocket or other internal tool to monitor output.
|
||||
Attach() (io.ReadCloser, error)
|
||||
|
||||
// Reads the log file for the process from the end backwards until the provided
|
||||
// number of bytes is met.
|
||||
Readlog(int64) ([]string, error)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
|
@ -8,7 +11,9 @@ import (
|
|||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/daemon/logger/jsonfilelog"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/net/context"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -77,6 +82,11 @@ func NewDockerEnvironment(cfg DockerConfiguration) (*DockerEnvironment, error) {
|
|||
// from the base environment interface.
|
||||
var _ Environment = (*DockerEnvironment)(nil)
|
||||
|
||||
// Returns the name of the environment.
|
||||
func (d *DockerEnvironment) Type() string {
|
||||
return "docker"
|
||||
}
|
||||
|
||||
// Determines if the container exists in this environment.
|
||||
func (d *DockerEnvironment) Exists() bool {
|
||||
_, err := d.Client.ContainerInspect(context.Background(), d.Server.Uuid)
|
||||
|
@ -127,6 +137,31 @@ func (d *DockerEnvironment) Terminate(signal os.Signal) error {
|
|||
return d.Client.ContainerKill(ctx, d.Server.Uuid, signal.String())
|
||||
}
|
||||
|
||||
// Contrary to the name, this doesn't actually attach to the Docker container itself,
|
||||
// but rather attaches to the log for the container and then pipes that output to
|
||||
// a websocket.
|
||||
//
|
||||
// This avoids us missing cruicial output that happens in the split seconds before the
|
||||
// code moves from 'Starting' to 'Attaching' on the process.
|
||||
//
|
||||
// @todo add throttle code
|
||||
func (d *DockerEnvironment) Attach() (io.ReadCloser, error) {
|
||||
if !d.Exists() {
|
||||
return nil, errors.New(fmt.Sprintf("no such container: %s", d.Server.Uuid))
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
opts := types.ContainerLogsOptions{
|
||||
ShowStderr: true,
|
||||
ShowStdout: true,
|
||||
Follow: true,
|
||||
}
|
||||
|
||||
r, err := d.Client.ContainerLogs(ctx, d.Server.Uuid, opts)
|
||||
|
||||
return r, err
|
||||
}
|
||||
|
||||
// Creates a new container for the server using all of the data that is currently
|
||||
// available for it. If the container already exists it will be returned.
|
||||
func (d *DockerEnvironment) Create() error {
|
||||
|
@ -239,6 +274,83 @@ func (d *DockerEnvironment) Create() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Reads the log file for the server. This does not care if the server is running or not, it will
|
||||
// simply try to read the last X bytes of the file and return them.
|
||||
func (d *DockerEnvironment) Readlog(len int64) ([]string, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
j, err := d.Client.ContainerInspect(ctx, d.Server.Uuid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if j.LogPath == "" {
|
||||
return nil, errors.New("empty log path defined for server")
|
||||
}
|
||||
|
||||
f, err := os.Open(j.LogPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// Check if the length of the file is smaller than the amount of data that was requested
|
||||
// for reading. If so, adjust the length to be the total length of the file. If this is not
|
||||
// done an error is thrown since we're reading backwards, and not forwards.
|
||||
if stat, err := os.Stat(j.LogPath); err != nil {
|
||||
return nil, err
|
||||
} else if stat.Size() < len {
|
||||
len = stat.Size()
|
||||
}
|
||||
|
||||
// Seed to the end of the file and then move backwards until the length is met to avoid
|
||||
// reading the entirety of the file into memory.
|
||||
if _, err := f.Seek(-len, io.SeekEnd); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b := make([]byte, len)
|
||||
|
||||
if _, err := f.Read(b); err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return d.parseLogToStrings(b)
|
||||
}
|
||||
|
||||
type dockerLogLine struct {
|
||||
Log string `json:"log"`
|
||||
}
|
||||
|
||||
// Docker stores the logs for server output in a JSON format. This function will iterate over the JSON
|
||||
// that was read from the log file and parse it into a more human readable format.
|
||||
func (d *DockerEnvironment) parseLogToStrings(b []byte) ([]string, error) {
|
||||
var hasError = false
|
||||
var out []string
|
||||
|
||||
scanner := bufio.NewScanner(bytes.NewReader(b))
|
||||
for scanner.Scan() {
|
||||
var l dockerLogLine
|
||||
// Unmarshal the contents and allow up to a single error before bailing out of the process. We
|
||||
// do this because if you're arbitrarily reading a length of the file you'll likely end up
|
||||
// with the first line in the output being improperly formatted JSON. In those cases we want to
|
||||
// just skip over it. However if we see another error we're going to bail out because that is an
|
||||
// abnormal situation.
|
||||
if err := json.Unmarshal([]byte(scanner.Text()), &l); err != nil {
|
||||
if hasError {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hasError = true
|
||||
continue
|
||||
}
|
||||
|
||||
out = append(out, l.Log)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Returns the environment variables for a server in KEY="VALUE" form.
|
||||
func (d *DockerEnvironment) environmentVariables() []string {
|
||||
var out = []string{
|
||||
|
|
|
@ -191,6 +191,11 @@ func FromConfiguration(data []byte, cfg DockerConfiguration) (*Server, error) {
|
|||
return s, nil
|
||||
}
|
||||
|
||||
// Reads the log file for a server up to a specified number of bytes.
|
||||
func (s *Server) ReadLogfile(len int64) ([]string, error) {
|
||||
return s.Environment().Readlog(len)
|
||||
}
|
||||
|
||||
func (s *Server) Environment() Environment {
|
||||
return s.environment
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user