github.com/wanddynosios/cli/v8@v8.7.9-0.20240221182337-1a92e3a7017f/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/v2"
    12  	"code.cloudfoundry.org/go-log-cache/v2/rpc/logcache_v1"
    13  	"code.cloudfoundry.org/go-loggregator/v9/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  	time.Sleep(b.interval)
   117  	return true
   118  }
   119  
   120  func (b *cliRetryBackoff) Reset() {
   121  	b.count = 0
   122  }
   123  
   124  func GetStreamingLogs(appGUID string, client LogCacheClient) (<-chan LogMessage, <-chan error, context.CancelFunc) {
   125  
   126  	logrus.Info("Start Tailing Logs")
   127  
   128  	outgoingLogStream := make(chan LogMessage, 1000)
   129  	outgoingErrStream := make(chan error, 1000)
   130  	ctx, cancelFunc := context.WithCancel(context.Background())
   131  	go func() {
   132  		defer close(outgoingLogStream)
   133  		defer close(outgoingErrStream)
   134  
   135  		ts := latestEnvelopeTimestamp(client, outgoingErrStream, ctx, appGUID)
   136  
   137  		// if the context was cancelled we may not have seen an envelope
   138  		if ts.IsZero() {
   139  			return
   140  		}
   141  
   142  		const offset = 1 * time.Second
   143  		walkStartTime := ts.Add(-offset)
   144  
   145  		logcache.Walk(
   146  			ctx,
   147  			appGUID,
   148  			logcache.Visitor(func(envelopes []*loggregator_v2.Envelope) bool {
   149  				logMessages := convertEnvelopesToLogMessages(envelopes)
   150  				for _, logMessage := range logMessages {
   151  					select {
   152  					case <-ctx.Done():
   153  						return false
   154  					default:
   155  						outgoingLogStream <- *logMessage
   156  					}
   157  				}
   158  				return true
   159  			}),
   160  			client.Read,
   161  			logcache.WithWalkDelay(2*time.Second),
   162  			logcache.WithWalkStartTime(walkStartTime),
   163  			logcache.WithWalkEnvelopeTypes(logcache_v1.EnvelopeType_LOG),
   164  			logcache.WithWalkBackoff(newCliRetryBackoff(retryInterval, retryCount)),
   165  			logcache.WithWalkLogger(log.New(channelWriter{
   166  				errChannel: outgoingErrStream,
   167  			}, "", 0)),
   168  		)
   169  	}()
   170  
   171  	return outgoingLogStream, outgoingErrStream, cancelFunc
   172  }
   173  
   174  func latestEnvelopeTimestamp(client LogCacheClient, errs chan error, ctx context.Context, sourceID string) time.Time {
   175  
   176  	// Fetching the most recent timestamp could be implemented with client.Read directly rather than using logcache.Walk
   177  	// We use Walk because we want the extra retry behavior provided through Walk
   178  
   179  	// Wrap client.Read in our own function to allow us to specify our own read options
   180  	// https://github.com/cloudfoundry/go-log-cache/issues/27
   181  	r := func(ctx context.Context, sourceID string, _ time.Time, opts ...logcache.ReadOption) ([]*loggregator_v2.Envelope, error) {
   182  		os := []logcache.ReadOption{
   183  			logcache.WithLimit(1),
   184  			logcache.WithDescending(),
   185  		}
   186  		for _, o := range opts {
   187  			os = append(os, o)
   188  		}
   189  		return client.Read(ctx, sourceID, time.Time{}, os...)
   190  	}
   191  
   192  	var timestamp time.Time
   193  
   194  	logcache.Walk(
   195  		ctx,
   196  		sourceID,
   197  		logcache.Visitor(func(envelopes []*loggregator_v2.Envelope) bool {
   198  			timestamp = time.Unix(0, envelopes[0].Timestamp)
   199  			return false
   200  		}),
   201  		r,
   202  		logcache.WithWalkBackoff(newCliRetryBackoff(retryInterval, retryCount)),
   203  		logcache.WithWalkLogger(log.New(channelWriter{
   204  			errChannel: errs,
   205  		}, "", 0)),
   206  	)
   207  
   208  	return timestamp
   209  }
   210  
   211  func GetRecentLogs(appGUID string, client LogCacheClient) ([]LogMessage, error) {
   212  	logLineRequestCount := RecentLogsLines
   213  	var envelopes []*loggregator_v2.Envelope
   214  	var err error
   215  
   216  	for logLineRequestCount >= 1 {
   217  		envelopes, err = client.Read(
   218  			context.Background(),
   219  			appGUID,
   220  			time.Time{},
   221  			logcache.WithEnvelopeTypes(logcache_v1.EnvelopeType_LOG),
   222  			logcache.WithLimit(logLineRequestCount),
   223  			logcache.WithDescending(),
   224  		)
   225  		if err == nil || err.Error() != "unexpected status code 429" {
   226  			break
   227  		}
   228  		logLineRequestCount /= 2
   229  	}
   230  	if err != nil {
   231  		return nil, fmt.Errorf("Failed to retrieve logs from Log Cache: %s", err)
   232  	}
   233  
   234  	logMessages := convertEnvelopesToLogMessages(envelopes)
   235  	var reorderedLogMessages []LogMessage
   236  	for i := len(logMessages) - 1; i >= 0; i-- {
   237  		reorderedLogMessages = append(reorderedLogMessages, *logMessages[i])
   238  	}
   239  
   240  	return reorderedLogMessages, nil
   241  }
   242  
   243  func convertEnvelopesToLogMessages(envelopes []*loggregator_v2.Envelope) []*LogMessage {
   244  	var logMessages []*LogMessage
   245  	for _, envelope := range envelopes {
   246  		logEnvelope, ok := envelope.GetMessage().(*loggregator_v2.Envelope_Log)
   247  		if !ok {
   248  			continue
   249  		}
   250  		log := logEnvelope.Log
   251  
   252  		logMessages = append(logMessages, NewLogMessage(
   253  			string(log.Payload),
   254  			loggregator_v2.Log_Type_name[int32(log.Type)],
   255  			time.Unix(0, envelope.GetTimestamp()),
   256  			envelope.GetTags()["source_type"],
   257  			envelope.GetInstanceId(),
   258  		))
   259  	}
   260  	return logMessages
   261  }