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 }