diff --git a/README.md b/README.md index ff3a899..2b4f410 100644 --- a/README.md +++ b/README.md @@ -26,10 +26,16 @@ them. Developers often have to choose between logging a message, emitting a metr - Medium/High cardinality data (user id / signature, other metadata fields). - Typically available in real-time to assess user experience / feature performance. +In addition to the complexity that each of these solutions bring with them, you often need to import a custom library +for each, and thus increase your dependency footprint. + ## Usage ### Basic +Basic usage for `okit` is fairly straight forward. You can create dedicated client, use the default, or even replace the +default. The example below demonstrates how to use the default client. + ```go package main @@ -78,9 +84,14 @@ func main() { // Instrument HTTP Clients okithttp.InstrumentClient(http.DefaultClient) - // Add reporting endpoints mux := http.NewServeMux() - okithttp.RouteEndpoint(mux, okithttp.NewEndpoint()) + + { // Add reporting endpoints + endpoint := okithttp.NewEndpoint() + mux.HandleFunc("/health", endpoint.Health) + mux.HandleFunc("/metrics", endpoint.Metrics) + mux.HandleFunc("/trace", endpoint.Trace) + } // Instrument HTTP Handlers handler := okithttp.InstrumentHandler(mux) @@ -91,3 +102,29 @@ func main() { } ``` +## Implementation + +### Wire Protocol + +TBD + +### Tracing + +Because `okit` aims at providing an all-in-one solution, tracing not only produces spec compliant traces for ingestion +into remote systems but also produces bookend log events for developers and operators. + +### Logging + +Logging follows a fairly standard implementation. It allows messages to be logged at different levels including `debug`, +`info`, `warn`, and `error`. A fifth, `trace` level is also available that allows tracing bookend events to be enabled +disabled. Logging output can be written as text or as JSON. + +### Metrics + +Metrics are implemented using an observation based approach. Results are recorded and stored locally for administrators +to be able to query. + +### Events + +Multi-dimensional events are a lot like metrics. The most common use case is when metrics have a statically coded value +of 1. For example, page views, checkout, and many other user-driven actions. diff --git a/api.go b/api.go index 1668211..fd911f1 100644 --- a/api.go +++ b/api.go @@ -22,6 +22,7 @@ package okit import ( "context" + "strings" ) // Logger defines common operations for writing log messages. @@ -68,6 +69,7 @@ type Span struct { ParentID string } +// Wither allows tags to be appended to any implementing client. type Wither[T any] interface { With(tags ...Tag) T } @@ -80,3 +82,49 @@ type Interface[T any] interface { Tracer Wither[T] } + +type Level int8 + +const ( + UnknownLevel = 0 + TraceLevel = 1 + DebugLevel = 2 + InfoLevel = 3 + WarnLevel = 4 + ErrorLevel = 5 +) + +func (l *Level) String() string { + switch *l { + case TraceLevel: + return "trace" + case DebugLevel: + return "debug" + case InfoLevel: + return "info" + case WarnLevel: + return "warn" + case ErrorLevel: + return "error" + } + + return "unknown" +} + +func (l *Level) Set(val string) error { + switch { + case strings.EqualFold("trace", val): + *l = TraceLevel + case strings.EqualFold("debug", val): + *l = DebugLevel + case strings.EqualFold("info", val): + *l = InfoLevel + case strings.EqualFold("warn", val): + *l = WarnLevel + case strings.EqualFold("error", val): + *l = ErrorLevel + } + + *l = UnknownLevel + return nil +} diff --git a/api_impl.go b/api_impl.go index 95af679..d88cdb8 100644 --- a/api_impl.go +++ b/api_impl.go @@ -24,16 +24,16 @@ import ( "bufio" "context" "os" - - "github.com/golang/protobuf/jsonpb" ) // DefaultClient provides a default client implementation. var DefaultClient = NewClient() +// DefaultFormat is used to format how data is written to stdout. +var DefaultFormat Format = TextFormat{} + // TODO: make this configurable var sink = bufio.NewWriter(os.Stdout) -var marshaler = &jsonpb.Marshaler{} // ContextKey is a holder structure that allows data to be attached to contexts. type ContextKey string diff --git a/client.go b/client.go index a678367..8d22229 100644 --- a/client.go +++ b/client.go @@ -58,21 +58,26 @@ type Client struct { callerSkip int } +// WithNow configures the function that's used to obtain the current timestamp. func (o Client) WithNow(now func() time.Time) Client { o.now = now return o } +// WithUUID returns a new uuid that uniquely identifies traces, spans, and tags. func (o Client) WithUUID(uuid func() string) Client { o.uuid = uuid return o } +// WithCallerSkip is used to configure the number of frames to skip when determining the caller. Callers are +// predominantly used when performing traces. func (o Client) WithCallerSkip(callerSkip int) Client { o.callerSkip = callerSkip return o } +// With appends tags to the current client that will automatically be added to all events, metrics, logs, and traces. func (o Client) With(tags ...Tag) Client { o.tags = append(o.tags, tags...) return o @@ -81,8 +86,8 @@ func (o Client) With(tags ...Tag) Client { // 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.With(tags...).emit(&pb.Entry{ - Kind: pb.Kind_Event, - Name: event, + Kind: pb.Kind_Event, + Scope: event, }) } @@ -91,7 +96,7 @@ func (o Client) Observe(metric string, value float64, tags ...Tag) { o.With(tags...).emit(&pb.Entry{ Value: &pb.Entry_Double{Double: value}, Kind: pb.Kind_Metric, - Name: metric, + Scope: metric, }) } @@ -114,34 +119,40 @@ func (o Client) Trace(ctx context.Context, tags ...Tag) (context.Context, DoneFu } } - o.With(tags...).With(String("bookend", "start")).emit(&pb.Entry{ - Kind: pb.Kind_Event, - Name: name, + //goland:noinspection GoAssignmentToReceiver + o = o.With( + append( + []Tag{ + String("traceId", span.TraceID), + String("traceSpanId", span.ID), + String("traceParentId", span.ParentID), + }, + tags..., + )..., + ) + + // bookend + o.With(String("bookend", "start")).emit(&pb.Entry{ + Kind: pb.Kind_Log, + Scope: name, Value: &pb.Entry_String_{String_: "trace"}, }) return context.WithValue(ctx, SpanKey, span), func() { duration := o.now().Sub(start) - o.With(tags...).With(String("bookend", "complete")).emit(&pb.Entry{ - Kind: pb.Kind_Event, - Name: name, + // bookend + o.With(String("bookend", "end")).emit(&pb.Entry{ + Kind: pb.Kind_Log, + Scope: name, Value: &pb.Entry_String_{String_: "trace"}, }) - tags = append( - []Tag{ - String("trace-id", span.TraceID), - String("trace-span-id", span.ID), - String("trace-parent-id", span.ParentID), - }, - tags..., - ) - - o.With(tags...).emit( + // trace + o.emit( &pb.Entry{ Kind: pb.Kind_Trace, - Name: name, + Scope: name, Value: &pb.Entry_Duration{Duration: pb.DurationPB(duration)}, }, ) @@ -152,7 +163,7 @@ func (o Client) Trace(ctx context.Context, tags ...Tag) (context.Context, DoneFu func (o Client) Debug(msg string, tags ...Tag) { o.With(tags...).emit(&pb.Entry{ Kind: pb.Kind_Log, - Name: msg, + Scope: msg, Value: &pb.Entry_String_{String_: "debug"}, }) } @@ -161,7 +172,7 @@ func (o Client) Debug(msg string, tags ...Tag) { func (o Client) Info(msg string, tags ...Tag) { o.With(tags...).emit(&pb.Entry{ Kind: pb.Kind_Log, - Name: msg, + Scope: msg, Value: &pb.Entry_String_{String_: "info"}, }) } @@ -170,7 +181,7 @@ func (o Client) Info(msg string, tags ...Tag) { func (o Client) Warn(msg string, tags ...Tag) { o.With(tags...).emit(&pb.Entry{ Kind: pb.Kind_Log, - Name: msg, + Scope: msg, Value: &pb.Entry_String_{String_: "warn"}, }) } @@ -179,7 +190,7 @@ func (o Client) Warn(msg string, tags ...Tag) { func (o Client) Error(msg string, tags ...Tag) { o.With(tags...).emit(&pb.Entry{ Kind: pb.Kind_Log, - Name: msg, + Scope: msg, Value: &pb.Entry_String_{String_: "error"}, }) } @@ -187,37 +198,21 @@ func (o Client) Error(msg string, tags ...Tag) { func (o Client) emit(entries ...*pb.Entry) { now := pb.TimestampPB(o.now()) + tags := make([]*pb.Tag, 0, len(o.tags)) + for _, tag := range o.tags { + tags = append(tags, tag.AsTagPB()) + } + for _, entry := range entries { - // todo: check if log level is enabled - entry.Timestamp = now + entry.Tags = tags - for _, tag := range o.tags { - if tagpb := tag.AsTagPB(); tagpb != nil { - entry.Tags = append(entry.Tags, tagpb) - } - } - - _ = marshaler.Marshal(sink, entry) - _ = sink.WriteByte('\n') + _ = DefaultFormat.Marshal(sink, entry) } _ = sink.Flush() } -// 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) { diff --git a/examples/json/main.go b/examples/json/main.go new file mode 100644 index 0000000..420279e --- /dev/null +++ b/examples/json/main.go @@ -0,0 +1,52 @@ +// Copyright (C) 2022 The OKit Authors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE +// OR OTHER DEALINGS IN THE SOFTWARE. + +package main + +import ( + "net/http" + + "go.pitz.tech/okit" + okithttp "go.pitz.tech/okit/http" +) + +func main() { + // Enable JSON output + okit.DefaultFormat = okit.JSONFormat{} + + // Instrument HTTP Clients + okithttp.InstrumentClient(http.DefaultClient) + + mux := http.NewServeMux() + + { // Add reporting endpoints + endpoint := okithttp.NewEndpoint() + mux.HandleFunc("/health", endpoint.Health) + mux.HandleFunc("/metrics", endpoint.Metrics) + mux.HandleFunc("/trace", endpoint.Trace) + } + + // Instrument HTTP Handlers + handler := okithttp.InstrumentHandler(mux) + err := http.ListenAndServe("0.0.0.0:8080", handler) + if err != nil { + panic(err) + } +} diff --git a/format.go b/format.go new file mode 100644 index 0000000..97bb934 --- /dev/null +++ b/format.go @@ -0,0 +1,61 @@ +package okit + +import ( + "bufio" + "strconv" + "strings" + "time" + + "github.com/golang/protobuf/jsonpb" + "go.pitz.tech/okit/pb" +) + +// Format defines an abstraction for writing entries to stdout/stderr. This is predominantly used in report log, trace, +// and event information from the process. +type Format interface { + Marshal(writer *bufio.Writer, entry *pb.Entry) error +} + +// JSONFormat writes entries using JSON encoded protocol buffers. +type JSONFormat struct { + Marshaler jsonpb.Marshaler +} + +func (f JSONFormat) Marshal(writer *bufio.Writer, entry *pb.Entry) error { + f.Marshaler.Marshal(writer, entry) + return writer.WriteByte('\n') +} + +// TextFormat writes entries using a custom text format. +type TextFormat struct{} + +func (f TextFormat) Marshal(writer *bufio.Writer, entry *pb.Entry) error { + sink.WriteString(entry.Timestamp.AsTime().Format(time.RFC3339)) + sink.WriteByte('\t') + sink.WriteString(strings.ToUpper(entry.Kind.String())) + sink.WriteByte('\t') + + switch v := entry.GetValue().(type) { + case *pb.Entry_String_: + sink.WriteString(v.String_) + case *pb.Entry_Double: + sink.WriteString(strconv.FormatFloat(v.Double, 'f', 5, 64)) + case *pb.Entry_Duration: + sink.WriteString(v.Duration.AsDuration().String()) + } + sink.WriteByte('\t') + sink.WriteString(entry.Scope) + sink.WriteByte('\t') + + for _, tag := range entry.Tags { + qs := tag.AsQueryString() + if qs == "" { + continue + } + + sink.WriteString(qs) + sink.WriteByte('&') + } + + return sink.WriteByte('\n') +} diff --git a/pb/obkit.pb.go b/pb/obkit.pb.go index a20efef..c7b28a0 100644 --- a/pb/obkit.pb.go +++ b/pb/obkit.pb.go @@ -379,12 +379,12 @@ type Entry struct { // Timestamp defines when the entry was emit. This system assumes the client clock is accurate enough. Timestamp *Timestamp `protobuf:"bytes,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + // Scope is used to identify the metric name, log message, or traced function. + Scope string `protobuf:"bytes,2,opt,name=scope,proto3" json:"scope,omitempty"` // Kind is used to indicate what type of entry this is. Kind Kind `protobuf:"varint,3,opt,name=kind,proto3,enum=obkit.Kind" json:"kind,omitempty"` - // Name is used to identify the metric name, log message, or traced function. - Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"` // Tags is an optional field that contains a list of metadata associated with the entry. - Tags []*Tag `protobuf:"bytes,5,rep,name=tags,proto3" json:"tags,omitempty"` + Tags []*Tag `protobuf:"bytes,4,rep,name=tags,proto3" json:"tags,omitempty"` // Value is an optional field that defines a value associated with the entry (e.g. in the case of a log, metric, or trace). // // Types that are assignable to Value: @@ -433,6 +433,13 @@ func (x *Entry) GetTimestamp() *Timestamp { return nil } +func (x *Entry) GetScope() string { + if x != nil { + return x.Scope + } + return "" +} + func (x *Entry) GetKind() Kind { if x != nil { return x.Kind @@ -440,13 +447,6 @@ func (x *Entry) GetKind() Kind { return Kind_Unknown } -func (x *Entry) GetName() string { - if x != nil { - return x.Name - } - return "" -} - func (x *Entry) GetTags() []*Tag { if x != nil { return x.Tags @@ -487,15 +487,15 @@ type isEntry_Value interface { } type Entry_String_ struct { - String_ string `protobuf:"bytes,6,opt,name=string,proto3,oneof"` // used for logs + String_ string `protobuf:"bytes,5,opt,name=string,proto3,oneof"` // used for logs } type Entry_Double struct { - Double float64 `protobuf:"fixed64,7,opt,name=double,proto3,oneof"` // used for metrics + Double float64 `protobuf:"fixed64,6,opt,name=double,proto3,oneof"` // used for metrics } type Entry_Duration struct { - Duration *Duration `protobuf:"bytes,8,opt,name=duration,proto3,oneof"` // used for traces + Duration *Duration `protobuf:"bytes,7,opt,name=duration,proto3,oneof"` // used for traces } func (*Entry_String_) isEntry_Value() {} @@ -647,38 +647,38 @@ var file_obkit_proto_rawDesc = []byte{ 0x61, 0x6d, 0x70, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x6f, 0x62, 0x6b, 0x69, 0x74, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x48, 0x00, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x42, 0x07, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x22, 0xf8, 0x01, 0x0a, 0x05, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x2e, 0x0a, 0x09, 0x74, + 0x65, 0x22, 0xfa, 0x01, 0x0a, 0x05, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x2e, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x6f, 0x62, 0x6b, 0x69, 0x74, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, - 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x1f, 0x0a, 0x04, 0x6b, - 0x69, 0x6e, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0b, 0x2e, 0x6f, 0x62, 0x6b, 0x69, - 0x74, 0x2e, 0x4b, 0x69, 0x6e, 0x64, 0x52, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x12, 0x12, 0x0a, 0x04, - 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, - 0x12, 0x1e, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0a, - 0x2e, 0x6f, 0x62, 0x6b, 0x69, 0x74, 0x2e, 0x54, 0x61, 0x67, 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, - 0x12, 0x18, 0x0a, 0x06, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, - 0x48, 0x00, 0x52, 0x06, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x12, 0x18, 0x0a, 0x06, 0x64, 0x6f, - 0x75, 0x62, 0x6c, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x01, 0x48, 0x00, 0x52, 0x06, 0x64, 0x6f, - 0x75, 0x62, 0x6c, 0x65, 0x12, 0x2d, 0x0a, 0x08, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x6f, 0x62, 0x6b, 0x69, 0x74, 0x2e, 0x44, - 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x08, 0x64, 0x75, 0x72, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x42, 0x07, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x3c, 0x0a, 0x0a, - 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, - 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, - 0x0a, 0x08, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x08, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x22, 0x5b, 0x0a, 0x06, 0x50, 0x61, - 0x63, 0x6b, 0x65, 0x74, 0x12, 0x29, 0x0a, 0x06, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6f, 0x62, 0x6b, 0x69, 0x74, 0x2e, 0x43, 0x6c, 0x69, - 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x06, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x12, - 0x26, 0x0a, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x0c, 0x2e, 0x6f, 0x62, 0x6b, 0x69, 0x74, 0x2e, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, - 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x2a, 0x3e, 0x0a, 0x04, 0x4b, 0x69, 0x6e, 0x64, 0x12, - 0x0b, 0x0a, 0x07, 0x55, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, - 0x45, 0x76, 0x65, 0x6e, 0x74, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, - 0x63, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x72, 0x61, 0x63, 0x65, 0x10, 0x03, 0x12, 0x07, - 0x0a, 0x03, 0x4c, 0x6f, 0x67, 0x10, 0x04, 0x42, 0x1c, 0x5a, 0x1a, 0x63, 0x6f, 0x64, 0x65, 0x2e, - 0x70, 0x69, 0x74, 0x7a, 0x2e, 0x74, 0x65, 0x63, 0x68, 0x2f, 0x6d, 0x79, 0x61, 0x2f, 0x6f, 0x6b, - 0x69, 0x74, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x14, 0x0a, 0x05, 0x73, + 0x63, 0x6f, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x63, 0x6f, 0x70, + 0x65, 0x12, 0x1f, 0x0a, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, + 0x0b, 0x2e, 0x6f, 0x62, 0x6b, 0x69, 0x74, 0x2e, 0x4b, 0x69, 0x6e, 0x64, 0x52, 0x04, 0x6b, 0x69, + 0x6e, 0x64, 0x12, 0x1e, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x0a, 0x2e, 0x6f, 0x62, 0x6b, 0x69, 0x74, 0x2e, 0x54, 0x61, 0x67, 0x52, 0x04, 0x74, 0x61, + 0x67, 0x73, 0x12, 0x18, 0x0a, 0x06, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x09, 0x48, 0x00, 0x52, 0x06, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x12, 0x18, 0x0a, 0x06, + 0x64, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x01, 0x48, 0x00, 0x52, 0x06, + 0x64, 0x6f, 0x75, 0x62, 0x6c, 0x65, 0x12, 0x2d, 0x0a, 0x08, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x6f, 0x62, 0x6b, 0x69, 0x74, + 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x08, 0x64, 0x75, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x07, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x3c, + 0x0a, 0x0a, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x12, 0x0a, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x12, 0x1a, 0x0a, 0x08, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x22, 0x5b, 0x0a, 0x06, + 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x29, 0x0a, 0x06, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6f, 0x62, 0x6b, 0x69, 0x74, 0x2e, 0x43, + 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x06, 0x63, 0x6c, 0x69, 0x65, 0x6e, + 0x74, 0x12, 0x26, 0x0a, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x6f, 0x62, 0x6b, 0x69, 0x74, 0x2e, 0x45, 0x6e, 0x74, 0x72, 0x79, + 0x52, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x2a, 0x3e, 0x0a, 0x04, 0x4b, 0x69, 0x6e, + 0x64, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x10, 0x00, 0x12, 0x09, + 0x0a, 0x05, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x4d, 0x65, 0x74, + 0x72, 0x69, 0x63, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x72, 0x61, 0x63, 0x65, 0x10, 0x03, + 0x12, 0x07, 0x0a, 0x03, 0x4c, 0x6f, 0x67, 0x10, 0x04, 0x42, 0x1c, 0x5a, 0x1a, 0x63, 0x6f, 0x64, + 0x65, 0x2e, 0x70, 0x69, 0x74, 0x7a, 0x2e, 0x74, 0x65, 0x63, 0x68, 0x2f, 0x6d, 0x79, 0x61, 0x2f, + 0x6f, 0x6b, 0x69, 0x74, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/pb/obkit.pbext.go b/pb/obkit.pbext.go index fcc2d7d..8803d72 100644 --- a/pb/obkit.pbext.go +++ b/pb/obkit.pbext.go @@ -21,6 +21,9 @@ package pb import ( + "encoding/base64" + "net/url" + "strconv" "time" ) @@ -46,3 +49,31 @@ func TimestampPB(t time.Time) *Timestamp { Nanos: int32(t.Nanosecond()), } } + +// AsQueryString converts the associated tag to a URI query string +func (x *Tag) AsQueryString() string { + val := "" + + switch v := x.GetValue().(type) { + case *Tag_String_: + val = v.String_ + case *Tag_Int64: + val = strconv.FormatInt(v.Int64, 10) + case *Tag_Double: + val = strconv.FormatFloat(v.Double, 'f', 9, 64) + case *Tag_Bytes: + val = base64.RawStdEncoding.EncodeToString(v.Bytes) + case *Tag_Bool: + val = strconv.FormatBool(v.Bool) + case *Tag_Duration: + val = v.Duration.AsDuration().String() + case *Tag_Timestamp: + val = v.Timestamp.AsTime().Format(time.RFC3339) + } + + if val == "" { + return "" + } + + return url.QueryEscape(x.Key) + "=" + url.QueryEscape(val) +} diff --git a/pb/obkit.proto b/pb/obkit.proto index 848a3c0..c146407 100644 --- a/pb/obkit.proto +++ b/pb/obkit.proto @@ -71,20 +71,20 @@ message Entry { // Timestamp defines when the entry was emit. This system assumes the client clock is accurate enough. Timestamp timestamp = 1; + // Scope is used to identify the metric name, log message, or traced function. + string scope = 2; + // Kind is used to indicate what type of entry this is. Kind kind = 3; - // Name is used to identify the metric name, log message, or traced function. - string name = 4; - // Tags is an optional field that contains a list of metadata associated with the entry. - repeated Tag tags = 5; + repeated Tag tags = 4; // Value is an optional field that defines a value associated with the entry (e.g. in the case of a log, metric, or trace). oneof value { - string string = 6; // used for logs - double double = 7; // used for metrics - Duration duration = 8; // used for traces + string string = 5; // used for logs + double double = 6; // used for metrics + Duration duration = 7; // used for traces } }