okit/client.go

193 lines
4.6 KiB
Go

// Copyright (C) 2022 Mya Pitzeruse
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero 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 Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package okit
import (
"context"
"runtime"
"strings"
"time"
"go.pitz.tech/okit/pb"
)
// Client provides a default implementation.
type Client struct {
now func() time.Time
uuid func() string
tags []Tag
}
func (o Client) withCaller() Client {
pkg, fn, line := infer(caller(3))
o.tags = append(o.tags,
String("package", pkg),
String("func", fn),
Int("line", line),
)
return o
}
// With adds additional tags to the observer instance and returns a new copy for reference.
func (o Client) With(tags ...Tag) Client {
o.tags = append(o.tags, tags...)
return o
}
// Emit allows for the emission of an event which has now value and only associated tags.
func (o Client) Emit(event string, tags ...Tag) {
o.withCaller().With(tags...).emit(&pb.Entry{
Kind: pb.Kind_Event,
Name: event,
})
}
// Observe reports a metric and it's associated value.
func (o Client) Observe(metric string, value float64, tags ...Tag) {
o.withCaller().With(tags...).emit(&pb.Entry{
Value: &pb.Entry_Double{Double: value},
Kind: pb.Kind_Metric,
Name: metric,
})
}
// Trace allows a method to be traced, and it's execution time recorded and reported for viewing.
func (o Client) Trace(ctx context.Context, tags ...Tag) (context.Context, DoneFunc) {
pkg, fn, _ := infer(caller(2))
name := pkg + "." + fn
start := o.now()
span := Span{
TraceID: o.uuid(),
ID: o.uuid(),
}
v := ctx.Value(spanKey)
if v != nil {
parent, hasParent := v.(Span)
if hasParent {
span.TraceID = parent.TraceID
span.ParentID = parent.ID
}
}
return context.WithValue(ctx, spanKey, span), func() {
duration := o.now().Sub(start)
tags = append(
[]Tag{
String("trace-id", span.TraceID),
String("trace-span-id", span.ID),
String("trace-parent-id", span.ParentID),
},
tags...,
)
o.withCaller().With(tags...).emit(&pb.Entry{
Kind: pb.Kind_Trace,
Name: name,
Value: &pb.Entry_Duration{Duration: pb.DurationPB(duration)},
})
}
}
// Debug produces a debug log event.
func (o Client) Debug(msg string, tags ...Tag) {
// todo: is debug enabled?
o.withCaller().With(tags...).emit(&pb.Entry{
Kind: pb.Kind_Log,
Name: msg,
Value: &pb.Entry_String_{String_: "debug"},
})
}
// Info produces an information log event.
func (o Client) Info(msg string, tags ...Tag) {
// todo: is info enabled?
o.withCaller().With(tags...).emit(&pb.Entry{
Kind: pb.Kind_Log,
Name: msg,
Value: &pb.Entry_String_{String_: "info"},
})
}
// Warn produces a warning that is surfaced to operators.
func (o Client) Warn(msg string, tags ...Tag) {
o.withCaller().With(tags...).emit(&pb.Entry{
Kind: pb.Kind_Log,
Name: msg,
Value: &pb.Entry_String_{String_: "warn"},
})
}
// Error produces a message that communicates an error has occurred.
func (o Client) Error(msg string, tags ...Tag) {
o.withCaller().With(tags...).emit(&pb.Entry{
Kind: pb.Kind_Log,
Name: msg,
Value: &pb.Entry_String_{String_: "error"},
})
}
func (o Client) emit(entry *pb.Entry) {
for _, tag := range o.tags {
if tagpb := tag.AsTagPB(); tagpb != nil {
entry.Tags = append(entry.Tags, tagpb)
}
}
// todo: send to sinks
}
// max returns the largest number in a series.
func max(series ...int) int {
largest := 0
for _, v := range series {
if v > largest {
largest = v
}
}
return largest
}
// caller attempts to get method caller information. This information is used for tracing information across an
// applications source code.
func caller(skip int) (name string, line int) {
pctr, _, line, ok := runtime.Caller(skip)
if !ok {
return "", 0
}
return runtime.FuncForPC(pctr).Name(), line
}
// infer attempts to determine the calling package and function using runtime information.
func infer(name string, line int) (pkg, fn string, l int) {
lastSlash := max(0, strings.LastIndexByte(name, '/'))
lastDot := lastSlash + strings.LastIndexByte(name[lastSlash:], '.')
return name[:lastDot], name[lastDot+1:], line
}