Reference implementation
This commit is contained in:
		
							
								
								
									
										126
									
								
								pkg/srv/http.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								pkg/srv/http.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,126 @@
 | 
			
		||||
package srv
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"net"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/prometheus/client_golang/prometheus/promhttp"
 | 
			
		||||
	"github.com/rs/zerolog"
 | 
			
		||||
	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
 | 
			
		||||
	"go.opentelemetry.io/otel/metric"
 | 
			
		||||
	"go.opentelemetry.io/otel/trace"
 | 
			
		||||
 | 
			
		||||
	"gitea.libretechconsulting.com/rmcguire/go-http-server-with-otel/pkg/config"
 | 
			
		||||
	"gitea.libretechconsulting.com/rmcguire/go-http-server-with-otel/pkg/otel"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	httpMeter    metric.Meter
 | 
			
		||||
	httpTracer   trace.Tracer
 | 
			
		||||
	readTimeout  = 10 * time.Second
 | 
			
		||||
	writeTimeout = 10 * time.Second
 | 
			
		||||
	idleTimeout  = 15 * time.Second
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type HTTPFunc struct {
 | 
			
		||||
	Path        string
 | 
			
		||||
	HandlerFunc http.HandlerFunc
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func prepHTTPServer(ctx context.Context, handleFuncs ...HTTPFunc) *http.Server {
 | 
			
		||||
	var (
 | 
			
		||||
		cfg = config.MustFromCtx(ctx)
 | 
			
		||||
		l   = zerolog.Ctx(ctx)
 | 
			
		||||
		mux = &http.ServeMux{}
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	// NOTE: Wraps handle func with otelhttp handler and
 | 
			
		||||
	// inserts route tag
 | 
			
		||||
	otelHandleFunc := func(pattern string, handlerFunc func(http.ResponseWriter, *http.Request)) {
 | 
			
		||||
		handler := otelhttp.WithRouteTag(pattern, http.HandlerFunc(handlerFunc))
 | 
			
		||||
		mux.Handle(pattern, handler) // Associate pattern with handler
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	otelHandleFunc("/health", handleHealthCheckFunc(ctx))
 | 
			
		||||
	otelHandleFunc("/", handleHealthCheckFunc(ctx))
 | 
			
		||||
 | 
			
		||||
	for _, f := range handleFuncs {
 | 
			
		||||
		otelHandleFunc(f.Path, f.HandlerFunc)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Prometheus metrics endpoint
 | 
			
		||||
	if cfg.OTEL.PrometheusEnabled {
 | 
			
		||||
		mux.Handle(cfg.OTEL.PrometheusPath, promhttp.Handler())
 | 
			
		||||
		l.Info().Str("prometheusPath", cfg.OTEL.PrometheusPath).
 | 
			
		||||
			Msg("mounted prometheus metrics endpoint")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Add OTEL, skip health-check spans
 | 
			
		||||
	// NOTE: Add any other span exclusions here
 | 
			
		||||
	handler := otelhttp.NewHandler(mux, "/",
 | 
			
		||||
		otelhttp.WithFilter(func(r *http.Request) bool {
 | 
			
		||||
			switch r.URL.Path {
 | 
			
		||||
			case "/health":
 | 
			
		||||
				return false
 | 
			
		||||
			case cfg.OTEL.PrometheusPath:
 | 
			
		||||
				return false
 | 
			
		||||
			default:
 | 
			
		||||
				return true
 | 
			
		||||
			}
 | 
			
		||||
		}))
 | 
			
		||||
 | 
			
		||||
	return &http.Server{
 | 
			
		||||
		Addr:         cfg.HTTP.Listen,
 | 
			
		||||
		ReadTimeout:  readTimeout,
 | 
			
		||||
		WriteTimeout: writeTimeout,
 | 
			
		||||
		IdleTimeout:  idleTimeout,
 | 
			
		||||
		Handler:      handler,
 | 
			
		||||
		BaseContext: func(_ net.Listener) context.Context {
 | 
			
		||||
			return ctx
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Returns a shutdown func and a done channel if the
 | 
			
		||||
// server aborts abnormally. Panics on error.
 | 
			
		||||
func MustInitHTTPServer(ctx context.Context, funcs ...HTTPFunc) (func(context.Context) error, <-chan interface{}) {
 | 
			
		||||
	shutdownFunc, doneChan, err := InitHTTPServer(ctx, funcs...)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		panic(err)
 | 
			
		||||
	}
 | 
			
		||||
	return shutdownFunc, doneChan
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Returns a shutdown func and a done channel if the
 | 
			
		||||
// server aborts abnormally. Returns error on failure to start
 | 
			
		||||
func InitHTTPServer(ctx context.Context, funcs ...HTTPFunc) (func(context.Context) error, <-chan interface{}, error) {
 | 
			
		||||
	l := zerolog.Ctx(ctx)
 | 
			
		||||
	doneChan := make(chan interface{})
 | 
			
		||||
 | 
			
		||||
	var server *http.Server
 | 
			
		||||
 | 
			
		||||
	httpMeter = otel.GetMeter(ctx, "http")
 | 
			
		||||
	httpTracer = otel.GetTracer(ctx, "http")
 | 
			
		||||
 | 
			
		||||
	server = prepHTTPServer(ctx, funcs...)
 | 
			
		||||
 | 
			
		||||
	go func() {
 | 
			
		||||
		l.Debug().Msg("HTTP Server Started")
 | 
			
		||||
		err := server.ListenAndServe()
 | 
			
		||||
		if err != nil && err != http.ErrServerClosed {
 | 
			
		||||
			l.Err(err).Msg("HTTP server error")
 | 
			
		||||
		} else {
 | 
			
		||||
			l.Info().Msg("HTTP server shut down")
 | 
			
		||||
		}
 | 
			
		||||
		doneChan <- nil
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	// Shut down http server with a deadline
 | 
			
		||||
	return func(shutdownCtx context.Context) error {
 | 
			
		||||
		l.Debug().Msg("stopping http server")
 | 
			
		||||
		server.Shutdown(shutdownCtx)
 | 
			
		||||
		return nil
 | 
			
		||||
	}, doneChan, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										59
									
								
								pkg/srv/http_health.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								pkg/srv/http_health.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,59 @@
 | 
			
		||||
package srv
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"math/rand"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func handleHealthCheckFunc(_ context.Context) func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	// Return http handle func
 | 
			
		||||
	return func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		var err error
 | 
			
		||||
		var healthChecksFailed bool
 | 
			
		||||
 | 
			
		||||
		// TODO: Insert useful health checks here
 | 
			
		||||
		// For multiple checks, perform concurrently
 | 
			
		||||
		// Consider using errors.Join() for multiple checks
 | 
			
		||||
		var hcWg sync.WaitGroup
 | 
			
		||||
		for range 5 {
 | 
			
		||||
			hcWg.Add(1)
 | 
			
		||||
			go func() {
 | 
			
		||||
				defer hcWg.Done()
 | 
			
		||||
				err = errors.Join(err, dummyHealthCheck(r.Context()))
 | 
			
		||||
			}()
 | 
			
		||||
		}
 | 
			
		||||
		hcWg.Wait()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			healthChecksFailed = true
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// TODO: Friendly reminder...
 | 
			
		||||
		err = errors.New("WARNING: Unimplemented health-check")
 | 
			
		||||
 | 
			
		||||
		if healthChecksFailed {
 | 
			
		||||
			w.WriteHeader(http.StatusInternalServerError)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			w.Write([]byte(err.Error()))
 | 
			
		||||
		} else {
 | 
			
		||||
			w.Write([]byte("ok"))
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func dummyHealthCheck(ctx context.Context) error {
 | 
			
		||||
	workFor := rand.Intn(750)
 | 
			
		||||
	ticker := time.NewTicker(time.Duration(time.Duration(workFor) * time.Millisecond))
 | 
			
		||||
 | 
			
		||||
	select {
 | 
			
		||||
	case <-ticker.C:
 | 
			
		||||
		return nil
 | 
			
		||||
	case <-ctx.Done():
 | 
			
		||||
		return ctx.Err()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user