/* Maddy Mail Server - Composable all-in-one email server. Copyright © 2019-2020 Max Mazurov , Maddy Mail Server contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ // Package parser provides utilities for parsing of structured log messsages // generated by maddy. package parser import ( "encoding/json" "strings" "time" "unicode" ) type ( Msg struct { Stamp time.Time Debug bool Module string Message string Context map[string]interface{} } MalformedMsg struct { Desc string Err error } ) const ( ISO8601_UTC = "2006-01-02T15:04:05.000Z" ) func (m MalformedMsg) Error() string { if m.Err != nil { return "parse: " + m.Desc + ": " + m.Err.Error() } return "parse: " + m.Desc } // Parse parses the message from the maddy log file. // // It assumes standard file output, including the [debug] tag and // ISO 8601 timestamp at the start of each line. Timestamp is assumed to be in // the UTC, as it is enforced by maddy. // // JSON context values are unmarshalled without any additional processing, // notably that means that all numbers are represented as float64. func Parse(line string) (Msg, error) { parts := strings.Split(line, "\t") if len(parts) != 2 { // All messages even without a Context have a trailing \t, // so this one is obviously malformed. return Msg{}, MalformedMsg{Desc: "missing a tab separator"} } m := Msg{ Context: map[string]interface{}{}, } // After that, the second part is the context. It can be empty, so don't fail // if there is none. if len(parts[1]) != 0 { if err := json.Unmarshal([]byte(parts[1]), &m.Context); err != nil { return Msg{}, MalformedMsg{Desc: "context unmarshal", Err: err} } } // Okay, the first one might contain the timestamp at start. // Cut it away. msgParts := strings.SplitN(parts[0], " ", 2) if len(msgParts) == 1 { return Msg{}, MalformedMsg{Desc: "missing a timestamp"} } var err error m.Stamp, err = time.ParseInLocation(ISO8601_UTC, msgParts[0], time.UTC) if err != nil { return Msg{}, MalformedMsg{Desc: "timestamp parse", Err: err} } msgText := msgParts[1] if strings.HasPrefix(msgText, "[debug] ") { msgText = strings.TrimPrefix(msgText, "[debug] ") m.Debug = true } moduleText := strings.SplitN(msgText, ": ", 2) if len(moduleText) == 1 { // No module prefix, that's fine. m.Message = msgText return m, nil } for _, ch := range moduleText[0] { switch { case unicode.IsDigit(ch), unicode.IsLetter(ch), ch == '/': default: // This is not a module prefix, don't treat it as such. m.Message = msgText return m, nil } } m.Module = moduleText[0] m.Message = moduleText[1] return m, nil }