| package consensus |
| |
| import ( |
| "encoding/json" |
| "fmt" |
| "sort" |
| "strconv" |
| "strings" |
| "time" |
| |
| "source.monogon.dev/metropolis/pkg/logbuffer" |
| "source.monogon.dev/metropolis/pkg/logtree" |
| "source.monogon.dev/metropolis/pkg/logtree/unraw" |
| ) |
| |
| // etcdLogEntry is a JSON-encoded, structured log entry received from a running |
| // etcd server. The format comes from the logging library used there, |
| // github.com/uber-go/zap. |
| type etcdLogEntry struct { |
| Level string `json:"level"` |
| TS time.Time `json:"ts"` |
| Caller string `json:"caller"` |
| Message string `json:"msg"` |
| Extras map[string]interface{} `json:"-"` |
| } |
| |
| // parseEtcdLogEntry is a logtree/unraw compatible parser for etcd log lines. |
| // It is fairly liberal in what it will accept, falling back to writing a |
| // message that outlines the given log entry could not have been parsed. This |
| // ensures that no lines are lost, even if malformed. |
| func parseEtcdLogEntry(l *logbuffer.Line, write unraw.LeveledWriter) { |
| if l.Truncated() { |
| write(&logtree.ExternalLeveledPayload{ |
| Message: "Log line truncated: " + l.Data, |
| }) |
| return |
| } |
| |
| e := etcdLogEntry{} |
| // Parse constant fields |
| if err := json.Unmarshal([]byte(l.Data), &e); err != nil { |
| write(&logtree.ExternalLeveledPayload{ |
| Message: "Log line unparseable: " + l.Data, |
| }) |
| return |
| } |
| // Parse extra fields. |
| if err := json.Unmarshal([]byte(l.Data), &e.Extras); err != nil { |
| // Not exactly sure how this could ever happen - the previous parse |
| // went fine, so why wouldn't this one? But to be on the safe side, |
| // just don't attempt to parse this line any further. |
| write(&logtree.ExternalLeveledPayload{ |
| Message: "Log line unparseable: " + l.Data, |
| }) |
| return |
| } |
| delete(e.Extras, "level") |
| delete(e.Extras, "ts") |
| delete(e.Extras, "caller") |
| delete(e.Extras, "msg") |
| |
| out := logtree.ExternalLeveledPayload{ |
| Timestamp: e.TS, |
| } |
| |
| // Attempt to parse caller (eg. raft/raft.go:765) into file/line (eg. |
| // raft.go 765). |
| if len(e.Caller) > 0 { |
| parts := strings.Split(e.Caller, "/") |
| fileLine := parts[len(parts)-1] |
| parts = strings.Split(fileLine, ":") |
| if len(parts) == 2 { |
| out.File = parts[0] |
| if line, err := strconv.ParseInt(parts[1], 10, 32); err == nil { |
| out.Line = int(line) |
| } |
| } |
| } |
| |
| // Convert zap level into logtree severity. |
| switch e.Level { |
| case "info": |
| out.Severity = logtree.INFO |
| case "warn": |
| out.Severity = logtree.WARNING |
| case "error": |
| out.Severity = logtree.ERROR |
| case "fatal", "panic", "dpanic": |
| out.Severity = logtree.FATAL |
| } |
| |
| // Sort extra keys alphabetically. |
| extraKeys := make([]string, 0, len(e.Extras)) |
| for k, _ := range e.Extras { |
| extraKeys = append(extraKeys, k) |
| } |
| sort.Strings(extraKeys) |
| |
| // Convert structured extras into a human-friendly logline. We will |
| // comma-join the received message and any structured logging data after |
| // it. |
| parts := make([]string, 0, len(e.Extras)+1) |
| parts = append(parts, e.Message) |
| for _, k := range extraKeys { |
| |
| // Format the value for logs. We elect to use JSON for representing |
| // each element, as: |
| // - this quotes strings |
| // - all the data we retrieved must already be representable in JSON, |
| // as we just decoded it from an existing blob. |
| // - the extra data might be arbitrarily nested (eg. an array or |
| // object) and we don't want to be in the business of coming up with |
| // our own serialization format in case of such data. |
| var v string |
| vbytes, err := json.Marshal(e.Extras[k]) |
| if err != nil { |
| // Fall back to +%v just in case. We don't make any API promises |
| // that the log line will be machine parseable or in any stable |
| // format. |
| v = fmt.Sprintf("%+v", v) |
| } else { |
| v = string(vbytes) |
| } |
| extra := fmt.Sprintf("%s: %s", k, v) |
| |
| parts = append(parts, extra) |
| } |
| |
| // If the given message was empty and there are some extra data attached, |
| // explicitly state that the message was empty (to avoid a mysterious |
| // leading comma). |
| // Otherwise, if the message was empty and there was no extra structured |
| // data, assume that the sender intended to have it represented as an empty |
| // line. |
| if len(parts) > 1 && parts[0] == "" { |
| parts[0] = "<empty>" |
| } |
| |
| // Finally build the message line to emit in leveled logging and emit it. |
| out.Message = strings.Join(parts, ", ") |
| |
| write(&out) |
| } |