// 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 . // 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 }