git » gofer » main » tree

[main] / reqlog / reqlog.go

package reqlog

import (
	"context"
	"fmt"
	"net"
	"net/http"
	"os"
	"strings"
	"text/template"
	"time"

	"blitiri.com.ar/go/gofer/config"
	"blitiri.com.ar/go/gofer/trace"
	"blitiri.com.ar/go/log"
)

type Log struct {
	path   string
	f      *os.File
	evs    chan *Event
	reopen chan bool
	tmpl   *template.Template

	tr *trace.Trace
}

type Event struct {
	T time.Time
	H *http.Request
	R *RawRequest

	Status int
	Length int64

	Latency time.Duration
}

type RawRequest struct {
	RemoteAddr net.Addr
	LocalAddr  net.Addr
}

// Common log format, used by many servers.
// https://en.wikipedia.org/wiki/Common_Log_Format
// https://httpd.apache.org/docs/2.4/logs.html#common
const commonFormat = "{{.H.RemoteAddr}} - - [{{.T.Format \"02/Jan/2006:15:04:05 -0700\"}}] \"{{.H.Method}} {{.H.URL}} {{.H.Proto}}\" {{.Status}} {{.Length}}\n"

// Combined log format, extension of the Common Log Format, and used by a lot
// of servers (e.g. Apache).
// https://httpd.apache.org/docs/2.4/logs.html#combined
const combinedFormat = "{{.H.RemoteAddr}} - - [{{.T.Format \"02/Jan/2006:15:04:05 -0700\"}}] \"{{.H.Method}} {{.H.URL}} {{.H.Proto}}\" {{.Status}} {{.Length}} {{.H.Header.Referer|q}} {{index .H.Header \"User-Agent\"|q}}\n"

// Extension of the combined log format, prepending the virtual host.
// https://httpd.apache.org/docs/2.4/logs.html#virtualhost
const combinedVHFormat = "{{.H.Host}} " + combinedFormat

// lighttpd log is like combined, but the virtual host is put instead of the
// ident field.
const lighttpdFormat = "{{.H.RemoteAddr}} {{.H.Host}} - [{{.T.Format \"02/Jan/2006:15:04:05 -0700\"}}] \"{{.H.Method}} {{.H.URL}} {{.H.Proto}}\" {{.Status}} {{.Length}}\n"

// gofer format, this is the default, and can handle both raw and HTTP events.
const goferFormat = "{{.T.Format \"2006-01-02 15:04:05.000\"}}" +
	"{{if .H}} {{.H.RemoteAddr}} {{.H.Proto}} {{.H.Host}} {{.H.Method}}" +
	" {{.H.URL}} {{.H.Header.Referer|q}} {{index .H.Header \"User-Agent\"|q}}{{end}}" +
	"{{if .R}} {{.R.RemoteAddr}} raw {{.R.LocalAddr}}{{end}}" +
	" = {{.Status}} {{.Length}}b {{.Latency.Milliseconds}}ms\n"

var knownFormats = map[string]string{
	"<common>":     commonFormat,
	"<combined>":   combinedFormat,
	"<combinedvh>": combinedVHFormat,
	"<lighttpd>":   lighttpdFormat,
	"<gofer>":      goferFormat,
	"":             goferFormat,
}

func New(path string, nbuf int, format string) (*Log, error) {
	var err error
	h := &Log{}

	if f, ok := knownFormats[format]; ok {
		format = f
	}
	h.tmpl = template.New(path)
	h.tmpl.Funcs(template.FuncMap{
		"q": quoteString,
	})
	_, err = h.tmpl.Parse(format)
	if err != nil {
		return nil, err
	}

	switch path {
	case "<stdout>":
		h.f = os.Stdout
	case "<stderr>":
		h.f = os.Stderr
	default:
		h.f, err = os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
		if err != nil {
			return nil, err
		}
		h.path = path
	}

	h.evs = make(chan *Event, nbuf)
	h.reopen = make(chan bool, 1)
	h.tr = trace.New("reqlog", path)
	h.tr.SetMaxEvents(1000)

	go h.run()
	return h, nil
}

func (h *Log) run() {
	var err error
	for {
		select {
		case e := <-h.evs:
			err = h.tmpl.Execute(h.f, e)
			if err != nil {
				h.tr.Errorf("error logging: %v", err)
			}
		case <-h.reopen:
			if h.path != "" {
				h.f.Close()
				h.f, err = os.OpenFile(
					h.path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
				if err != nil {
					h.tr.Errorf("error reopening: %v", err)
				}
			}
		}
	}
}

func (h *Log) Log(e *Event) {
	h.evs <- e
}

func (h *Log) Reopen() {
	h.reopen <- true
}

// Global registry for convenience.
// This is not pretty but it simplifies a lot of the handling for now.
var registry = map[string]*Log{}

func FromConfig(name string, conf config.ReqLog) error {
	h, err := New(conf.File, conf.BufSize, conf.Format)
	if err != nil {
		return fmt.Errorf("reqlog %q failed to initialize: %v", name, err)
	}
	registry[name] = h
	log.Infof("reqlog %q writing to %q", name, conf.File)
	return nil
}

func FromName(name string) *Log {
	return registry[name]
}

func ReopenAll() {
	for _, rl := range registry {
		rl.Reopen()
	}
}

type ctxKeyT string

const ctxKey = ctxKeyT("reqlog")

func NewContext(ctx context.Context, log *Log) context.Context {
	return context.WithValue(ctx, ctxKey, log)
}
func FromContext(ctx context.Context) *Log {
	v := ctx.Value(ctxKey)
	if v == nil {
		return nil
	}
	return v.(*Log)
}

func quoteString(i interface{}) string {
	if i == nil {
		return `""`
	}

	switch v := i.(type) {
	case string:
		return fmt.Sprintf("%q", v)
	case []string:
		return fmt.Sprintf("%q", strings.Join(v, ", "))
	default:
		return fmt.Sprintf("unknown-type-%T", v)
	}
}