diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..e24c084 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,30 @@ +name: build + +on: + push: + pull_request: + branches: + - main + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Running tests + uses: docker/build-push-action@v6 + with: + push: false + target: test + + - name: Build production image + uses: docker/build-push-action@v6 + with: + push: false + target: production diff --git a/Dockerfile b/Dockerfile index 9e4b6a9..a5c4f58 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # Stage to copy the code and fetch dependencies # try to keep the debian version in sync with the distroless version -FROM golang:1.23-bookworm as base +FROM golang:1.23-bookworm AS base ARG VERSION=development @@ -14,18 +14,18 @@ COPY main.go ./ COPY pkg/ ./pkg # Stage to test the code -FROM base as test +FROM base AS test -RUN go vet -v -RUN go test -v +RUN go vet -v ./... +RUN go test -v ./... # Stage to build the binary -FROM base as build +FROM base AS build RUN CGO_ENABLED=0 go build -ldflags="-X 'github.com/blackskad/go-web-scaffold/environment.Version=${VERSION}'" -o app . # Stage with the production binary -FROM gcr.io/distroless/static-debian12 as production +FROM gcr.io/distroless/static-debian12 AS production COPY --from=build /work/app / diff --git a/go.mod b/go.mod index c60caea..6be6f7a 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,9 @@ module github.com/blackskad/go-web-scaffold go 1.23.0 require ( + github.com/kelseyhightower/envconfig v1.4.0 github.com/prometheus/client_golang v1.20.1 + github.com/stretchr/testify v1.9.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 go.opentelemetry.io/contrib/instrumentation/runtime v0.53.0 go.opentelemetry.io/otel v1.28.0 @@ -18,6 +20,7 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -25,6 +28,7 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect github.com/klauspost/compress v1.17.9 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.55.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect @@ -39,4 +43,5 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect google.golang.org/grpc v1.64.1 // indirect google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 8045f31..5c3ea5c 100644 --- a/go.sum +++ b/go.sum @@ -19,8 +19,14 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= +github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= +github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= @@ -35,6 +41,8 @@ github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= @@ -77,5 +85,8 @@ google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index bf98ee9..26548fd 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "os/signal" "syscall" + "github.com/blackskad/go-web-scaffold/pkg/environment" "github.com/blackskad/go-web-scaffold/pkg/o11y" "github.com/blackskad/go-web-scaffold/pkg/web" ) @@ -14,6 +15,8 @@ import ( func main() { ctx := context.Background() + conf := environment.Parse() + o11y.StartPProfServer() sigc := make(chan os.Signal, 1) @@ -28,7 +31,7 @@ func main() { server := &http.Server{ Addr: ":8080", - Handler: o11y.Register(ctx, mux), + Handler: o11y.Register(ctx, conf, mux), } done := make(chan struct{}) diff --git a/pkg/environment/environment.go b/pkg/environment/environment.go index 595acb2..fd365dd 100644 --- a/pkg/environment/environment.go +++ b/pkg/environment/environment.go @@ -1,4 +1,18 @@ package environment +import "github.com/kelseyhightower/envconfig" + const Name = "go-web-scaffold" const Version = "development" + +type Config struct { + EnableTracesStdout bool `default:"true" split_words:"true"` +} + +func Parse() Config { + var conf Config + + envconfig.MustProcess("go-web-scaffold", &conf) + + return conf +} diff --git a/pkg/environment/environment_test.go b/pkg/environment/environment_test.go new file mode 100644 index 0000000..be2ea7b --- /dev/null +++ b/pkg/environment/environment_test.go @@ -0,0 +1,52 @@ +package environment_test + +import ( + "os" + "testing" + + "github.com/blackskad/go-web-scaffold/pkg/environment" + "github.com/stretchr/testify/assert" +) + +func TestConfig(t *testing.T) { + + const envVarName = "GO-WEB-SCAFFOLD_ENABLE_TRACES_STDOUT" + + // Make sure the env var is not accidentially set somewhere else + os.Unsetenv(envVarName) + + tests := []struct { + name string + value string + enabled bool + }{ + { + name: "default", + value: "", + enabled: true, + }, + { + name: "explicit true", + value: "true", + enabled: true, + }, + { + name: "explicit false", + value: "false", + enabled: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer os.Unsetenv(envVarName) + + if tt.value != "" { + os.Setenv(envVarName, tt.value) + } + + c := environment.Parse() + assert.Equal(t, tt.enabled, c.EnableTracesStdout) + }) + } +} diff --git a/pkg/o11y/otel.go b/pkg/o11y/otel.go index 0e477ab..2782a3e 100644 --- a/pkg/o11y/otel.go +++ b/pkg/o11y/otel.go @@ -23,7 +23,7 @@ import ( semconv "go.opentelemetry.io/otel/semconv/v1.26.0" ) -func initTracer(ctx context.Context) (*sdktrace.TracerProvider, error) { +func initTracer(ctx context.Context, conf environment.Config) (*sdktrace.TracerProvider, error) { opts := []sdktrace.TracerProviderOption{ // TODO: add ability to switch between sdktrace.AlwaysSample and sdktrace.ProbabilitySampler for production. sdktrace.WithSampler(sdktrace.AlwaysSample()), @@ -37,11 +37,13 @@ func initTracer(ctx context.Context) (*sdktrace.TracerProvider, error) { } // Always export to stdout - expStdout, err := stdouttrace.New() - if err != nil { - return nil, err + if conf.EnableTracesStdout { + expStdout, err := stdouttrace.New() + if err != nil { + return nil, err + } + opts = append(opts, sdktrace.WithBatcher(expStdout)) } - opts = append(opts, sdktrace.WithBatcher(expStdout)) // If the env var is set, also export to an otel collector if os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT") != "" { @@ -56,7 +58,7 @@ func initTracer(ctx context.Context) (*sdktrace.TracerProvider, error) { tp := sdktrace.NewTracerProvider(opts...) otel.SetTracerProvider(tp) otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) - return tp, err + return tp, nil } func initMeter() (*sdkmetric.MeterProvider, error) { @@ -96,9 +98,9 @@ func runPrometheusServer() { var initOnce sync.Once -func Register(ctx context.Context, h http.Handler) http.Handler { +func Register(ctx context.Context, conf environment.Config, h http.Handler) http.Handler { initOnce.Do(func() { - _, err := initTracer(ctx) + _, err := initTracer(ctx, conf) if err != nil { log.Fatal(err) }