github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/api/logstream/logstream.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package logstream
     5  
     6  import (
     7  	"io"
     8  	"sync"
     9  
    10  	"github.com/juju/errors"
    11  	"github.com/juju/loggo"
    12  	"github.com/juju/version"
    13  	"gopkg.in/juju/names.v2"
    14  
    15  	"github.com/juju/juju/api/base"
    16  	"github.com/juju/juju/api/common/stream"
    17  	"github.com/juju/juju/apiserver/params"
    18  	"github.com/juju/juju/logfwd"
    19  )
    20  
    21  // jsonReadCloser provides the functionality to send JSON-serialized
    22  // values over a streaming connection.
    23  type jsonReadCloser interface {
    24  	io.Closer
    25  
    26  	// ReadJSON decodes the next JSON value from the connection and
    27  	// sets the value at the provided pointer to that newly decoded one.
    28  	ReadJSON(interface{}) error
    29  }
    30  
    31  // LogStream streams log entries of the /logstream API endpoint over
    32  // a websocket connection.
    33  type LogStream struct {
    34  	mu             sync.Mutex
    35  	stream         jsonReadCloser
    36  	controllerUUID string
    37  }
    38  
    39  // Open opens a websocket to the API's /logstream endpoint and returns
    40  // a stream of log records from that connection.
    41  func Open(conn base.StreamConnector, cfg params.LogStreamConfig, controllerUUID string) (*LogStream, error) {
    42  	wsStream, err := stream.Open(conn, "/logstream", &cfg)
    43  	if err != nil {
    44  		return nil, errors.Trace(err)
    45  	}
    46  	ls := &LogStream{
    47  		stream:         wsStream,
    48  		controllerUUID: controllerUUID,
    49  	}
    50  	return ls, nil
    51  }
    52  
    53  // Next returns the next batch of log records from the server. The records are
    54  // converted from the wire format into logfwd.Record. The first returned
    55  // record will be the one after the last successfully sent record. If no
    56  // records have been sent yet then it will be the oldest log record.
    57  //
    58  // An error indicates either the streaming connection is closed, the
    59  // connection failed, or the data read from the connection is corrupted.
    60  // In each of these cases the stream should be re-opened. It will start
    61  // at the record after the one marked as successfully sent. So the
    62  // the record at which Next() failed previously will be streamed again.
    63  //
    64  // That is only a problem when the same record is consistently streamed
    65  // as invalid data. This will happen only if the record was invalid
    66  // before being stored in the DB or if the DB on-disk storage for the
    67  // record becomes corrupted. Both scenarios are highly unlikely and
    68  // the respective systems are managed such that neither should happen.
    69  func (ls *LogStream) Next() ([]logfwd.Record, error) {
    70  	apiRecords, err := ls.next()
    71  	if err != nil {
    72  		return nil, errors.Trace(err)
    73  	}
    74  	records, err := recordsFromAPI(apiRecords, ls.controllerUUID)
    75  	if err != nil {
    76  		// This should only happen if the data got corrupted over the
    77  		// network. Any other cause should be addressed by fixing the
    78  		// code that resulted in the bad data that caused the error
    79  		// here. If that code is between the DB on-disk storage and
    80  		// the server-side stream then care should be taken to not
    81  		// block on a consistently invalid record or to throw away
    82  		// a record. The log stream needs to maintain a high level
    83  		// of reliable delivery.
    84  		return nil, errors.Trace(err)
    85  	}
    86  	return records, nil
    87  }
    88  
    89  func (ls *LogStream) next() (params.LogStreamRecords, error) {
    90  	ls.mu.Lock()
    91  	defer ls.mu.Unlock()
    92  
    93  	var result params.LogStreamRecords
    94  	if ls.stream == nil {
    95  		return result, errors.Errorf("cannot read from closed stream")
    96  	}
    97  
    98  	err := ls.stream.ReadJSON(&result)
    99  	if err != nil {
   100  		return result, errors.Trace(err)
   101  	}
   102  	return result, nil
   103  }
   104  
   105  // Close closes the stream.
   106  func (ls *LogStream) Close() error {
   107  	ls.mu.Lock()
   108  	defer ls.mu.Unlock()
   109  
   110  	if ls.stream == nil {
   111  		return nil
   112  	}
   113  	if err := ls.stream.Close(); err != nil {
   114  		return errors.Trace(err)
   115  	}
   116  	ls.stream = nil
   117  	return nil
   118  }
   119  
   120  // See the counterpart in apiserver/logstream.go.
   121  func recordsFromAPI(apiRecords params.LogStreamRecords, controllerUUID string) ([]logfwd.Record, error) {
   122  	result := make([]logfwd.Record, len(apiRecords.Records))
   123  	for i, apiRec := range apiRecords.Records {
   124  		rec, err := recordFromAPI(apiRec, controllerUUID)
   125  		if err != nil {
   126  			return nil, errors.Trace(err)
   127  		}
   128  		result[i] = rec
   129  	}
   130  	return result, nil
   131  }
   132  
   133  func recordFromAPI(apiRec params.LogStreamRecord, controllerUUID string) (logfwd.Record, error) {
   134  	rec := logfwd.Record{
   135  		ID:        apiRec.ID,
   136  		Timestamp: apiRec.Timestamp,
   137  		Message:   apiRec.Message,
   138  	}
   139  
   140  	origin, err := originFromAPI(apiRec, controllerUUID)
   141  	if err != nil {
   142  		return rec, errors.Trace(err)
   143  	}
   144  	rec.Origin = origin
   145  
   146  	loc, err := logfwd.ParseLocation(apiRec.Module, apiRec.Location)
   147  	if err != nil {
   148  		return rec, errors.Trace(err)
   149  	}
   150  	rec.Location = loc
   151  
   152  	level, ok := loggo.ParseLevel(apiRec.Level)
   153  	if !ok {
   154  		return rec, errors.Errorf("unrecognized log level %q", apiRec.Level)
   155  	}
   156  	rec.Level = level
   157  
   158  	if err := rec.Validate(); err != nil {
   159  		return rec, errors.Trace(err)
   160  	}
   161  
   162  	return rec, nil
   163  }
   164  
   165  func originFromAPI(apiRec params.LogStreamRecord, controllerUUID string) (logfwd.Origin, error) {
   166  	var origin logfwd.Origin
   167  
   168  	tag, err := names.ParseTag(apiRec.Entity)
   169  	if err != nil {
   170  		return origin, errors.Annotate(err, "invalid entity")
   171  	}
   172  
   173  	ver, err := version.Parse(apiRec.Version)
   174  	if err != nil {
   175  		return origin, errors.Annotatef(err, "invalid version %q", apiRec.Version)
   176  	}
   177  
   178  	switch tag := tag.(type) {
   179  	case names.MachineTag:
   180  		origin = logfwd.OriginForMachineAgent(tag, controllerUUID, apiRec.ModelUUID, ver)
   181  	case names.UnitTag:
   182  		origin = logfwd.OriginForUnitAgent(tag, controllerUUID, apiRec.ModelUUID, ver)
   183  	default:
   184  		origin, err = logfwd.OriginForJuju(tag, controllerUUID, apiRec.ModelUUID, ver)
   185  		if err != nil {
   186  			return origin, errors.Annotate(err, "could not extract origin")
   187  		}
   188  	}
   189  	return origin, nil
   190  }