gofuncy/go.go
2025-05-13 18:17:36 +02:00

227 lines
5.3 KiB
Go

package gofuncy
import (
"context"
"crypto/sha256"
"fmt"
"os"
"runtime"
"time"
"github.com/Ju0x/humanhash"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/metric"
semconv "go.opentelemetry.io/otel/semconv/v1.30.0"
"go.opentelemetry.io/otel/trace"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
type (
Options struct {
l *zap.Logger
ctx context.Context //nolint:containedctx // required
level zapcore.Level
name string
// telemetry
meter metric.Meter
tracer trace.Tracer
runningCounter metric.Int64UpDownCounter
runningCounterName string
completedCounter metric.Int64Counter
completedCounterName string
durationHistogram metric.Int64Histogram
durationHistogramName string
durationHistogramEnabled bool
telemetryEnabled bool
}
Option func(*Options)
)
func WithName(name string) Option {
return func(o *Options) {
o.name = name
}
}
func WithContext(ctx context.Context) Option {
return func(o *Options) {
o.ctx = ctx
}
}
func WithLogger(l *zap.Logger) Option {
return func(o *Options) {
o.l = l
}
}
func WithLogLevel(level zapcore.Level) Option {
return func(o *Options) {
o.level = level
}
}
func WithMeter(v metric.Meter) Option {
return func(o *Options) {
o.meter = v
}
}
func WithTracer(v trace.Tracer) Option {
return func(o *Options) {
o.tracer = v
}
}
func WithCompletedCounterName(name string) Option {
return func(o *Options) {
o.completedCounterName = name
}
}
func WithRunningCounterName(name string) Option {
return func(o *Options) {
o.runningCounterName = name
}
}
func WithDurationHistogramEnabled(v bool) Option {
return func(o *Options) {
o.durationHistogramEnabled = v
}
}
func WithHistogramName(name string) Option {
return func(o *Options) {
o.durationHistogramName = name
}
}
func Go(fn Func, opts ...Option) <-chan error {
o := &Options{
l: zap.NewNop(),
level: zapcore.DebugLevel,
runningCounterName: "gofuncy.routine.running.count",
completedCounterName: "gofuncy.routine.completed.count",
durationHistogramName: "gofuncy.routine.duration",
telemetryEnabled: os.Getenv("OTEL_ENABLED") == "true",
}
for _, opt := range opts {
if opt != nil {
opt(o)
}
}
if o.ctx == nil {
o.ctx = context.Background()
}
if o.name == "" {
if _, file, line, ok := runtime.Caller(0); ok {
h := sha256.New()
_, _ = fmt.Fprintf(h, "%s:%d", file, line)
o.name, _ = humanhash.Humanize(h.Sum(nil), 2, "-")
}
}
// create telemetry if enabled
if o.telemetryEnabled {
if o.meter == nil {
o.meter = otel.Meter("gofuncy")
}
if o.tracer == nil {
o.tracer = otel.Tracer("gofuncy")
}
}
if o.meter != nil {
if value, err := o.meter.Int64UpDownCounter(
o.runningCounterName,
metric.WithDescription("Gofuncy running go routine count"),
); err != nil {
o.l.Error("failed to initialize counter", zap.Error(err))
} else {
o.runningCounter = value
}
if value, err := o.meter.Int64Counter(
o.completedCounterName,
metric.WithDescription("Gofuncy completed go routine count"),
); err != nil {
o.l.Error("failed to initialize counter", zap.Error(err))
} else {
o.completedCounter = value
}
}
if o.meter != nil && o.durationHistogramEnabled {
if value, err := o.meter.Int64Histogram(
o.durationHistogramName,
metric.WithDescription("Gofuncy go routine duration histogram"),
); err != nil {
o.l.Error("failed to initialize histogram", zap.Error(err))
} else {
o.durationHistogram = value
}
}
delay := time.Now()
errChan := make(chan error, 1)
go func(o *Options, errChan chan<- error) {
var err error
ctx := o.ctx
start := time.Now()
defer close(errChan)
l := o.l.With(zap.String("name", o.name))
if value := RoutineFromContext(ctx); value != NoNameRoutine {
l = l.With(zap.String("parent", value))
}
var span trace.Span
if o.tracer != nil {
ctx, span = o.tracer.Start(o.ctx, o.name)
if span.IsRecording() {
l = l.With(zap.String("trace_id", span.SpanContext().TraceID().String()))
}
defer func() {
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
}
span.End()
}()
}
l.Log(o.level, "starting gofuncy routine",
zap.Duration("delay", time.Since(delay).Round(time.Millisecond)),
)
defer func() {
l.Log(o.level, "exiting gofuncy routine",
zap.Duration("duration", time.Since(start).Round(time.Millisecond)),
zap.Error(err),
)
}()
// create telemetry if enabled
attrs := metric.WithAttributes(semconv.ProcessRuntimeName(o.name))
if o.runningCounter != nil {
o.runningCounter.Add(ctx, 1, attrs)
defer o.runningCounter.Add(ctx, -1, attrs)
}
if o.completedCounter != nil {
defer o.completedCounter.Add(ctx, 1, attrs, metric.WithAttributes(
attribute.Bool("error", err != nil),
))
}
if o.durationHistogram != nil {
defer func() {
o.durationHistogram.Record(ctx, time.Since(start).Milliseconds(), attrs, metric.WithAttributes(
attribute.Bool("error", err != nil),
))
}()
}
ctx = injectParentRoutineIntoContext(ctx, RoutineFromContext(ctx))
ctx = injectRoutineIntoContext(ctx, o.name)
err = fn(ctx)
errChan <- err
}(o, errChan)
return errChan
}