Skip to content

Commit

Permalink
Add Logging interface to Autometrics (#80)
Browse files Browse the repository at this point in the history
  • Loading branch information
gagbo committed Nov 16, 2023
1 parent 560c75c commit be7aa75
Show file tree
Hide file tree
Showing 11 changed files with 159 additions and 12 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ specification.
- [Generator] The generator now tries to keep going with instrumentation even if some instrumentation
fails. On error, still _No file will be modified_, and the generator will exit with an error code
and output all the collected errors instead of only the first one.
- [All] `autometrics.Init` function takes an additional argument, a logging interface to decide whether
and how users want autometrics to log events. Passing `nil` for the argument value will use a "No Op"
logger that does not do anything.

### Deprecated

Expand Down
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ And then in your main function initialize the metrics
autometrics.DefBuckets,
autometrics.BuildInfo{Version: "0.4.0", Commit: "anySHA", Branch: "", Service: "myApp"},
nil,
nil,
)
if err != nil {
log.Fatalf("could not initialize autometrics: %s", err)
Expand Down Expand Up @@ -273,6 +274,7 @@ func main() {
autometrics.DefBuckets,
autometrics.BuildInfo{Version: "0.4.0", Commit: "anySHA", Branch: "", Service: "myApp"},
nil,
nil,
)
http.Handle("/metrics", promhttp.Handler())
}
Expand Down Expand Up @@ -384,6 +386,7 @@ metric. You can use the name of the application or its version for example
autometrics.DefBuckets,
autometrics.BuildInfo{ Version: "2.1.37", Commit: "anySHA", Branch: "", Service: "myApp" },
nil,
nil,
)
```

Expand Down Expand Up @@ -447,6 +450,7 @@ passing a non `nil` argument to `autometrics.Init` for the `pushConfiguration`:
+ Period: 1 * time.Second, // Period is only relevant when using OpenTelemetry implementation
+ Timeout: 500 * time.Millisecond, // Timeout is only relevant when using OpenTelementry implementation
+ },
nil,
)
```

Expand All @@ -456,6 +460,35 @@ can contact us so we can setup a managed instance of Prometheus for you. We will
give you collector URLs, that will work with both OpenTelemetry and Prometheus; and can be
visualized easily with our explorer as well!

#### Logging

Monitoring/Observability must not crash the application.

So when Autometrics encounters
errors, instead of bubbling it up until the program stops, it will log the error and absorb it.
To leave choice in the logging implementation (depending on your application dependencies),
Autometrics exposes a `Logger` interface you can implement and then inject in the `Init` call
to have the logging you want.

The `Logger` interface is a subset of `slog.Logger` methods, so that most loggers can be used.
Autometrics also provides 2 simple loggers out of the box:
- `NoOpLogger`, which is the default logger and does nothing,
- `PrintLogger` which uses `fmt.Print` to write logging messages on stdout.

To use the `PrintLogger` instead of the `NoOpLogger` for examble, you just have to change
the `Init` call:

``` patch
shutdown, err := autometrics.Init(
nil,
autometrics.DefBuckets,
autometrics.BuildInfo{ Version: "2.1.37", Commit: "anySHA", Branch: "", Service: "myApp" },
nil,
- nil,
+ autometrics.PrintLogger{},
)
```

#### Git hook

