github.com/mhilton/juju-juju@v0.0.0-20150901100907-a94dd2c73455/worker/logsender/worker.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package logsender
     5  
     6  import (
     7  	"encoding/json"
     8  	"fmt"
     9  	"time"
    10  
    11  	"github.com/juju/errors"
    12  	"github.com/juju/loggo"
    13  	"github.com/juju/utils"
    14  	"golang.org/x/net/websocket"
    15  
    16  	"github.com/juju/juju/agent"
    17  	"github.com/juju/juju/api"
    18  	"github.com/juju/juju/apiserver"
    19  	"github.com/juju/juju/apiserver/params"
    20  	"github.com/juju/juju/worker"
    21  	"github.com/juju/juju/worker/gate"
    22  )
    23  
    24  const loggerName = "juju.worker.logsender"
    25  
    26  var logger = loggo.GetLogger(loggerName)
    27  
    28  // New starts a logsender worker which reads log message structs from
    29  // a channel and sends them to the JES via the logsink API.
    30  func New(logs LogRecordCh, apiInfoGate gate.Waiter, agent agent.Agent) worker.Worker {
    31  	loop := func(stop <-chan struct{}) error {
    32  		logger.Debugf("started log-sender worker; waiting for api info")
    33  		select {
    34  		case <-apiInfoGate.Unlocked():
    35  		case <-stop:
    36  			return nil
    37  		}
    38  
    39  		logger.Debugf("dialing log-sender connection")
    40  		apiInfo := agent.CurrentConfig().APIInfo()
    41  		conn, err := dialLogsinkAPI(apiInfo)
    42  		if err != nil {
    43  			return errors.Annotate(err, "logsender dial failed")
    44  		}
    45  		defer conn.Close()
    46  
    47  		for {
    48  			select {
    49  			case rec := <-logs:
    50  				err := sendLogRecord(conn, rec.Time, rec.Module, rec.Location, rec.Level, rec.Message)
    51  				if err != nil {
    52  					return errors.Trace(err)
    53  				}
    54  				if rec.DroppedAfter > 0 {
    55  					// If messages were dropped after this one, report
    56  					// the count (the source of the log messages -
    57  					// BufferedLogWriter - handles the actual dropping
    58  					// and counting).
    59  					//
    60  					// Any logs indicated as dropped here are will
    61  					// never end up in the logs DB in the JES
    62  					// (although will still be in the local agent log
    63  					// file). Message dropping by the
    64  					// BufferedLogWriter is last resort protection
    65  					// against memory exhaustion and should only
    66  					// happen if API connectivity is lost for extended
    67  					// periods. The maximum in-memory log buffer is
    68  					// quite large (see the InstallBufferedLogWriter
    69  					// call in jujuDMain).
    70  					err := sendLogRecord(conn, rec.Time, loggerName, "", loggo.WARNING,
    71  						fmt.Sprintf("%d log messages dropped due to lack of API connectivity", rec.DroppedAfter))
    72  					if err != nil {
    73  						return errors.Trace(err)
    74  					}
    75  				}
    76  
    77  			case <-stop:
    78  				return nil
    79  			}
    80  		}
    81  	}
    82  	return worker.NewSimpleWorker(loop)
    83  }
    84  
    85  func dialLogsinkAPI(apiInfo *api.Info) (*websocket.Conn, error) {
    86  	// TODO(mjs) Most of this should be extracted to be shared for
    87  	// connections to both /log (debuglog) and /logsink.
    88  	header := utils.BasicAuthHeader(apiInfo.Tag.String(), apiInfo.Password)
    89  	header.Set("X-Juju-Nonce", apiInfo.Nonce)
    90  	conn, err := api.Connect(apiInfo, "/logsink", header, api.DialOpts{})
    91  	if err != nil {
    92  		return nil, errors.Annotate(err, "failed to connect to logsink API")
    93  	}
    94  
    95  	// Read the initial error and translate to a real error.
    96  	// Read up to the first new line character. We can't use bufio here as it
    97  	// reads too much from the reader.
    98  	line := make([]byte, 4096)
    99  	n, err := conn.Read(line)
   100  	if err != nil {
   101  		return nil, errors.Annotate(err, "unable to read initial response")
   102  	}
   103  	line = line[0:n]
   104  
   105  	var errResult params.ErrorResult
   106  	err = json.Unmarshal(line, &errResult)
   107  	if err != nil {
   108  		return nil, errors.Annotate(err, "unable to unmarshal initial response")
   109  	}
   110  	if errResult.Error != nil {
   111  		return nil, errors.Annotatef(errResult.Error, "initial server error")
   112  	}
   113  
   114  	return conn, nil
   115  }
   116  
   117  func sendLogRecord(conn *websocket.Conn, ts time.Time, module, location string, level loggo.Level, msg string) error {
   118  	err := websocket.JSON.Send(conn, &apiserver.LogMessage{
   119  		Time:     ts,
   120  		Module:   module,
   121  		Location: location,
   122  		Level:    level,
   123  		Message:  msg,
   124  	})
   125  	// Note: due to the fire-and-forget nature of the
   126  	// logsink API, it is possible that when the
   127  	// connection dies, any logs that were "in-flight"
   128  	// will not be recorded on the server side.
   129  	return errors.Annotate(err, "logsink connection failed")
   130  }