diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000000000000000000000000000000000..32142e4fe2c1c0f445c1808489b5579208a9491e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +# EditorConfig is awesome: https://editorconfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true diff --git a/.gitignore b/.gitignore index 7b86b0c38885f4f3418212a1f6ad0efc5fab4bbd..7973f4e0e80615ccb708844ab5b87f225bfac1c5 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ gen/* +bin/* diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 863723aaa30f5fdc6ee2ac9d161759276c1ac987..1c9fa66cb7fdd053e4655186315c05023eb45648 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,43 +1,77 @@ stages: - - build - - lint - #- test - #- analyze + - prepare + - build + - lint + - test variables: - IMAGE_PATH: "${CI_REGISTRY_IMAGE}" - GOLANG_VERSION: "1.24" - GOLANG_MINOR_VERSION: "${GOLANG_VERSION}.0" - DOCKER_TLS_CERTDIR: "/certs" - -lint-buf: - stage: lint - image: - name: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/bufbuild/buf - entrypoint: [""] - script: - - buf format - needs: [] + IMAGE_PATH: "${CI_REGISTRY_IMAGE}" + GOLANG_VERSION: "1.24" + GOLANG_MINOR_VERSION: "${GOLANG_VERSION}.0" + DOCKER_TLS_CERTDIR: "/certs" + +generate-api: + stage: prepare + image: + name: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/bufbuild/buf + entrypoint: [""] + script: + - buf generate + artifacts: + untracked: true + paths: + - gen/ + +lint-api: + stage: lint + image: + name: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/bufbuild/buf + entrypoint: [""] + script: + - buf format + needs: + - [] + +lint-go: + stage: lint + image: + name: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/golangci/golangci-lint:v2.0.2 + script: + - golangci-lint run + dependencies: + - generate-api + +test-go: + stage: test + image: + name: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/golang:${GOLANG_VERSION} + script: + - go test ./... + dependencies: + - generate-api # Build stage .build: &build - stage: build - image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/docker:latest - services: - - name: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/docker:latest - alias: docker - before_script: - - apk add git - - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - - docker login -u $CI_DEPENDENCY_PROXY_USER -p $CI_DEPENDENCY_PROXY_PASSWORD $CI_DEPENDENCY_PROXY_SERVER - needs: [] + stage: build + image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/docker:latest + services: + - name: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/docker:latest + alias: docker + before_script: + - apk add git + - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY + - docker login -u $CI_DEPENDENCY_PROXY_USER -p $CI_DEPENDENCY_PROXY_PASSWORD $CI_DEPENDENCY_PROXY_SERVER build-kms: - script: - - docker build -t kms -f kms/Dockerfile . - <<: *build + script: + - docker build -t kms -f kms/Dockerfile . + dependencies: + - generate-api + <<: *build build-ctrl: - script: - - docker build -t ctrl -f ctrl/Dockerfile . - <<: *build + script: + - docker build -t ctrl -f ctrl/Dockerfile . + dependencies: + - generate-api + <<: *build diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000000000000000000000000000000000000..43c27a6206c097c68c6c5cd67f44025a54ff1c50 --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,28 @@ +version: "2" + +linters: + default: standard + +formatters: + # Enable specific formatter. + # Default: [] (uses standard Go formatting) + enable: + - gci + - gofmt + - gofumpt + - goimports + - golines + +issues: + # Maximum issues count per one linter. + # Set to 0 to disable. + # Default: 50 + max-issues-per-linter: 0 + # Maximum count of issues with the same text. + # Set to 0 to disable. + # Default: 3 + max-same-issues: 0 + fix: true + +run: + timeout: 5m diff --git a/Makefile b/Makefile deleted file mode 100644 index 16b2ab1f520ea4e9c4638dcfc8041556128d8234..0000000000000000000000000000000000000000 --- a/Makefile +++ /dev/null @@ -1,4 +0,0 @@ - -generate-plantuml: - @echo "Generating PlantUML diagrams if plantUML is installed..." - plantuml -o . api/_Docs/*/**.plantuml \ No newline at end of file diff --git a/README.md b/README.md index b3657ee867ff12ad143cc08daa2344f5ff995e1f..05f34478cf2e810fceb74f784f68310d0ee4f715 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,9 @@ If you are interested in production grade QKDN software, please feel free to con ## Overview This repository is separated into different parts, mainly API definitions and code. -We follow a SDN approach which is *controller centric*. +We follow a SDN approach which is _controller centric_. This means that a central `QKDN-Controller` is responsible to manage and configure the network. - [API v1](api/_Docs/v1/Readme.md) +- [Logging Guide](docs/Logging.md) +- [Tracing Guide](docs/Tracing.md) diff --git a/api/_Docs/v1/gfx/forwarding_complete.plantuml b/api/_Docs/v1/gfx/forwarding_complete.plantuml index 340c1359737a2ece1fd3a7b6a757ddfbd46fc724..abbc3206fa9ea73e7e6c8832291df7c99a018cf7 100644 --- a/api/_Docs/v1/gfx/forwarding_complete.plantuml +++ b/api/_Docs/v1/gfx/forwarding_complete.plantuml @@ -32,4 +32,4 @@ KMS1 <-- QKDNController : AnnouncePayloadRelayResponse() User1 <-- KMS1 : PayloadExchangeResponse() KMS3 -> User2 : PayloadExchange() -@enduml \ No newline at end of file +@enduml diff --git a/api/_Docs/v1/gfx/forwarding_complete.png b/api/_Docs/v1/gfx/forwarding_complete.png index 2a907643d8f89162318408b8cb8c6fac7629d88f..ff4aae61baaac836b62117ad8b550a719823c9e3 100644 Binary files a/api/_Docs/v1/gfx/forwarding_complete.png and b/api/_Docs/v1/gfx/forwarding_complete.png differ diff --git a/api/_Docs/v1/gfx/key_sync.png b/api/_Docs/v1/gfx/key_sync.png index 2b2c8102b8622efefe5b4fae458f2c37ebb19090..743674e7f3be3ac52ebe9e150181b4260a22613a 100644 Binary files a/api/_Docs/v1/gfx/key_sync.png and b/api/_Docs/v1/gfx/key_sync.png differ diff --git a/api/ctrl/v1/health.proto b/api/ctrl/v1/health.proto new file mode 100644 index 0000000000000000000000000000000000000000..50cccfd7964330e0bb8da11b7f69c8efc3b66e8a --- /dev/null +++ b/api/ctrl/v1/health.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; +package ctrl.v1; + +service HealthCtrl { + rpc Health(HealthRequest) returns (HealthResponse); +} + +message HealthRequest { + string Message = 1; +} + +message HealthResponse { + string Message = 1; +} diff --git a/api/kms/v1/health.proto b/api/kms/v1/health.proto new file mode 100644 index 0000000000000000000000000000000000000000..c3d81907163c02662c404a7b4708ef4aba6d5853 --- /dev/null +++ b/api/kms/v1/health.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; +package kms.v1; + +service HealthKms { + rpc Health(HealthKmsRequest) returns (HealthKmsResponse); +} + +message HealthKmsRequest { + string Message = 1; +} + +message HealthKmsResponse { + string Message = 1; +} diff --git a/ctrl/cmd/main.go b/ctrl/cmd/main.go index f06940d7cfad9bc3a560e16bbfeceada4c248261..ecd9eb6d71c3e14e3b36e127ff8e6bc9d333ea98 100644 --- a/ctrl/cmd/main.go +++ b/ctrl/cmd/main.go @@ -1,26 +1,104 @@ package main import ( + "context" "flag" - "fmt" + "net" + "runtime/debug" config "code.fbi.h-da.de/danet/costaquanta/ctrl/internal" + "code.fbi.h-da.de/danet/costaquanta/ctrl/internal/core/application" + pb "code.fbi.h-da.de/danet/costaquanta/gen/go/ctrl/v1" + "code.fbi.h-da.de/danet/costaquanta/libs/logging" + "code.fbi.h-da.de/danet/costaquanta/libs/tracing" + "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/recovery" + "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) func main() { + log := logging.CreateProductionLogger("kms") + defer func() { + _ = log.Sync() + }() + logs := log.Sugar() + var bindAddr string flag.StringVar( &bindAddr, "bindaddr", "127.0.0.1:1337", - "default adress and port to bind the controller to", + "default adress and port to bind the controller interface to", ) flag.Parse() cfg := config.GetConfig() - fmt.Printf("%+v\n", cfg) - fmt.Printf("Binding to address %s", bindAddr) + logs.Infof("config: %+v", cfg) + logs.Infof("binding to address %s", bindAddr) + + listen, err := net.Listen("tcp", bindAddr) + if err != nil { + logs.Fatalf("failed to listen: %v", err) + } + + tracing.SetRuntimeSettings("kms") + traceProvider, err := tracing.GetTracer(context.Background(), tracing.Backend) + if err != nil { + logs.Fatal(err) + } + defer func() { + if err := traceProvider.Shutdown(context.Background()); err != nil { + logs.Infof("Error shutting down tracer provider: %v", err) + } + }() + + grpcPanicRecoveryHandler := func(p any) (err error) { + logs.Errorf("recovered from gRPC panic %+v; %v", p, debug.Stack()) + + return status.Errorf(codes.Internal, "%s", p) + } + + // var opts []grpc.ServerOption + // if *tls { + // if *certFile == "" { + // *certFile = data.Path("x509/server_cert.pem") + // } + // if *keyFile == "" { + // *keyFile = data.Path("x509/server_key.pem") + // } + // creds, err := credentials.NewServerTLSFromFile(*certFile, *keyFile) + // if err != nil { + // log.Fatalf("Failed to generate credentials: %v", err) + // } + // opts = []grpc.ServerOption{grpc.Creds(creds)} + // } + // grpcServer := grpc.NewServer(opts...) + + grpcServer := grpc.NewServer( + grpc.StatsHandler(otelgrpc.NewServerHandler()), + grpc.ChainUnaryInterceptor( + // srvMetrics.UnaryServerInterceptor(grpcprom.WithExemplarFromContext(exemplarFromContext)), + // logging.UnaryServerInterceptor(interceptorLogger(rpcLogger), logging.WithFieldsFromContext(logTraceID)), + // selector.UnaryServerInterceptor(auth.UnaryServerInterceptor(authFn), selector.MatchFunc(allButHealthZ)), + recovery.UnaryServerInterceptor(recovery.WithRecoveryHandler(grpcPanicRecoveryHandler)), + ), + grpc.ChainStreamInterceptor( + // srvMetrics.StreamServerInterceptor(grpcprom.WithExemplarFromContext(exemplarFromContext)), + // logging.StreamServerInterceptor(interceptorLogger(rpcLogger), logging.WithFieldsFromContext(logTraceID)), + // selector.StreamServerInterceptor(auth.StreamServerInterceptor(authFn), selector.MatchFunc(allButHealthZ)), + recovery.StreamServerInterceptor( + recovery.WithRecoveryHandler(grpcPanicRecoveryHandler), + ), + ), + ) + + app := application.NewApplication(log, traceProvider) + + pb.RegisterHealthCtrlServer(grpcServer, app.HealthServer) + _ = grpcServer.Serve(listen) } diff --git a/ctrl/internal/core/application/app.go b/ctrl/internal/core/application/app.go new file mode 100644 index 0000000000000000000000000000000000000000..17cbf6e2643bcfd23f3369944b03eaa32f878c30 --- /dev/null +++ b/ctrl/internal/core/application/app.go @@ -0,0 +1,24 @@ +package application + +import ( + "code.fbi.h-da.de/danet/costaquanta/ctrl/internal/infrastructure/interaction" + "code.fbi.h-da.de/danet/costaquanta/libs/logging" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.uber.org/zap" +) + +type Application struct { + tracer *sdktrace.TracerProvider + HealthServer *interaction.HealthServer +} + +func NewApplication(log *zap.Logger, tracer *sdktrace.TracerProvider) *Application { + healthServerLogger := logging.CreateChildLogger(log, "healthServer") + childTracer := tracer.Tracer("healthServer") + healthSrv := interaction.NewHealthServer(healthServerLogger, childTracer) + + return &Application{ + HealthServer: healthSrv, + tracer: tracer, + } +} diff --git a/ctrl/internal/infrastructure/interaction/health.go b/ctrl/internal/infrastructure/interaction/health.go new file mode 100644 index 0000000000000000000000000000000000000000..ffb554c87997ea8c5454a0042c9c397dfd1eb6af --- /dev/null +++ b/ctrl/internal/infrastructure/interaction/health.go @@ -0,0 +1,39 @@ +package interaction + +import ( + "context" + + pb "code.fbi.h-da.de/danet/costaquanta/gen/go/ctrl/v1" + "go.opentelemetry.io/otel/trace" + "go.uber.org/zap" +) + +type HealthServer struct { + pb.UnimplementedHealthCtrlServer + + logger *zap.SugaredLogger + tracer trace.Tracer +} + +func (h *HealthServer) Health( + ctx context.Context, + request *pb.HealthRequest, +) (*pb.HealthResponse, error) { + h.logger.Debugf("got health request %+v", request) + + _, span := h.tracer.Start(ctx, "Health") + defer span.End() + + return &pb.HealthResponse{ + Message: request.Message, + }, nil +} + +func NewHealthServer(logger *zap.SugaredLogger, tracer trace.Tracer) *HealthServer { + s := &HealthServer{ + logger: logger, + tracer: tracer, + } + + return s +} diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000000000000000000000000000000000000..393d21033ca9fd6f8de7a8b6a1fd7b46deb73fb5 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,9 @@ +services: + jaeger: + image: jaegertracing/all-in-one:latest + environment: + COLLECTOR_OTLP_ENABLED: true + ports: + - 127.0.0.1:16686:16686 + - 127.0.0.1:4317:4317 + - 127.0.0.1:4318:4318 diff --git a/docs/Logging.md b/docs/Logging.md new file mode 100644 index 0000000000000000000000000000000000000000..343596b9292beb756587ad70e68bdf9c22814f17 --- /dev/null +++ b/docs/Logging.md @@ -0,0 +1,65 @@ +# Logging + +We use [zap](https://github.com/uber-go/zap) as primary logging library. +It is [fast](https://github.com/uber-go/zap?tab=readme-ov-file#performance), provides structured logging out of the box and is convinent to use. + +## How to use zap + +We are (at least for now) defaulting to the suggared logger, so we get structured logs automatically, but don't have to manually configure every log +call. + +In the following are a few examples how to use zap: + +```go +log := zap.Must(zap.NewProduction()) + +defer log.Sync() + +logger := log.Sugar() + + +sugar.Infof( + "Hello from Zap at %s", + time.Now().Format("03:04 AM"), +) + +sugar.Infow("User logged in", + "username", "johndoe", + "userid", 123456, + zap.String("provider", "google"), +) +``` + +## Logger lib + +To make sure all components in the monorepo use the same logger settings, +we created a centralized logger lib in `libs/logger`. + +## Rules + +1. Each component **must** use the provided logger library. +2. Every service in a component **must** create it's own child logger. + This will make debugging much easier, as it can be seen at a glance which + component and which service in that component emitted the log. + + ```go + log := logger.CreateProductionLogger("kms") + defer log.Sync() + logs := log.Sugar() + + healthServerLogger := logger.CreateChildLogger(log, "healthServer") + healthServerLogger.Info("Hello") + ``` + + will result in the following log statement: + + ```json + { + "level": "info", + "timestamp": "2025-03-31T08:50:06.921+0200", + "caller": "cmd/main.go:37", + "msg": "Hello", + "component": "kms", + "service": "healthServer" + } + ``` diff --git a/docs/Tracing.md b/docs/Tracing.md new file mode 100644 index 0000000000000000000000000000000000000000..4f20d4313fe08a646a2fcf5dbfd53679600632d5 --- /dev/null +++ b/docs/Tracing.md @@ -0,0 +1,44 @@ +# Tracing + +We use [OpenTelemetry](https://opentelemetry.io/) to instrument our applications +with tracing support. +It was an deliberate decision to don't [auto-instrument](https://github.com/open-telemetry/opentelemetry-go-instrumentation/tree/main) +the applications to have more control about what flows must be instrumented. + +## Backend + +The current implementation supports two different exporters targeting two different +backends to export spans to. +At first there is the **Stdout** exporter, which exports spans to stdout. +The more interesting exporter is the **Backend** exporter, which can export +spans into external tools like [Jaeger](https://www.jaegertracing.io/). + +The exporter mode must be configured during set up of the tracer: + +```go +traceProvider, err := tracing.Init(tracing.Stdout) + or +traceProvider, err := tracing.Init(tracing.Backend) +``` + +This setup includes a all in one Jaeger instance that can be started with +`docker compose up -d` and then reached via [localhost:16686/search](http://localhost:16686/search). + +## Configuration + +To be able to work with traces a few specific configurations are needed for otel +to properly work and be useful at the end. +Therefore we introduced the `SetRuntimeSettings` method in the lib. +It makes sure to set for example the service name to be be able to visually +differentiate spans in a trace. + +## Testing + +The KMS is instrumented with tracing support on its `HealthKms/Health` service. +For now the traces are just emitted on stdout until we decided for a common +trace backend. + +```sh +grpcurl -plaintext -import-path api/ -proto api/kms/v1/health.proto localhost:1337 kms.v1.HealthKms/Health + +``` diff --git a/go.mod b/go.mod index bc042e58eed559848946e325c820f0fcd9f84703..5b33b9adeff6da8d81b55b777528810b71299e6f 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,36 @@ module code.fbi.h-da.de/danet/costaquanta go 1.24 -require github.com/caarlos0/env/v11 v11.3.1 +require ( + github.com/caarlos0/env/v11 v11.3.1 + github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.1 + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 + go.opentelemetry.io/otel v1.35.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 + go.opentelemetry.io/otel/sdk v1.35.0 + go.opentelemetry.io/otel/trace v1.35.0 + go.uber.org/zap v1.27.0 + google.golang.org/grpc v1.71.0 + google.golang.org/protobuf v1.36.5 +) + +require ( + github.com/cenkalti/backoff/v4 v4.3.0 // 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 + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/proto/otlp v1.5.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/net v0.35.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect +) diff --git a/go.sum b/go.sum index 1724948ebb6c354479037ecb2ccb4ccd25f4c3f0..34518bdc687361222561bf7619776bd786e946d4 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,67 @@ github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA= github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +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/go-grpc-middleware/v2 v2.3.1 h1:KcFzXwzM/kGhIRHvc8jdixfIJjVzuUJdnv+5xsPutog= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.1/go.mod h1:qOchhhIlmRcqk/O9uCo/puJlyo07YINaIqdZfZG3Jkc= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0 h1:T0Ec2E+3YZf5bgTNQVet8iTDW7oIk03tXHq+wkwIDnE= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.35.0/go.mod h1:30v2gqH+vYGJsesLWFov8u47EpYTcIQcBjKpI6pJThg= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= +go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= +go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ= +google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= +google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= +google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= diff --git a/justfile b/justfile new file mode 100644 index 0000000000000000000000000000000000000000..fe7514a1292fc42596127917d37acc75985034a0 --- /dev/null +++ b/justfile @@ -0,0 +1,29 @@ +golangci-lint-version := "v2.0.2" +tools-install-path := "bin" + +run-kms: + go run kms/cmd/main.go + +run-ctrl: + go run ctrl/cmd/main.go + +build-api: + buf generate + +lint: + docker run --rm -t -v $(pwd):/app -w /app \ + --user $(id -u):$(id -g) \ + -v $(go env GOCACHE):/.cache/go-build -e GOCACHE=/.cache/go-build \ + -v $(go env GOMODCACHE):/.cache/mod -e GOMODCACHE=/.cache/mod \ + -v ~/.cache/golangci-lint:/.cache/golangci-lint -e GOLANGCI_LINT_CACHE=/.cache/golangci-lint \ + golangci/golangci-lint:{{ golangci-lint-version }} golangci-lint run + +ci-lint: + mkdir -p {{ tools-install-path }} + export GOBIN={{ tools-install-path }} + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s {{ golangci-lint-version }} + ./{{ tools-install-path }}/golangci-lint run + +generate-plantuml: + echo "Generating PlantUML diagrams if plantUML is installed..." + plantuml -o . api/_Docs/*/**.plantuml diff --git a/kms/cmd/main.go b/kms/cmd/main.go index b6cd3a5e6ae5141030c07b00484bb723e287c8eb..37392a437938ba79632894c5bce49ed7e251709b 100644 --- a/kms/cmd/main.go +++ b/kms/cmd/main.go @@ -1,13 +1,30 @@ package main import ( + "context" "flag" - "fmt" + "net" + "runtime/debug" + pb "code.fbi.h-da.de/danet/costaquanta/gen/go/kms/v1" config "code.fbi.h-da.de/danet/costaquanta/kms/internal" + "code.fbi.h-da.de/danet/costaquanta/kms/internal/core/application" + "code.fbi.h-da.de/danet/costaquanta/libs/logging" + "code.fbi.h-da.de/danet/costaquanta/libs/tracing" + "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/recovery" + "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) func main() { + log := logging.CreateProductionLogger("kms") + defer func() { + _ = log.Sync() + }() + logs := log.Sugar() + var bindAddr string var ctrlAddr string @@ -28,6 +45,67 @@ func main() { cfg := config.GetConfig() - fmt.Printf("%+v\n", cfg) - fmt.Printf("Binding to address %s", bindAddr) + logs.Infof("config: %+v", cfg) + logs.Infof("binding to address %s", bindAddr) + + listen, err := net.Listen("tcp", bindAddr) + if err != nil { + logs.Fatalf("failed to listen: %v", err) + } + + tracing.SetRuntimeSettings("kms") + traceProvider, err := tracing.GetTracer(context.Background(), tracing.Backend) + if err != nil { + logs.Fatal(err) + } + defer func() { + if err := traceProvider.Shutdown(context.Background()); err != nil { + logs.Infof("Error shutting down tracer provider: %v", err) + } + }() + + grpcPanicRecoveryHandler := func(p any) (err error) { + logs.Errorf("recovered from gRPC panic %+v; %v", p, debug.Stack()) + + return status.Errorf(codes.Internal, "%s", p) + } + + // var opts []grpc.ServerOption + // if *tls { + // if *certFile == "" { + // *certFile = data.Path("x509/server_cert.pem") + // } + // if *keyFile == "" { + // *keyFile = data.Path("x509/server_key.pem") + // } + // creds, err := credentials.NewServerTLSFromFile(*certFile, *keyFile) + // if err != nil { + // log.Fatalf("Failed to generate credentials: %v", err) + // } + // opts = []grpc.ServerOption{grpc.Creds(creds)} + // } + // grpcServer := grpc.NewServer(opts...) + + grpcServer := grpc.NewServer( + grpc.StatsHandler(otelgrpc.NewServerHandler()), + grpc.ChainUnaryInterceptor( + // srvMetrics.UnaryServerInterceptor(grpcprom.WithExemplarFromContext(exemplarFromContext)), + // logging.UnaryServerInterceptor(interceptorLogger(rpcLogger), logging.WithFieldsFromContext(logTraceID)), + // selector.UnaryServerInterceptor(auth.UnaryServerInterceptor(authFn), selector.MatchFunc(allButHealthZ)), + recovery.UnaryServerInterceptor(recovery.WithRecoveryHandler(grpcPanicRecoveryHandler)), + ), + grpc.ChainStreamInterceptor( + // srvMetrics.StreamServerInterceptor(grpcprom.WithExemplarFromContext(exemplarFromContext)), + // logging.StreamServerInterceptor(interceptorLogger(rpcLogger), logging.WithFieldsFromContext(logTraceID)), + // selector.StreamServerInterceptor(auth.StreamServerInterceptor(authFn), selector.MatchFunc(allButHealthZ)), + recovery.StreamServerInterceptor( + recovery.WithRecoveryHandler(grpcPanicRecoveryHandler), + ), + ), + ) + + app := application.NewApplication(log, traceProvider) + + pb.RegisterHealthKmsServer(grpcServer, app.HealthServer) + _ = grpcServer.Serve(listen) } diff --git a/kms/internal/core/application/app.go b/kms/internal/core/application/app.go new file mode 100644 index 0000000000000000000000000000000000000000..eac591d4b97973e7b2cdabba6bc50a2fc01567d5 --- /dev/null +++ b/kms/internal/core/application/app.go @@ -0,0 +1,24 @@ +package application + +import ( + "code.fbi.h-da.de/danet/costaquanta/kms/internal/infrastructure/interaction" + "code.fbi.h-da.de/danet/costaquanta/libs/logging" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.uber.org/zap" +) + +type Application struct { + tracer *sdktrace.TracerProvider + HealthServer *interaction.HealthServer +} + +func NewApplication(log *zap.Logger, tracer *sdktrace.TracerProvider) *Application { + healthServerLogger := logging.CreateChildLogger(log, "healthServer") + childTracer := tracer.Tracer("healthServer") + healthSrv := interaction.NewHealthServer(healthServerLogger, childTracer) + + return &Application{ + HealthServer: healthSrv, + tracer: tracer, + } +} diff --git a/kms/internal/core/ports/interaction.go b/kms/internal/core/ports/interaction.go new file mode 100644 index 0000000000000000000000000000000000000000..808de880458d99a7446599504e73a6c7ff0c94dc --- /dev/null +++ b/kms/internal/core/ports/interaction.go @@ -0,0 +1 @@ +package ports diff --git a/kms/internal/infrastructure/interaction/health.go b/kms/internal/infrastructure/interaction/health.go new file mode 100644 index 0000000000000000000000000000000000000000..d97bbe7d2a2847ef2c36ce8f000c65b423a1b91e --- /dev/null +++ b/kms/internal/infrastructure/interaction/health.go @@ -0,0 +1,39 @@ +package interaction + +import ( + "context" + + pb "code.fbi.h-da.de/danet/costaquanta/gen/go/kms/v1" + "go.opentelemetry.io/otel/trace" + "go.uber.org/zap" +) + +type HealthServer struct { + pb.UnimplementedHealthKmsServer + + logger *zap.SugaredLogger + tracer trace.Tracer +} + +func (h *HealthServer) Health( + ctx context.Context, + request *pb.HealthKmsRequest, +) (*pb.HealthKmsResponse, error) { + h.logger.Debugf("got health request %+v", request) + + _, span := h.tracer.Start(ctx, "Health") + defer span.End() + + return &pb.HealthKmsResponse{ + Message: request.Message, + }, nil +} + +func NewHealthServer(logger *zap.SugaredLogger, tracer trace.Tracer) *HealthServer { + s := &HealthServer{ + logger: logger, + tracer: tracer, + } + + return s +} diff --git a/libs/logging/logging.go b/libs/logging/logging.go new file mode 100644 index 0000000000000000000000000000000000000000..e195799807f22c66480c20133d5e2943d6db597a --- /dev/null +++ b/libs/logging/logging.go @@ -0,0 +1,41 @@ +package logging + +import ( + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +func CreateProductionLogger(component string) *zap.Logger { + encoderCfg := zap.NewProductionEncoderConfig() + encoderCfg.TimeKey = "timestamp" + encoderCfg.EncodeTime = zapcore.ISO8601TimeEncoder + + config := zap.Config{ + Level: zap.NewAtomicLevelAt(zap.InfoLevel), + Development: false, + DisableCaller: false, + DisableStacktrace: false, + Sampling: nil, + Encoding: "json", + EncoderConfig: encoderCfg, + OutputPaths: []string{ + "stderr", + }, + ErrorOutputPaths: []string{ + "stderr", + }, + InitialFields: map[string]interface{}{ + "component": component, + }, + } + + return zap.Must(config.Build()) +} + +func CreateChildLogger(logger *zap.Logger, service string) *zap.SugaredLogger { + childLogger := logger.With( + zap.String("service", service), + ) + + return childLogger.Sugar() +} diff --git a/libs/tracing/tracing.go b/libs/tracing/tracing.go new file mode 100644 index 0000000000000000000000000000000000000000..87c49326c84a6c452a30114b068e51c25ecf01da --- /dev/null +++ b/libs/tracing/tracing.go @@ -0,0 +1,61 @@ +package tracing + +import ( + "context" + "fmt" + "os" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + stdout "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" + "go.opentelemetry.io/otel/propagation" + sdktrace "go.opentelemetry.io/otel/sdk/trace" +) + +type TraceExportTarget int + +const ( + Stdout TraceExportTarget = iota + Backend +) + +func GetTracer(ctx context.Context, target TraceExportTarget) (*sdktrace.TracerProvider, error) { + var exporter sdktrace.SpanExporter + var err error + + switch target { + case Stdout: + exporter, err = stdout.New(stdout.WithPrettyPrint()) + if err != nil { + return nil, err + } + case Backend: + exporter, err = otlptracehttp.New( + ctx, + otlptracehttp.WithInsecure(), + otlptracehttp.WithEndpoint("127.0.0.1:4318"), + ) + if err != nil { + return nil, fmt.Errorf("failed to create trace exporter: %w", err) + } + + } + + tp := sdktrace.NewTracerProvider( + sdktrace.WithSampler(sdktrace.AlwaysSample()), + sdktrace.WithBatcher(exporter), + ) + otel.SetTracerProvider(tp) + otel.SetTextMapPropagator( + propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + propagation.Baggage{}, + ), + ) + + return tp, nil +} + +func SetRuntimeSettings(serviceName string) { + _ = os.Setenv("OTEL_SERVICE_NAME", serviceName) +}