As autometrics is a Go generator that modifies the source code when run, it
Expand Down
1 change: 1 addition & 0 deletions examples/otel/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ func main() {
Branch: Branch,
},
pushConfiguration,
autometrics.PrintLogger{},
)
if err != nil {
log.Fatalf("Failed initialization of autometrics: %s", err)
Expand Down
1 change: 1 addition & 0 deletions examples/web/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ func main() {
Branch: Branch,
},
pushConfiguration,
autometrics.PrintLogger{},
)
if err != nil {
log.Fatalf("Failed initialization of autometrics: %s", err)
Expand Down
1 change: 1 addition & 0 deletions examples/web/cmd/main.go.orig
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ func main() {
Service: "autometrics-go-example-prometheus"
},
pushConfiguration,
autometrics.PrintLogger{},
)
if err != nil {
log.Fatalf("Failed initialization of autometrics: %s", err)
Expand Down
4 changes: 1 addition & 3 deletions internal/generate/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -320,9 +320,7 @@ func detectContextSelectorImpl(ctx *internal.GeneratorContext, argName string, s
}
}
} else {
// TODO: log that autometrics cannot detect multi-nested contexts instead of errorring
// continue
return true, fmt.Errorf("expecting parent to be an identifier, got %s instead", reflect.TypeOf(selector.X).String())
am.GetLogger().Error("expecting parent to be an identifier, got %s instead", reflect.TypeOf(selector.X).String())
}
return false, nil
}
Expand Down
24 changes: 20 additions & 4 deletions otel/autometrics/otel.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import (
"context"
"errors"
"fmt"
"log"
"os"
"sync"
"time"

"github.com/autometrics-dev/autometrics-go/pkg/autometrics"
"github.com/autometrics-dev/autometrics-go/pkg/autometrics/log"

"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
Expand Down Expand Up @@ -120,6 +120,17 @@ func completeMeterName(meterName string) string {
// the current (otel) package imported at the call site.
type BuildInfo = autometrics.BuildInfo

// Logger is an interface for logging autometrics-related events.
//
// This is a reexport to allow using only the current package at call site.
type Logger = log.Logger

// This is a reexport to allow using only the current package at call site.
type PrintLogger = log.PrintLogger

// This is a reexport to allow using only the current package at call site.
type NoOpLogger = log.NoOpLogger

// PushConfiguration holds meta information about the push-to-collector configuration of the instrumented code.
type PushConfiguration struct {
// URL of the collector to push to. It must be non-empty if this struct is built.
Expand Down Expand Up @@ -180,14 +191,19 @@ type PushConfiguration struct {
// Make sure that all the latency targets you want to use for SLOs are
// present in the histogramBuckets array, otherwise the alerts will fail
// to work (they will never trigger).
func Init(meterName string, histogramBuckets []float64, buildInformation BuildInfo, pushConfiguration *PushConfiguration) (context.CancelCauseFunc, error) {
func Init(meterName string, histogramBuckets []float64, buildInformation BuildInfo, pushConfiguration *PushConfiguration, logger log.Logger) (context.CancelCauseFunc, error) {
var err error
newCtx, cancelFunc := context.WithCancelCause(context.Background())
amCtx = newCtx

autometrics.SetCommit(buildInformation.Commit)
autometrics.SetVersion(buildInformation.Version)
autometrics.SetBranch(buildInformation.Branch)
if logger == nil {
autometrics.SetLogger(log.NoOpLogger{})
} else {
autometrics.SetLogger(logger)
}

var pushExporter metric.Exporter
if pushConfiguration != nil {
Expand Down Expand Up @@ -334,7 +350,7 @@ func initProvider(pushExporter metric.Exporter, pushConfiguration *PushConfigura
metric.WithResource(autometricsSrc),
), nil
} else {
log.Printf("autometrics: opentelemetry: setting up OTLP push configuration, pushing %s to %s\n",
autometrics.GetLogger().Debug("opentelemetry: setting up OTLP push configuration, pushing %s to %s\n",
autometrics.GetPushJobName(),
autometrics.GetPushJobURL(),
)
Expand Down Expand Up @@ -368,7 +384,7 @@ func initProvider(pushExporter metric.Exporter, pushConfiguration *PushConfigura
}

func initPushExporter(pushConfiguration *PushConfiguration) (metric.Exporter, error) {
log.Println("autometrics: opentelemetry: Init: detected push configuration")
autometrics.GetLogger().Debug("opentelemetry: Init: detected push configuration")
if pushConfiguration.CollectorURL == "" {
return nil, errors.New("invalid PushConfiguration: the CollectorURL must be set.")
}
Expand Down
6 changes: 4 additions & 2 deletions pkg/autometrics/ctx.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,8 +269,10 @@ func FillTracingAndCallerInfo(ctx context.Context) context.Context {
// NOTE: This also means that goroutines that outlive their as the caller will not have access to parent
// caller information, but hopefully by that point we got all the necessary accesses done.
// If not, it is a convenience we accept to give up to prevent memory usage from exploding.
// TODO: once settled on a login library, log the error instead of ignoring it
_ = PushFunctionName(ctx, callInfo.Current)
err := PushFunctionName(ctx, callInfo.Current)
if err != nil {
GetLogger().Error("adding a function name to the known spans: %s", err)
}

return ctx
}
Expand Down
13 changes: 13 additions & 0 deletions pkg/autometrics/global_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package autometrics // import "github.com/autometrics-dev/autometrics-go/pkg/aut

import (
"fmt"

"github.com/autometrics-dev/autometrics-go/pkg/autometrics/log"
)

// These variables are describing the state of the application being autometricized,
Expand Down Expand Up @@ -36,13 +38,24 @@ var (
pushJobName string
pushJobURL string
instrumentedSpans map[spanKey]FunctionID = make(map[spanKey]FunctionID)
logger log.Logger
)

type spanKey struct {
tid TraceID
sid SpanID
}

// GetLogger returns the current logging interface for Autometrics
func GetLogger() log.Logger {
return logger
}

// SetLogger sets the logging interface for Autometrics.
func SetLogger(newLogger log.Logger) {
logger = newLogger
}

// GetVersion returns the version of the codebase being instrumented.
func GetVersion() string {
return version
Expand Down
63 changes: 63 additions & 0 deletions pkg/autometrics/log/log.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package log // import "github.com/autometrics-dev/autometrics-go/pkg/autometrics/log"

import (
"context"
"fmt"
)

// Logger is an interface to implement to be able to inject a logger to Autometrics.
// The interface follows the interface of slog.Logger
type Logger interface {
Debug(msg string, args ...any)
DebugContext(ctx context.Context, msg string, args ...any)
Info(msg string, args ...any)
InfoContext(ctx context.Context, msg string, args ...any)
Warn(msg string, args ...any)
WarnContext(ctx context.Context, msg string, args ...any)
Error(msg string, args ...any)
ErrorContext(ctx context.Context, msg string, args ...any)
}

// NoOpLogger is the default logger for Autometrics. It does nothing.
type NoOpLogger struct{}

var _ Logger = NoOpLogger{}

func (_ NoOpLogger) Debug(msg string, args ...any) {}
func (_ NoOpLogger) DebugContext(ctx context.Context, msg string, args ...any) {}
func (_ NoOpLogger) Info(msg string, args ...any) {}
func (_ NoOpLogger) InfoContext(ctx context.Context, msg string, args ...any) {}
func (_ NoOpLogger) Warn(msg string, args ...any) {}
func (_ NoOpLogger) WarnContext(ctx context.Context, msg string, args ...any) {}
func (_ NoOpLogger) Error(msg string, args ...any) {}
func (_ NoOpLogger) ErrorContext(ctx context.Context, msg string, args ...any) {}

// PrintLogger is a simple logger implementation that simply prints the events to stdout
type PrintLogger struct{}

var _ Logger = PrintLogger{}

func (_ PrintLogger) Debug(msg string, args ...any) {
fmt.Printf("Autometrics - Debug: %v", fmt.Sprintf(msg, args...))
}
func (_ PrintLogger) DebugContext(ctx context.Context, msg string, args ...any) {
fmt.Printf("Autometrics - Debug: %v", fmt.Sprintf(msg, args...))
}
func (_ PrintLogger) Info(msg string, args ...any) {
fmt.Printf("Autometrics - Info: %v", fmt.Sprintf(msg, args...))
}
func (_ PrintLogger) InfoContext(ctx context.Context, msg string, args ...any) {
fmt.Printf("Autometrics - Info: %v", fmt.Sprintf(msg, args...))
}
func (_ PrintLogger) Warn(msg string, args ...any) {
fmt.Printf("Autometrics - Warn: %v", fmt.Sprintf(msg, args...))
}
func (_ PrintLogger) WarnContext(ctx context.Context, msg string, args ...any) {
fmt.Printf("Autometrics - Warn: %v", fmt.Sprintf(msg, args...))
}
func (_ PrintLogger) Error(msg string, args ...any) {
fmt.Printf("Autometrics - Error: %v", fmt.Sprintf(msg, args...))
}
func (_ PrintLogger) ErrorContext(ctx context.Context, msg string, args ...any) {
fmt.Printf("Autometrics - Error: %v", fmt.Sprintf(msg, args...))
}
22 changes: 19 additions & 3 deletions prometheus/autometrics/prometheus.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import (
"context"
"errors"
"fmt"
"log"
"os"
"sync"

"github.com/autometrics-dev/autometrics-go/pkg/autometrics"
"github.com/autometrics-dev/autometrics-go/pkg/autometrics/log"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/push"
"github.com/prometheus/common/expfmt"
Expand Down Expand Up @@ -105,6 +105,17 @@ const (
// the current (prometheus) package imported at the call site.
type BuildInfo = autometrics.BuildInfo

// Logger is an interface for logging autometrics-related events.
//
// This is a reexport to allow using only the current package at call site.
type Logger = log.Logger

// This is a reexport to allow using only the current package at call site.
type PrintLogger = log.PrintLogger

// This is a reexport to allow using only the current package at call site.
type NoOpLogger = log.NoOpLogger

// PushConfiguration holds meta information about the push-to-collector configuration of the instrumented code.
//
// This is a reexport of the autometrics type to allow [Init] to work with only
Expand All @@ -131,17 +142,22 @@ type PushConfiguration = autometrics.PushConfiguration
// Make sure that all the latency targets you want to use for SLOs are
// present in the histogramBuckets array, otherwise the alerts will fail
// to work (they will never trigger.)
func Init(reg *prometheus.Registry, histogramBuckets []float64, buildInformation BuildInfo, pushConfiguration *PushConfiguration) (context.CancelCauseFunc, error) {
func Init(reg *prometheus.Registry, histogramBuckets []float64, buildInformation BuildInfo, pushConfiguration *PushConfiguration, logger log.Logger) (context.CancelCauseFunc, error) {
newCtx, cancelFunc := context.WithCancelCause(context.Background())
amCtx = newCtx

autometrics.SetCommit(buildInformation.Commit)
autometrics.SetVersion(buildInformation.Version)
autometrics.SetBranch(buildInformation.Branch)
if logger == nil {
autometrics.SetLogger(log.NoOpLogger{})
} else {
autometrics.SetLogger(logger)
}

pusher = nil
if pushConfiguration != nil {
log.Printf("autometrics: Init: detected push configuration to %s", pushConfiguration.CollectorURL)
autometrics.GetLogger().Debug("Init: detected push configuration to %s", pushConfiguration.CollectorURL)

if pushConfiguration.CollectorURL == "" {
return nil, errors.New("invalid PushConfiguration: the CollectorURL must be set.")
Expand Down

0 comments on commit be7aa75

Please sign in to comment.