github.com/kubeshop/testkube@v1.17.23/pkg/logs/events/events.go (about)

     1  package events
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"regexp"
     7  	"time"
     8  
     9  	"github.com/kubeshop/testkube/pkg/api/v1/testkube"
    10  	"github.com/kubeshop/testkube/pkg/executor/output"
    11  )
    12  
    13  type LogVersion string
    14  
    15  const (
    16  	// v1 - old log format based on shell output of executors {"line":"...", "time":"..."}
    17  	LogVersionV1 LogVersion = "v1"
    18  	// v2 - raw binary format, timestamps are based on Kubernetes logs, line is raw log line
    19  	LogVersionV2 LogVersion = "v2"
    20  
    21  	SourceJobPod            = "job-pod"
    22  	SourceScheduler         = "test-scheduler"
    23  	SourceContainerExecutor = "container-executor"
    24  	SourceJobExecutor       = "job-executor"
    25  	SourceLogsProxy         = "logs-proxy"
    26  )
    27  
    28  // check if trigger implements model generic event type
    29  var _ testkube.Trigger = Trigger{}
    30  
    31  // NewTrigger returns Trigger instance
    32  func NewTrigger(id string) Trigger {
    33  	return Trigger{ResourceId: id}
    34  }
    35  
    36  // Generic event like log-start log-end with resource id
    37  type Trigger struct {
    38  	ResourceId string `json:"resourceId,omitempty"`
    39  }
    40  
    41  // GetResourceId implements testkube.Trigger interface
    42  func (t Trigger) GetResourceId() string {
    43  	return t.ResourceId
    44  }
    45  
    46  type LogResponse struct {
    47  	Log   Log
    48  	Error error
    49  }
    50  
    51  type Log testkube.LogV2
    52  
    53  func NewFinishLog() *Log {
    54  	return &Log{
    55  		Time:    time.Now(),
    56  		Content: "processing logs finished",
    57  		Type_:   "finish",
    58  		Source:  "log-server",
    59  	}
    60  }
    61  
    62  func IsFinished(log *Log) bool {
    63  	return log.Type_ == "finish"
    64  }
    65  
    66  func NewErrorLog(err error) *Log {
    67  	var msg string
    68  	if err != nil {
    69  		msg = err.Error()
    70  	}
    71  	return &Log{
    72  		Time:    time.Now(),
    73  		Error_:  true,
    74  		Content: msg,
    75  	}
    76  }
    77  
    78  func NewLog(content ...string) *Log {
    79  	log := &Log{
    80  		Time:     time.Now(),
    81  		Metadata: map[string]string{},
    82  	}
    83  
    84  	if len(content) > 0 {
    85  		log.WithContent(content[0])
    86  	}
    87  
    88  	return log
    89  }
    90  
    91  func (l *Log) WithContent(s string) *Log {
    92  	l.Content = s
    93  	return l
    94  }
    95  
    96  func (l *Log) WithError(err error) *Log {
    97  	l.Error_ = true
    98  
    99  	if err != nil {
   100  		l.Content = err.Error()
   101  	}
   102  
   103  	return l
   104  }
   105  
   106  func (l *Log) WithMetadataEntry(key, value string) *Log {
   107  	if l.Metadata == nil {
   108  		l.Metadata = map[string]string{}
   109  	}
   110  	l.Metadata[key] = value
   111  	return l
   112  }
   113  
   114  func (l *Log) WithType(t string) *Log {
   115  	l.Type_ = t
   116  	return l
   117  }
   118  
   119  func (l *Log) WithSource(s string) *Log {
   120  	l.Source = s
   121  	return l
   122  }
   123  
   124  func (l *Log) WithVersion(version LogVersion) *Log {
   125  	l.Version = string(version)
   126  	return l
   127  }
   128  
   129  func (l *Log) WithV1Result(result *testkube.ExecutionResult) *Log {
   130  	l.V1.Result = result
   131  	return l
   132  }
   133  
   134  var timestampRegexp = regexp.MustCompile("^[0-9]{4}-[0-9]{2}-[0-9]{2}T.*")
   135  
   136  // NewLogFromBytes creates new LogResponse from bytes it's aware of new and old log formats
   137  // default log format will be based on raw bytes with timestamp on the beginning
   138  func NewLogFromBytes(b []byte) *Log {
   139  
   140  	// detect timestamp - new logs have timestamp
   141  	var (
   142  		hasTimestamp bool
   143  		ts           time.Time
   144  		content      []byte
   145  		err          error
   146  	)
   147  
   148  	if timestampRegexp.Match(b) {
   149  		hasTimestamp = true
   150  	}
   151  
   152  	// if there is output with timestamp
   153  	if hasTimestamp {
   154  		s := bytes.SplitN(b, []byte(" "), 2)
   155  		ts, err = time.Parse(time.RFC3339Nano, string(s[0]))
   156  		// fallback to now in case of error
   157  		if err != nil {
   158  			ts = time.Now()
   159  		}
   160  
   161  		content = s[1]
   162  	} else {
   163  		ts = time.Now()
   164  		content = b
   165  	}
   166  
   167  	// DEPRECATED - old log format
   168  	// detect JSON and try to parse old log structure
   169  
   170  	// We need .Content if available
   171  	// .Time - is not needed at all - timestamp will be get from Kubernetes logs
   172  	// One thing which need to be handled is result
   173  	// .Result
   174  
   175  	if bytes.HasPrefix(content, []byte("{")) {
   176  		o := output.GetLogEntry(content)
   177  		// pass parsed results for v1
   178  		// for new executor it'll be omitted in logs (as looks like we're not using it already)
   179  		if o.Type_ == output.TypeResult {
   180  			return &Log{
   181  				Time:    ts,
   182  				Content: o.Content,
   183  				Version: string(LogVersionV1),
   184  				V1: &testkube.LogV1{
   185  					Result: o.Result,
   186  				},
   187  			}
   188  		}
   189  
   190  		return &Log{
   191  			Time:    ts,
   192  			Content: o.Content,
   193  			Version: string(LogVersionV1),
   194  		}
   195  	}
   196  	// END DEPRECATED
   197  
   198  	// new non-JSON format (just raw lines will be logged)
   199  	return &Log{
   200  		Time:    ts,
   201  		Content: string(content),
   202  		Version: string(LogVersionV2),
   203  	}
   204  }
   205  
   206  // ReadLogLine tries to read possible log lines from any source
   207  // - logv2 - JSON
   208  // - logv1 - old log format JSON - DEPRECATED
   209  // - possible errors or raw log lines
   210  func ReadLogLine(b []byte) *Log {
   211  	logsV1Prefix := []byte("{\"id\"")
   212  	logsV2Prefix := []byte("{")
   213  
   214  	switch true {
   215  	case bytes.HasPrefix(b, logsV1Prefix):
   216  		return mapLogV1toV2(output.GetLogEntry(b))
   217  
   218  	case bytes.HasPrefix(b, logsV2Prefix):
   219  		var o Log
   220  		err := json.Unmarshal(b, &o)
   221  		if err != nil {
   222  			return newErrorLog(err, b)
   223  		}
   224  		return &o
   225  	}
   226  
   227  	return &Log{
   228  		Content: string(b),
   229  	}
   230  }
   231  
   232  func newErrorLog(err error, b []byte) *Log {
   233  	return &Log{
   234  		Content:  string(b),
   235  		Error_:   true,
   236  		Version:  string(LogVersionV1),
   237  		Metadata: map[string]string{"error": err.Error()},
   238  	}
   239  
   240  }
   241  
   242  func mapLogV1toV2(o output.Output) *Log {
   243  	// pass parsed results for v1
   244  	// for new executor it'll be omitted in logs (as looks like we're not using it already)
   245  	if o.Type_ == output.TypeResult {
   246  		return &Log{
   247  			Time:    o.Time,
   248  			Content: o.Content,
   249  			Version: string(LogVersionV1),
   250  			V1: &testkube.LogV1{
   251  				Result: o.Result,
   252  			},
   253  		}
   254  	}
   255  
   256  	return &Log{
   257  		Time:    o.Time,
   258  		Content: o.Content,
   259  		Version: string(LogVersionV1),
   260  	}
   261  
   262  }