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/onsi/gomega v1.5.0 // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
|
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
|
||||||
github.com/opencontainers/image-spec v1.0.1 // 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/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/remeh/sizedwaitgroup v0.0.0-20180822144253-5e7302b12cce
|
github.com/remeh/sizedwaitgroup v0.0.0-20180822144253-5e7302b12cce
|
||||||
github.com/sirupsen/logrus v1.0.5 // indirect
|
github.com/sirupsen/logrus v1.0.5 // indirect
|
||||||
|
|
22
http.go
22
http.go
|
@ -9,6 +9,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -169,6 +170,25 @@ func (rt *Router) routeServerPower(w http.ResponseWriter, r *http.Request, ps ht
|
||||||
w.WriteHeader(http.StatusAccepted)
|
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.
|
// Configures the router and all of the associated routes.
|
||||||
func (rt *Router) ConfigureRouter() *httprouter.Router {
|
func (rt *Router) ConfigureRouter() *httprouter.Router {
|
||||||
router := httprouter.New()
|
router := httprouter.New()
|
||||||
|
@ -176,6 +196,8 @@ func (rt *Router) ConfigureRouter() *httprouter.Router {
|
||||||
router.GET("/", rt.routeIndex)
|
router.GET("/", rt.routeIndex)
|
||||||
router.GET("/api/servers", rt.AuthenticateToken("i:servers", rt.routeAllServers))
|
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", 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)))
|
router.POST("/api/servers/:server/power", rt.AuthenticateToken("s:power", rt.AuthenticateServer(rt.routeServerPower)))
|
||||||
|
|
||||||
return router
|
return router
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Defines the basic interface that all environments need to implement so that
|
// Defines the basic interface that all environments need to implement so that
|
||||||
// a server can be properly controlled.
|
// a server can be properly controlled.
|
||||||
type Environment interface {
|
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
|
// Starts a server instance. If the server instance is not in a state where it
|
||||||
// can be started an error should be returned.
|
// can be started an error should be returned.
|
||||||
Start() error
|
Start() error
|
||||||
|
@ -28,4 +32,12 @@ type Environment interface {
|
||||||
// in the Docker environment create will create a new container instance for the
|
// in the Docker environment create will create a new container instance for the
|
||||||
// server.
|
// server.
|
||||||
Create() error
|
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
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
|
@ -8,7 +11,9 @@ import (
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/docker/docker/daemon/logger/jsonfilelog"
|
"github.com/docker/docker/daemon/logger/jsonfilelog"
|
||||||
"github.com/docker/go-connections/nat"
|
"github.com/docker/go-connections/nat"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -77,6 +82,11 @@ func NewDockerEnvironment(cfg DockerConfiguration) (*DockerEnvironment, error) {
|
||||||
// from the base environment interface.
|
// from the base environment interface.
|
||||||
var _ Environment = (*DockerEnvironment)(nil)
|
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.
|
// Determines if the container exists in this environment.
|
||||||
func (d *DockerEnvironment) Exists() bool {
|
func (d *DockerEnvironment) Exists() bool {
|
||||||
_, err := d.Client.ContainerInspect(context.Background(), d.Server.Uuid)
|
_, 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())
|
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
|
// 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.
|
// available for it. If the container already exists it will be returned.
|
||||||
func (d *DockerEnvironment) Create() error {
|
func (d *DockerEnvironment) Create() error {
|
||||||
|
@ -239,6 +274,83 @@ func (d *DockerEnvironment) Create() error {
|
||||||
return nil
|
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.
|
// Returns the environment variables for a server in KEY="VALUE" form.
|
||||||
func (d *DockerEnvironment) environmentVariables() []string {
|
func (d *DockerEnvironment) environmentVariables() []string {
|
||||||
var out = []string{
|
var out = []string{
|
||||||
|
|
|
@ -191,6 +191,11 @@ func FromConfiguration(data []byte, cfg DockerConfiguration) (*Server, error) {
|
||||||
return s, nil
|
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 {
|
func (s *Server) Environment() Environment {
|
||||||
return s.environment
|
return s.environment
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user