DEV Community

Douglas Makey Mendez Molero
Douglas Makey Mendez Molero

Posted on • Updated on

Golang: capturing logs and send an email

In my company we have an ETL wrote in Golang to process the integrations with our partners, each integration is executed in an unique and isolate POD using cronjob k8s, each one print a bunch of data and metrics for each step executed using log the package in the standard library, all these logs are useful to monitor the integrations with different tools.

In my team now we want to receive an email when some integration is failed with the logs of the process, so for that, we use a feature of log to change the output destination for the standard logger called SetOutput.

Using io.MultiWriter we can create a writer combine multiple writers, in this case, we will combine a buffer with the standard os.Stderr to save the logs into the buffer and keep logs in stderr for monitorization.

The package log use os.Stderr for default.

        buf := new(bytes.Buffer)
        w := io.MultiWriter(buf, os.Stderr)
        log.SetOutput(w)
Enter fullscreen mode Exit fullscreen mode

Then, all our logs are into the buffer, so we can create a file to save it in a temporal file and attach it to an email.

    f, err := os.Create("/path/log.txt")
    if err != nil {
        // Handler ....
    }

    defer f.Close()

    // We copy the buffer into the file.
    if _, err := io.Copy(f, buf); err != nil {
        // Handler ....
    }
Enter fullscreen mode Exit fullscreen mode

Now we can send an email with the logs file, I found some implementations to send an email with attachment file using only the standard libraries but they didn't work, so I combine different approach and got this implementation.

package main

import (
    "bytes"
    "encoding/base64"
    "fmt"
    "io/ioutil"
    "mime/multipart"
    "net/smtp"
    "os"
    "path/filepath"
)

var (
    host       = os.Getenv("EMAIL_HOST")
    username   = os.Getenv("EMAiL_USERNAME")
    password   = os.Getenv("EMAIL_PASSWORD")
    portNumber = os.Getenv("EMAIL_PORT")
)

type Sender struct {
    auth smtp.Auth
}

type Message struct {
    To          []string
    Subject     string
    Body        string
    Attachments map[string][]byte
}

func New() *Sender {
    auth := smtp.PlainAuth("", username, password, host)
    return &Sender{auth}, nil
}

func (s *Sender) Send(m *Message) error {
    return smtp.SendMail(fmt.Sprintf("%s:%s", host, portNumber), s.auth, username, m.To, m.ToBytes())
}

func NewMessage(s, b string) *Message {
    return &Message{Subject: s, Body: b, Attachments: make(map[string][]byte)}
}

func (m *Message) AttachFile(src string) error {
    b, err := ioutil.ReadFile(src)
    if err != nil {
        return err
    }

    _, fileName := filepath.Split(src)
    m.Attachments[fileName] = b
    return nil
}

func (m *Message) ToBytes() []byte {
    buf := bytes.NewBuffer(nil)
    withAttachments := len(m.Attachments) > 0

    buf.WriteString(fmt.Sprintf("Subject: %s\n", m.Subject))
    buf.WriteString("MIME-Version: 1.0\n")
    writer := multipart.NewWriter(buf)
    boundary := writer.Boundary()

    if withAttachments {
        buf.WriteString(fmt.Sprintf("Content-Type: multipart/mixed; boundary=%s\n", boundary))
        buf.WriteString(fmt.Sprintf("--%s\n", boundary))
    }

    buf.WriteString("Content-Type: text/plain; charset=utf-8\n")
    buf.WriteString(m.Body)

    if withAttachments {
        for k, v := range m.Attachments {
            buf.WriteString(fmt.Sprintf("\n\n--%s\n", boundary))
            buf.WriteString("Content-Type: application/octet-stream\n")
            buf.WriteString("Content-Transfer-Encoding: base64\n")
            buf.WriteString(fmt.Sprintf("Content-Disposition: attachment; filename=%s\n", k))

            b := make([]byte, base64.StdEncoding.EncodedLen(len(v)))
            base64.StdEncoding.Encode(b, v)
            buf.Write(b)
            buf.WriteString(fmt.Sprintf("\n--%s", boundary))
        }

        buf.WriteString("--")
    }

    return buf.Bytes()
}

func main() {
    sender := New()
    m := NewMessage("Test", "Body message.")
    m.To = []string{"to@gmail.com"}
    m.AttachFile("/path/to/file")
    fmt.Println(s.Send(m))
}
Enter fullscreen mode Exit fullscreen mode

If you have some tips to improve this implementation or one way to do better that would be amazing ...

Github.

Discussion (0)