github.com/swisscom/cloudfoundry-cli@v7.1.0+incompatible/actor/sharedaction/logging.go (about)

     1  package sharedaction
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"log"
     8  	"strings"
     9  	"time"
    10  
    11  	logcache "code.cloudfoundry.org/go-log-cache"
    12  	"code.cloudfoundry.org/go-log-cache/rpc/logcache_v1"
    13  	"code.cloudfoundry.org/go-loggregator/rpc/loggregator_v2"
    14  	"github.com/sirupsen/logrus"
    15  )
    16  
    17  const (
    18  	StagingLog      = "STG"
    19  	RecentLogsLines = 1000
    20  
    21  	retryCount    = 5
    22  	retryInterval = time.Millisecond * 250
    23  )
    24  
    25  type LogMessage struct {
    26  	message        string
    27  	messageType    string
    28  	timestamp      time.Time
    29  	sourceType     string
    30  	sourceInstance string
    31  }
    32  
    33  func (log LogMessage) Message() string {
    34  	return log.message
    35  }
    36  
    37  func (log LogMessage) Type() string {
    38  	return log.messageType
    39  }
    40  
    41  func (log LogMessage) Staging() bool {
    42  	return log.sourceType == StagingLog
    43  }
    44  
    45  func (log LogMessage) Timestamp() time.Time {
    46  	return log.timestamp
    47  }
    48  
    49  func (log LogMessage) SourceType() string {
    50  	return log.sourceType
    51  }
    52  
    53  func (log LogMessage) SourceInstance() string {
    54  	return log.sourceInstance
    55  }
    56  
    57  func NewLogMessage(message string, messageType string, timestamp time.Time, sourceType string, sourceInstance string) *LogMessage {
    58  	return &LogMessage{
    59  		message:        message,
    60  		messageType:    messageType,
    61  		timestamp:      timestamp,
    62  		sourceType:     sourceType,
    63  		sourceInstance: sourceInstance,
    64  	}
    65  }
    66  
    67  type LogMessages []*LogMessage
    68  
    69  func (lm LogMessages) Len() int { return len(lm) }
    70  
    71  func (lm LogMessages) Less(i, j int) bool {
    72  	return lm[i].timestamp.Before(lm[j].timestamp)
    73  }
    74  
    75  func (lm LogMessages) Swap(i, j int) {
    76  	lm[i], lm[j] = lm[j], lm[i]
    77  }
    78  
    79  type channelWriter struct {
    80  	errChannel chan error
    81  }
    82  
    83  func (c channelWriter) Write(bytes []byte) (n int, err error) {
    84  	c.errChannel <- errors.New(strings.Trim(string(bytes), "\n"))
    85  
    86  	return len(bytes), nil
    87  }
    88  
    89  // cliRetryBackoff returns true for OnErr after sleeping the given interval for a limited number of times,
    90  // and returns true for OnEmpty always.
    91  // Basically: retry x times on connection failures, and wait forever for logs to show up.
    92  type cliRetryBackoff struct {
    93  	interval time.Duration
    94  	maxCount int
    95  	count    int
    96  }
    97  
    98  func newCliRetryBackoff(interval time.Duration, maxCount int) *cliRetryBackoff {
    99  	return &cliRetryBackoff{
   100  		interval: interval,
   101  		maxCount: maxCount,
   102  	}
   103  }
   104  
   105  func (b *cliRetryBackoff) OnErr(error) bool {
   106  	b.count++
   107  	if b.count >= b.maxCount {
   108  		return false
   109  	}
   110  
   111  	time.Sleep(b.interval)
   112  	return true
   113  }
   114  
   115  func (b *cliRetryBackoff) OnEmpty() bool {
   116  	return true
   117  }
   118  
   119  func (b *cliRetryBackoff) Reset() {
   120  	b.count = 0
   121  }
   122  
   123  func GetStreamingLogs(appGUID string, client LogCacheClient) (<-chan LogMessage, <-chan error, context.CancelFunc) {
   124  
   125  	logrus.Info("Start Tailing Logs")
   126  
   127  	outgoingLogStream := make(chan LogMessage, 1000)
   128  	outgoingErrStream := make(chan error, 1000)
   129  	ctx, cancelFunc := context.WithCancel(context.Background())
   130  	go func() {
   131  		defer close(outgoingLogStream)
   132  		defer close(outgoingErrStream)
   133  
   134  		ts := latestEnvelopeTimestamp(client, outgoingErrStream, ctx, appGUID)
   135  
   136  		// if the context was cancelled we may not have seen an envelope
   137  		if ts.IsZero() {
   138  			return
   139  		}
   140  
   141  		const offset = 1 * time.Second
   142  		walkStartTime := ts.Add(-offset)
   143  
   144  		logcache.Walk(
   145  			ctx,
   146  			appGUID,
   147  			logcache.Visitor(func(envelopes []*loggregator_v2.Envelope) bool {
   148  				logMessages := convertEnvelopesToLogMessages(envelopes)
   149  				for _, logMessage := range logMessages {
   150  					select {
   151  					case <-ctx.Done():
   152  						return false
   153  					default:
   154  						outgoingLogStream <- *logMessage
   155  					}
   156  				}
   157  				return true
   158  			}),
   159  			client.Read,
   160  			logcache.WithWalkDelay(2*time.Second),
   161  			logcache.WithWalkStartTime(walkStartTime),
   162  			logcache.WithWalkEnvelopeTypes(logcache_v1.EnvelopeType_LOG),
   163  			logcache.WithWalkBackoff(newCliRetryBackoff(retryInterval, retryCount)),
   164  			logcache.WithWalkLogger(log.New(channelWriter{
   165  				errChannel: outgoingErrStream,
   166  			}, "", 0)),
   167  		)
   168  	}()
   169  
   170  	return outgoingLogStream, outgoingErrStream, cancelFunc
   171  }
   172  
   173  func latestEnvelopeTimestamp(client LogCacheClient, errs chan error, ctx context.Context, sourceID string) time.Time {
   174  
   175  	// Fetching the most recent timestamp could be implemented with client.Read directly rather than using logcache.Walk
   176  	// We use Walk because we want the extra retry behavior provided through Walk
   177  
   178  	// Wrap client.Read in our own function to allow us to specify our own read options
   179  	// https://github.com/cloudfoundry/go-log-cache/issues/27
   180  	r := func(ctx context.Context, sourceID string, _ time.Time, opts ...logcache.ReadOption) ([]*loggregator_v2.Envelope, error) {
   181  		os := []logcache.ReadOption{
   182  			logcache.WithLimit(1),
   183  			logcache.WithDescending(),
   184  		}
   185  		for _, o := range opts {
   186  			os = append(os, o)
   187  		}
   188  		return client.Read(ctx, sourceID, time.Time{}, os...)
   189  	}
   190  
   191  	var timestamp time.Time
   192  
   193  	logcache.Walk(
   194  		ctx,
   195  		sourceID,
   196  		logcache.Visitor(func(envelopes []*loggregator_v2.Envelope) bool {
   197  			timestamp = time.Unix(0, envelopes[0].Timestamp)
   198  			return false
   199  		}),
   200  		r,
   201  		logcache.WithWalkBackoff(newCliRetryBackoff(retryInterval, retryCount)),
   202  		logcache.WithWalkLogger(log.New(channelWriter{
   203  			errChannel: errs,
   204  		}, "", 0)),
   205  	)
   206  
   207  	return timestamp
   208  }
   209  
   210  func GetRecentLogs(appGUID string, client LogCacheClient) ([]LogMessage, error) {
   211  	logLineRequestCount := RecentLogsLines
   212  	var envelopes []*loggregator_v2.Envelope
   213  	var err error
   214  
   215  	for logLineRequestCount >= 1 {
   216  		envelopes, err = client.Read(
   217  			context.Background(),
   218  			appGUID,
   219  			time.Time{},
   220  			logcache.WithEnvelopeTypes(logcache_v1.EnvelopeType_LOG),
   221  			logcache.WithLimit(logLineRequestCount),
   222  			logcache.WithDescending(),
   223  		)
   224  		if err == nil || err.Error() != "unexpected status code 429" {
   225  			break
   226  		}
   227  		logLineRequestCount /= 2
   228  	}
   229  	if err != nil {
   230  		return nil, fmt.Errorf("Failed to retrieve logs from Log Cache: %s", err)
   231  	}
   232  
   233  	logMessages := convertEnvelopesToLogMessages(envelopes)
   234  	var reorderedLogMessages []LogMessage
   235  	for i := len(logMessages) - 1; i >= 0; i-- {
   236  		reorderedLogMessages = append(reorderedLogMessages, *logMessages[i])
   237  	}
   238  
   239  	return reorderedLogMessages, nil
   240  }
   241  
   242  func convertEnvelopesToLogMessages(envelopes []*loggregator_v2.Envelope) []*LogMessage {
   243  	var logMessages []*LogMessage
   244  	for _, envelope := range envelopes {
   245  		logEnvelope, ok := envelope.GetMessage().(*loggregator_v2.Envelope_Log)
   246  		if !ok {
   247  			continue
   248  		}
   249  		log := logEnvelope.Log
   250  
   251  		logMessages = append(logMessages, NewLogMessage(
   252  			string(log.Payload),
   253  			loggregator_v2.Log_Type_name[int32(log.Type)],
   254  			time.Unix(0, envelope.GetTimestamp()),
   255  			envelope.GetTags()["source_type"],
   256  			envelope.GetInstanceId(),
   257  		))
   258  	}
   259  	return logMessages
   260  }