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