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)
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 ....
}
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))
}
If you have some tips to improve this implementation or one way to do better that would be amazing ...