Added OpenTelemetry and use structured logging
This commit is contained in:
+1
-2
@@ -1,8 +1,7 @@
|
|||||||
FROM golang:1.26 AS build
|
FROM golang:1.26 AS build
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
#COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
COPY go.mod ./
|
|
||||||
|
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
|
||||||
|
|||||||
+46
-7
@@ -3,7 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"log"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
@@ -11,9 +11,44 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/robindittmar/dttmr-api/internal/api/router"
|
"github.com/robindittmar/dttmr-api/internal/api/router"
|
||||||
|
"github.com/robindittmar/dttmr-api/internal/telemetry"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
serviceName := "dttmr-api"
|
||||||
|
serviceVersion := "0.1.0"
|
||||||
|
|
||||||
|
if err := run(serviceName, serviceVersion); err != nil {
|
||||||
|
slog.Error("Service crashed", slog.Any("error", err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(serviceName string, serviceVersion string) error {
|
||||||
|
baseHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
|
||||||
|
Level: slog.LevelInfo,
|
||||||
|
})
|
||||||
|
traceHandler := &telemetry.TraceHandler{Handler: baseHandler}
|
||||||
|
|
||||||
|
logger := slog.New(traceHandler)
|
||||||
|
slog.SetDefault(logger)
|
||||||
|
|
||||||
|
slog.Info("Starting service", slog.String("service", serviceName), slog.String("version", serviceVersion))
|
||||||
|
|
||||||
|
shutdownTelemetry, err := telemetry.Init(context.Background(), serviceName, serviceVersion)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to initialize telemetry", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := shutdownTelemetry(shutdownCtx); err != nil {
|
||||||
|
slog.Error("Failed to shutdown telemetry", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
cfg := router.Config{}
|
cfg := router.Config{}
|
||||||
mux := router.NewMux(cfg)
|
mux := router.NewMux(cfg)
|
||||||
|
|
||||||
@@ -26,22 +61,26 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
log.Printf("Listening on %s\n", srv.Addr)
|
slog.Info("Starting http server", "addr", srv.Addr)
|
||||||
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
log.Fatalf("Failed to start server: %v", err)
|
slog.Error("Failed to start http server", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
quit := make(chan os.Signal, 1)
|
quit := make(chan os.Signal, 1)
|
||||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||||
<-quit
|
sig := <-quit
|
||||||
log.Println("Shutting down server...")
|
slog.Info("Shutting down server...", slog.String("signal", sig.String()))
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
if err := srv.Shutdown(ctx); err != nil {
|
if err := srv.Shutdown(ctx); err != nil {
|
||||||
log.Fatalf("Server forced to shutdown: %v", err)
|
slog.Error("Server forced to shutdown", slog.Any("error", err))
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
log.Println("Server shutdown successful!")
|
|
||||||
|
slog.Info("Service shutdown successful!")
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,32 @@
|
|||||||
module github.com/robindittmar/dttmr-api
|
module github.com/robindittmar/dttmr-api
|
||||||
|
|
||||||
go 1.26
|
go 1.26
|
||||||
|
|
||||||
|
require (
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0
|
||||||
|
go.opentelemetry.io/otel v1.44.0
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0
|
||||||
|
go.opentelemetry.io/otel/sdk v1.44.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/metric v1.44.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.44.0 // indirect
|
||||||
|
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
||||||
|
golang.org/x/net v0.55.0 // indirect
|
||||||
|
golang.org/x/sys v0.45.0 // indirect
|
||||||
|
golang.org/x/text v0.37.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
||||||
|
google.golang.org/grpc v1.81.1 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/robindittmar/dttmr-api/internal/api/response"
|
"github.com/robindittmar/dttmr-api/internal/api/response"
|
||||||
@@ -18,15 +18,17 @@ type apiResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func DefaultHandler(w http.ResponseWriter, r *http.Request) {
|
func DefaultHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
var resp apiResponse
|
ctx := r.Context()
|
||||||
|
|
||||||
resp.Method = r.Method
|
resp := apiResponse{
|
||||||
resp.Url = r.URL.String()
|
Method: r.Method,
|
||||||
resp.Proto = r.Proto
|
Url: r.URL.String(),
|
||||||
resp.Header = make(map[string]string)
|
Proto: r.Proto,
|
||||||
resp.Host = r.Host
|
Header: make(map[string]string),
|
||||||
resp.RemoteAddr = r.RemoteAddr
|
Host: r.Host,
|
||||||
resp.Form = make(map[string]string)
|
RemoteAddr: r.RemoteAddr,
|
||||||
|
Form: make(map[string]string),
|
||||||
|
}
|
||||||
|
|
||||||
for k, v := range r.Header {
|
for k, v := range r.Header {
|
||||||
resp.Header[k] = v[0]
|
resp.Header[k] = v[0]
|
||||||
@@ -37,8 +39,8 @@ func DefaultHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
resp.Form[k] = v[0]
|
resp.Form[k] = v[0]
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Println(err)
|
slog.ErrorContext(ctx, "Error parsing form", slog.Any("error", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
response.JSON(w, http.StatusOK, resp)
|
response.JSON(ctx, w, http.StatusOK, resp)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,10 @@ type healthResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func HealthHandler(w http.ResponseWriter, r *http.Request) {
|
func HealthHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
_ = r
|
ctx := r.Context()
|
||||||
|
|
||||||
resp := healthResponse{
|
resp := healthResponse{
|
||||||
Status: "ok",
|
Status: "ok",
|
||||||
}
|
}
|
||||||
response.JSON(w, http.StatusOK, resp)
|
response.JSON(ctx, w, http.StatusOK, resp)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ package middleware
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||||
)
|
)
|
||||||
|
|
||||||
func WithTelemetry(next http.Handler) http.Handler {
|
func WithTelemetry(next http.Handler) http.Handler {
|
||||||
//return otelhttp.NewHandler(next, "dttmr-api")
|
return otelhttp.NewHandler(next, "dttmr-api")
|
||||||
return next
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
package response
|
package response
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"log"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
func JSON(w http.ResponseWriter, status int, data any) {
|
func JSON(ctx context.Context, w http.ResponseWriter, status int, data any) {
|
||||||
payload, err := json.Marshal(data)
|
payload, err := json.Marshal(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
_, err := w.Write([]byte(`{"error": "internal server error: failed to marshal response"}`))
|
_, err := w.Write([]byte(`{"error": "internal server error: failed to marshal response"}`))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
slog.ErrorContext(ctx, "Failed to write JSON", slog.Any("error", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -23,11 +24,11 @@ func JSON(w http.ResponseWriter, status int, data any) {
|
|||||||
w.WriteHeader(status)
|
w.WriteHeader(status)
|
||||||
_, err = w.Write(payload)
|
_, err = w.Write(payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
slog.ErrorContext(ctx, "Failed to write response", slog.Any("error", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Error(w http.ResponseWriter, status int, message string) {
|
func Error(ctx context.Context, w http.ResponseWriter, status int, message string) {
|
||||||
JSON(w, status, map[string]string{"error": message})
|
JSON(ctx, w, status, map[string]string{"error": message})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package telemetry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TraceHandler struct {
|
||||||
|
slog.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *TraceHandler) Handle(ctx context.Context, r slog.Record) error {
|
||||||
|
spanCtx := trace.SpanContextFromContext(ctx)
|
||||||
|
if spanCtx.HasTraceID() {
|
||||||
|
r.AddAttrs(
|
||||||
|
slog.String("trace_id", spanCtx.TraceID().String()),
|
||||||
|
slog.String("span_id", spanCtx.SpanID().String()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.Handler.Handle(ctx, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *TraceHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||||
|
return &TraceHandler{Handler: h.Handler.WithAttrs(attrs)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *TraceHandler) WithGroup(name string) slog.Handler {
|
||||||
|
return &TraceHandler{Handler: h.Handler.WithGroup(name)}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package telemetry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
||||||
|
"go.opentelemetry.io/otel/propagation"
|
||||||
|
"go.opentelemetry.io/otel/sdk/resource"
|
||||||
|
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||||
|
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Init(ctx context.Context, serviceName string, version string) (func(context.Context) error, error) {
|
||||||
|
var shutdownFns []func(context.Context) error
|
||||||
|
|
||||||
|
shutdown := func(ctx context.Context) error {
|
||||||
|
var err error
|
||||||
|
for _, fn := range shutdownFns {
|
||||||
|
err = errors.Join(err, fn(ctx))
|
||||||
|
}
|
||||||
|
shutdownFns = nil
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := resource.New(ctx,
|
||||||
|
resource.WithAttributes(
|
||||||
|
semconv.ServiceName(serviceName),
|
||||||
|
semconv.ServiceVersion(version),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return shutdown, err
|
||||||
|
}
|
||||||
|
|
||||||
|
otel.SetTextMapPropagator(
|
||||||
|
propagation.NewCompositeTextMapPropagator(
|
||||||
|
propagation.TraceContext{},
|
||||||
|
propagation.Baggage{},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
traceExporter, err := otlptracegrpc.New(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return shutdown, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tracerProvider := sdktrace.NewTracerProvider(
|
||||||
|
sdktrace.WithBatcher(traceExporter),
|
||||||
|
sdktrace.WithResource(res),
|
||||||
|
)
|
||||||
|
otel.SetTracerProvider(tracerProvider)
|
||||||
|
shutdownFns = append(shutdownFns, tracerProvider.Shutdown)
|
||||||
|
|
||||||
|
return shutdown, nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user