diff --git a/README.md b/README.md index 49f7811..d0be48b 100644 --- a/README.md +++ b/README.md @@ -205,7 +205,13 @@ Here is an example of how one might use it: ```go func yourAction(c *cli.Context) error { - obsvr, err := newObserver(c) + obsvr, err := observe.NewFromCLI(c, "my-service", &observe.Options{ + LogTimestamps: true, + StatsRuntime: true, + TracingAttrs: []attribute.KeyValue{ + semconv.ServiceVersionKey.String("1.0.0"), + }, + }) if err != nil { return err } @@ -215,29 +221,6 @@ func yourAction(c *cli.Context) error { return nil } - -func newObserver(c *cli.Context) (*observe.Observer, error) { - log, err := cmd.NewLogger(c) - if err != nil { - return nil, err - } - - stats, err := cmd.NewStatter(c, log) - if err != nil { - return nil, err - } - - tracer, err := cmd.NewTracer(c, log, - semconv.ServiceNameKey.String("my-service"), - semconv.ServiceVersionKey.String("1.0.0"), - ) - if err != nil { - return nil, err - } - tracerCancel := func() { _ = tracer.Shutdown(context.Background()) } - - return observe.New(log, stats, tracer, tracerCancel), nil -} ``` It also exposes `NewFake` which allows you to pass fake loggers, tracers and statters in your tests easily. diff --git a/observe/doc.go b/observe/doc.go index 8dd7b50..b6597e8 100644 --- a/observe/doc.go +++ b/observe/doc.go @@ -14,6 +14,15 @@ Example usage: return nil, err } + prof, err := cmd.NewProfiler(c, "my-service", log) + if err != nil { + return nil, err + } + profStop := func() {} + if prof != nil { + profStop = func() { _ = prof.Stop() } + } + tracer, err := cmd.NewTracer(c, log, semconv.ServiceNameKey.String("my-service"), semconv.ServiceVersionKey.String("1.0.0"), @@ -23,7 +32,7 @@ Example usage: } tracerCancel := func() { _ = tracer.Shutdown(context.Background()) } - return observe.New(log, stats, tracer, tracerCancel), nil + return observe.New(log, stats, tracer, tracerCancel, profStop), nil } */ package observe diff --git a/observe/example_test.go b/observe/example_test.go index fe4c37d..9979697 100644 --- a/observe/example_test.go +++ b/observe/example_test.go @@ -6,6 +6,7 @@ import ( "github.com/hamba/cmd/v2" "github.com/hamba/cmd/v2/observe" "github.com/urfave/cli/v2" + "go.opentelemetry.io/otel/attribute" semconv "go.opentelemetry.io/otel/semconv/v1.17.0" ) @@ -24,6 +25,15 @@ func ExampleNew() { return } + prof, err := cmd.NewProfiler(c, "my-service", log) + if err != nil { + return + } + profStop := func() {} + if prof != nil { + profStop = func() { _ = prof.Stop() } + } + tracer, err := cmd.NewTracer(c, log, semconv.ServiceNameKey.String("my-service"), semconv.ServiceVersionKey.String("1.0.0"), @@ -34,7 +44,25 @@ func ExampleNew() { } tracerCancel := func() { _ = tracer.Shutdown(context.Background()) } - obsrv := observe.New(log, stats, tracer, tracerCancel) + obsrv := observe.New(log, stats, tracer, tracerCancel, profStop) + + _ = obsrv +} + +func ExampleNewFromCLI() { + var c *cli.Context // Get this from your action. + + obsrv, err := observe.NewFromCLI(c, "my-service", &observe.Options{ + LogTimestamps: true, + StatsRuntime: true, + TracingAttrs: []attribute.KeyValue{ + semconv.ServiceVersionKey.String("1.0.0"), + }, + }) + if err != nil { + // Handle error. + return + } _ = obsrv } diff --git a/observe/observer.go b/observe/observer.go index 155d34c..88b23f6 100644 --- a/observe/observer.go +++ b/observe/observer.go @@ -1,15 +1,36 @@ package observe import ( + "context" "io" "time" + otelpyroscope "github.com/grafana/otel-profiling-go" + "github.com/hamba/cmd/v2" "github.com/hamba/logger/v2" + lctx "github.com/hamba/logger/v2/ctx" "github.com/hamba/statter/v2" + "github.com/hamba/statter/v2/runtime" + "github.com/hamba/statter/v2/tags" + "github.com/urfave/cli/v2" "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + semconv "go.opentelemetry.io/otel/semconv/v1.17.0" "go.opentelemetry.io/otel/trace" ) +// Options optionally configures an observer. +type Options struct { + LogTimeFormat string + LogTimestamps bool + LogCtx []logger.Field + + StatsRuntime bool + StatsTags []statter.Tag + + TracingAttrs []attribute.KeyValue +} + // Observer contains observability primitives. type Observer struct { Log *logger.Logger @@ -29,6 +50,81 @@ func New(log *logger.Logger, stats *statter.Statter, traceProv trace.TracerProvi } } +// NewFromCLI returns an observer with the given observability primitives. +// +//nolint:cyclop // Splitting this will not make it simpler. +func NewFromCLI(cliCtx *cli.Context, svc string, opts *Options) (*Observer, error) { + var closeFns []func() + + if opts == nil { + opts = &Options{} + } + + // Logger. + log, err := cmd.NewLogger(cliCtx) + if err != nil { + return nil, err + } + if opts.LogTimeFormat != "" { + logger.TimeFormat = opts.LogTimeFormat + } + if opts.LogTimestamps { + closeFns = append(closeFns, log.WithTimestamp()) + } + opts.LogCtx = append([]logger.Field{lctx.Str("svc", svc)}, opts.LogCtx...) + log = log.With(opts.LogCtx...) + + // Statter. + stats, err := cmd.NewStatter(cliCtx, log) + if err != nil { + for _, fn := range closeFns { + fn() + } + return nil, err + } + closeFns = append(closeFns, func() { _ = stats.Close() }) + if opts.StatsRuntime { + go runtime.Collect(stats) + } + opts.StatsTags = append([]statter.Tag{tags.Str("svc", svc)}, opts.StatsTags...) + stats = stats.With("", opts.StatsTags...) + + // Profiler. + prof, err := cmd.NewProfiler(cliCtx, svc, log) + if err != nil { + for _, fn := range closeFns { + fn() + } + return nil, err + } + if prof != nil { + closeFns = append(closeFns, func() { _ = prof.Stop() }) + } + + // Tracer. + opts.TracingAttrs = append(opts.TracingAttrs, semconv.ServiceNameKey.String(svc)) + tracer, err := cmd.NewTracer(cliCtx, log, opts.TracingAttrs...) + if err != nil { + for _, fn := range closeFns { + fn() + } + return nil, err + } + closeFns = append(closeFns, func() { _ = tracer.Shutdown(context.Background()) }) + + var tp trace.TracerProvider = tracer + if prof != nil && tracer != nil { + tp = otelpyroscope.NewTracerProvider(tp) + } + + return &Observer{ + Log: log, + Stats: stats, + TraceProv: tp, + closeFns: closeFns, + }, nil +} + // Tracer returns a tracer with the given name and options. // If no trace provider has been set, this function will panic. func (o *Observer) Tracer(name string, opts ...trace.TracerOption) trace.Tracer {