From 8198e8cfee39adbbe2d314fb32ede82139a27e1d Mon Sep 17 00:00:00 2001 From: Ryan McGuire Date: Sat, 4 Jul 2026 20:29:29 +0000 Subject: [PATCH] Initial commit --- .air.toml | 36 ++ .dockerignore | 4 + .gitea/workflows/ci.yml | 146 +++++ .gitignore | 28 + .vscode/launch.json | 16 + CHANGELOG.md | 18 + Dockerfile | 27 + LICENSE | 9 + Makefile | 96 ++++ README.md | 74 +++ TODO.md | 10 + api/demo/app/v1alpha1/app.pb.go | 212 ++++++++ api/demo/app/v1alpha1/app.pb.gw.go | 165 ++++++ api/demo/app/v1alpha1/app_grpc.pb.go | 119 +++++ buf.gen.yaml | 28 + buf.lock | 12 + buf.yaml | 14 + config.yaml | 30 ++ contrib/go-server-with-otel.swagger.json | 99 ++++ contrib/mcpinspector.json | 8 + contrib/schema.json | 154 ++++++ env-sample | 14 + go.mod | 79 +++ go.sum | 186 +++++++ helm/.gitignore | 2 + helm/Chart.yaml | 30 ++ helm/library-hull.yaml | 643 +++++++++++++++++++++++ helm/templates/app.yaml | 1 + helm/values.yaml | 195 +++++++ main.go | 145 +++++ pkg/config/custom.go | 31 ++ pkg/demo/demo.go | 58 ++ pkg/demo/demogrpc/demo.go | 86 +++ pkg/demo/demogrpc/fact.go | 25 + pkg/demo/demogrpc/server.go | 30 ++ pkg/demo/demohttp/server.go | 44 ++ pkg/demo/demomcp/server.go | 70 +++ pkg/demo/demomcp/tool.go | 60 +++ pkg/service/service.go | 63 +++ proto/demo/app/v1alpha1/app.proto | 29 + 40 files changed, 3096 insertions(+) create mode 100644 .air.toml create mode 100644 .dockerignore create mode 100644 .gitea/workflows/ci.yml create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 CHANGELOG.md create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 TODO.md create mode 100644 api/demo/app/v1alpha1/app.pb.go create mode 100644 api/demo/app/v1alpha1/app.pb.gw.go create mode 100644 api/demo/app/v1alpha1/app_grpc.pb.go create mode 100644 buf.gen.yaml create mode 100644 buf.lock create mode 100644 buf.yaml create mode 100644 config.yaml create mode 100644 contrib/go-server-with-otel.swagger.json create mode 100644 contrib/mcpinspector.json create mode 100644 contrib/schema.json create mode 100644 env-sample create mode 100644 go.mod create mode 100644 go.sum create mode 100644 helm/.gitignore create mode 100644 helm/Chart.yaml create mode 100644 helm/library-hull.yaml create mode 100644 helm/templates/app.yaml create mode 100644 helm/values.yaml create mode 100644 main.go create mode 100644 pkg/config/custom.go create mode 100644 pkg/demo/demo.go create mode 100644 pkg/demo/demogrpc/demo.go create mode 100644 pkg/demo/demogrpc/fact.go create mode 100644 pkg/demo/demogrpc/server.go create mode 100644 pkg/demo/demohttp/server.go create mode 100644 pkg/demo/demomcp/server.go create mode 100644 pkg/demo/demomcp/tool.go create mode 100644 pkg/service/service.go create mode 100644 proto/demo/app/v1alpha1/app.proto diff --git a/.air.toml b/.air.toml new file mode 100644 index 0000000..6bf0429 --- /dev/null +++ b/.air.toml @@ -0,0 +1,36 @@ +# .air.toml + +# Root directory of the project +root = "." + +# Directory for temporary build artifacts +tmp_dir = "tmp" + +[build] +cmd = "go build -o tmp/main ." +bin = "tmp/main" +args_bin = ["-config", "config.yaml"] + +# Extensions to watch +include_ext = ["go", "toml", "yaml", "yml"] + +# Ignore temp folders and irrelevant dirs +exclude_dir = ["tmp", "vendor", ".git", "cmd", "proto"] +exclude_regex = ["_test.go"] + +# Stop process on error and send interrupt for graceful shutdown +stop_on_error = true +send_interrupt = true +kill_delay = 500 + +[log] +time = true + +[color] +main = "magenta" +watcher = "cyan" +build = "yellow" +runner = "green" + +[misc] +clean_on_exit = true diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b42f428 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +.git +config.y*ml +go.work* +docker-compose-sample* diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..deb7dba --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,146 @@ +name: Build and Publish +on: + push: + tags: ["v*"] + branches: ["main"] + +env: + PACKAGE_NAME: go-server-with-otel + BINARY_PATH: bin + BINARY_NAME: go-server-with-otel + GO_MOD_PATH: gitea.libretechconsulting.com/rmcguire/go-server-with-otel + GO_GIT_HOST: gitea.libretechconsulting.com + VER_PKG: gitea.libretechconsulting.com/rmcguire/go-app/pkg/config.Version + VERSION: ${{ github.ref_name }} + PLATFORMS: linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 + DOCKER_REGISTRY: gitea.libretechconsulting.com + DOCKER_USER: rmcguire + DOCKER_REPO: rmcguire/go-server-with-otel + DOCKER_IMG: ${{ env.DOCKER_REGISTRY }}/${{ env.DOCKER_REPO }} + CHART_DIR: helm/ + +jobs: + go-binaries: + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/v') # Only run on tag push + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up Go Environment + uses: actions/setup-go@v4 + with: + go-version: '1.24' + + - name: Build Binary + run: make build + + - name: Upload Binaries to Generic Registry + env: + API_TOKEN: ${{ secrets.API_TOKEN }} + run: | + for platform in $PLATFORMS; do + OS=$(echo $platform | cut -d/ -f1) + ARCH=$(echo $platform | cut -d/ -f2) + BINARY_FILE="${BINARY_PATH}/${PACKAGE_NAME}-${OS}-${ARCH}" + echo "Uploading $BINARY_FILE" + if [ -f "$BINARY_FILE" ]; then + curl -X PUT \ + -H "Authorization: token ${API_TOKEN}" \ + --upload-file "$BINARY_FILE" \ + "${GITHUB_SERVER_URL}/api/packages/${GITHUB_REPOSITORY_OWNER}/generic/${PACKAGE_NAME}/${{ github.ref_name }}/${PACKAGE_NAME}-${OS}-${ARCH}" + else + echo "Error: Binary $BINARY_FILE not found." + exit 1 + fi + done + + - name: Run Go List + continue-on-error: true + env: + TAG_NAME: ${{ github.ref_name }} # Use the pushed tag name + run: | + if [[ "$TAG_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + GOPROXY=proxy.golang.org go list -m ${GO_GIT_HOST}/${GITHUB_REPOSITORY}@$TAG_NAME + else + echo "Error: Invalid tag format '$TAG_NAME'. Expected 'vX.X.X'." + exit 1 + fi + + container-images: + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/v') # Only run on tag push + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Custom Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ env.DOCKER_USER }} + password: ${{ secrets.API_TOKEN }} + + - name: Build and Push Docker Image + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: | + ${{ env.DOCKER_IMG }}:${{ github.ref_name }} + ${{ env.DOCKER_IMG }}:latest + build-args: | + VER_PKG=${{ env.VER_PKG }} + VERSION=${{ github.ref_name }} + + # Detect if the helm chart was updated + check-chart: + runs-on: ubuntu-latest + outputs: + chart-updated: ${{ steps.filter.outputs.chart }} + steps: + - uses: actions/checkout@v4 + - name: Check Chart Changed + uses: dorny/paths-filter@v3 + id: filter + with: + base: ${{ github.ref }} + filters: | + chart: + - helm/Chart.yaml + + helm-release: + runs-on: ubuntu-latest + needs: check-chart + if: ${{ needs.check-chart.outputs.chart-updated == 'true' }} + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Install Helm + env: + BINARY_NAME: helm + run: | + curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash + + - name: Fetch Dependencies + run: | + cd ${CHART_DIR} && \ + helm repo add hull https://vidispine.github.io/hull && \ + helm dep build + + - name: Package Chart + run: | + helm package --app-version ${VERSION} ${CHART_DIR} + + - name: Publish Chart + env: + API_TOKEN: ${{ secrets.API_TOKEN }} + run: | + curl -X POST \ + -H "Authorization: token ${API_TOKEN}" \ + --upload-file ./${PACKAGE_NAME}-*.tgz \ + https://gitea.libretechconsulting.com/api/packages/${GITHUB_REPOSITORY_OWNER}/helm/api/charts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e24201d --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# ---> Go +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work* + +# Environment +.env + +bin/* +tmp diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..b79f986 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://raw.githubusercontent.com/mfussenegger/dapconfig-schema/master/dapconfig-schema.json", + "version": "0.2.0", + "configurations": [ + { + "name": "Run Server", + "type": "delve", + "request": "launch", + "program": "${workspaceFolder}", + "env": { + "APP_NAME": "Go HTTP with OTEL" + }, + "args": [] + } + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5299870 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,18 @@ +# v0.7.0 +* feat: Add HTTP log exclusion regex paths to configuration schema. +* chore: Update Go version to 1.25. +* chore: Update module dependencies. + +# v0.6.0 +* feat: Introduce Model Context Protocol (MCP) server with a demo random fact tool. +* feat: Add MCP server configuration file (`contrib/mcpinspector.json`). +* deps: Update Go module dependencies, including `bufbuild/protovalidate`, `go-app`, `grpc-gateway`, `googleapis/api`, `grpc`, `protobuf`, `golang.org/x/exp`, and `googleapis/rpc`. +* deps: Add new Go module dependencies: `modelcontextprotocol/go-sdk` and `k8s.io/utils`. + +# v0.5.0 + +* Added OpenTelemetry tracing for application startup. +* Updated Go module dependencies, including `protovalidate`, `go-app`, `grpc`, and OpenTelemetry related packages. +* Enhanced `Makefile` `rename` target to update the application name constant in `main.go`. +* Configured `air` live-reloading to exclude the `proto` directory. +* Refactored application initialization logic for improved modularity and OpenTelemetry integration. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c3c81d5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +FROM golang:1-alpine AS build +WORKDIR /app + +ENV GO111MODULE=auto CGO_ENABLED=0 GOOS=linux + +ARG GOPROXY +ARG GONOSUMDB=gitea.libretechconsulting.com +ARG VER_PKG=gitea.libretechconsulting.com/rmcguire/go-app/pkg/config.Version +ARG VERSION=(devel) +ARG APP_NAME=go-server-with-otel + +COPY ./go.mod ./go.sum ./ +RUN go mod download + +COPY ./ /app +RUN go build -C . -v -ldflags "-extldflags '-static' -X ${VER_PKG}=${VERSION}" -o ${APP_NAME} . + +FROM alpine:latest + +ARG APP_NAME=go-server-with-otel + +WORKDIR /app +USER 100:101 + +COPY --from=build --chown=100:101 /app/${APP_NAME} /app/app + +ENTRYPOINT [ "/app/app" ] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4d42a02 --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2025 rmcguire + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f597875 --- /dev/null +++ b/Makefile @@ -0,0 +1,96 @@ +.PHONY: all test build docker install clean proto check_protoc + +CMD_NAME := go-server-with-otel +VERSION ?= development +API_DIR := api/ +SCHEMA_DIR := contrib/ +PROTO_DIRS := $(wildcard proto/demo/app/*) # TODO: Update path (probably not demo) +PLATFORMS := linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 +OUTPUT_DIR := bin +VER_PKG := gitea.libretechconsulting.com/rmcguire/go-app/pkg/config.Version +GIT_REPO := gitea.libretechconsulting.com/rmcguire/go-server-with-otel + +all: proto test build docker + +proto: check_buf + buf dep update + buf generate + +test: + go test -v ./... + +build: test + @echo "Building for platforms: $(PLATFORMS)" + @for platform in $(PLATFORMS); do \ + OS=$$(echo $$platform | cut -d/ -f1); \ + ARCH=$$(echo $$platform | cut -d/ -f2); \ + OUTPUT="$(OUTPUT_DIR)/$(CMD_NAME)-$$OS-$$ARCH"; \ + mkdir -vp $(OUTPUT_DIR); \ + echo "Building for $$platform into $$OUTPUT"; \ + GOOS=$$OS GOARCH=$$ARCH go build -ldflags "-X $(VER_PKG)=$(VERSION)" -o $$OUTPUT .; \ + echo "Built $$OUTPUT"; \ + done + go build -ldflags "-X $(VER_PKG)=$(VERSION)" -o bin/${CMD_NAME} + +schema: + go run . -schema > contrib/schema.json + +docker: + @echo "Building Docker image $(GIT_REPO):$(VERSION)" + docker build \ + --build-arg VER_PKG=$(VER_PKG) \ + --build-arg VERSION=$(VERSION) \ + --build-arg APP_NAME=$(CMD_NAME) \ + -t $(GIT_REPO):$(VERSION) . + docker push $(GIT_REPO):$(VERSION) + +install: + go install -v -ldflags "-X $(VER_PKG)=$(VERSION)" . + +clean: + rm -rf bin/${CMD_NAME} + +check_buf: + @if ! command -v buf > /dev/null; then \ + echo "Error: buf not found in PATH"; \ + exit 1; \ + fi + +rename: + @echo "Current module path: $(GIT_REPO)" + @echo "Usage: make rename NAME=your/new/module/name APP=your-app-name" + @if [ -z "$(NAME)" ]; then \ + echo "No package name provided. Aborting."; \ + exit 1; \ + fi + @if [ -z "$(APP)" ]; then \ + echo "No app name provided. Aborting."; \ + exit 1; \ + fi + @echo "New name: app=$(APP) pkg=$(NAME)" + @echo "Are you sure you want to proceed? (y/N): " && read CONFIRM && if [ "$$CONFIRM" != "y" ] && [ "$$CONFIRM" != "Y" ]; then \ + echo "Aborted."; \ + exit 1; \ + fi + @sed -i "s|APP_NAME=.*|APP_NAME=$(APP)|g" Dockerfile + @sed -i "s|^CMD_NAME := .*|CMD_NAME := $(APP)|g" Makefile + @sed -i "s|merge_file_name=.*|merge_file_name=$(APP)|g" buf.gen.yaml + @sed -i "s|^name: .*|name: $(APP)|g" helm/Chart.yaml + @sed -i "s|otelServiceName: .*|otelServiceName: $(APP)|g" helm/values.yaml + @sed -i "s|= \"demo-app\"|= \"$(APP)\"|g" main.go + @sed -i "s|app=.*|app=$(APP)|g" helm/values.yaml + @sed -i "s|$(CMD_NAME)|$(APP)|g" README.md + @sed -i "s|$(CMD_NAME)|$(APP)|g" .gitea/workflows/ci.yml + @sed -i "s|$(GIT_REPO)|$(NAME)|g" .gitea/workflows/ci.yml + @rm contrib/$(CMD_NAME).swagger.json + @find . -type f -a \ + \( -name '*.go' -o -name 'go.mod' \ + -o -name 'go.sum' -o -name '*.proto' \ + -o -name 'Makefile' \ + -o -name '*.yml' -o -name '*.yaml' \ + \) \ + -not -path './.git' -not -path './.git/*' \ + -exec sed -i "s|$(GIT_REPO)|$(NAME)|g" {} + + @echo "\n* Project renamed to $(NAME)" + @echo "* NOTE: You will have to update .gitea/workflows/ci.yml" + @echo "* NOTE: You will have to run buf generate" diff --git a/README.md b/README.md new file mode 100644 index 0000000..57bc773 --- /dev/null +++ b/README.md @@ -0,0 +1,74 @@ +# go-server-with-otel 🚀 + +A powerful and flexible template for building Go HTTP + GRPC servers with full OpenTelemetry (OTEL) support. +Bootstrapped with the go-app framework to provide all the bells and whistles right out of the box. +Ideal for rapidly creating production-ready microservices. + +Check out the [go-app framework](https://gitea.libretechconsulting.com/rmcguire/go-app) for more detail there. + +## 🌟 Features + +- **📈 OpenTelemetry (OTEL) Metrics & Traces** – Comprehensive observability with built-in support for metrics and traces. +- 📝 Logging with Zerolog – High-performance structured logging with zerolog for ultra-fast, leveled logging. +- 💻 Local dev with air pre-configured (just run `air`) +- **💬 GRPC + GRPC-Gateway** – Supports RESTful JSON APIs alongside gRPC with auto-generated Swagger (OpenAPI2) specs. +- 🌐 HTTP and GRPC Middleware – Flexible middleware support for HTTP and GRPC to enhance request handling, authentication, and observability. +- **📦 Multi-Arch Builds** – Robust Makefile that supports building for multiple architectures (amd64, arm64, etc.). +- **🐳 Docker Image Generation** – Easily build Docker images using `make docker`. +- **📜 Config Schema Generation** – Automatically generate JSON schemas for configuration validation. +- **📝 Proto Compilation** – Seamlessly compile .proto files with `make proto`. +- **🔄 Project Renaming** – Easily rename your project using `make rename NAME=your.gitremote.com/pathto/repo`. +- **📦 Helm Chart** – Deploy your application with Kubernetes using the provided Helm chart. +- **🤖 Gitea CI Integration** – Out-of-the-box Gitea CI pipeline configuration with `.gitea/workflows/ci.yaml`. +- **⚙️ Expandable Configuration** – Extend your app-specific configuration with go-app's built-in logging, HTTP, and GRPC config support. + +--- + +## 📚 Getting Started + +1. Install tools: + - Install make, protoc, and go using brew, apt, etc.. + - Install protoc plugins (provided in go.mod tool()): + - `go get -v tool && go install -v tool` + +1. **Rename your package:** + ```sh + make rename NAME=my.gitremote.com/pathto/repo + ``` + +1. **Review the config struct:** + Update and customize your app-specific configuration. This merges with go-app's configuration, providing logging, HTTP, and GRPC config for free. + +1. **Generate a new JSON schema:** + ```sh + make schema + ``` + - Ensure your structs have `yaml` and `json` tags. + - With the `yaml-language-server` LSP plugin, the schema will be auto-detected in your `config.yaml`. + +1. **Compile proto files:** + ```sh + make proto + ``` + - Add paths under `proto/` as necessary. + +1. **Implement your application logic.** + +1. **Update Gitea CI configuration:** + Modify parameters in `.gitea/workflows/ci.yaml` as needed. + +1. Tag your release: `git tag v0.1.0` and push `git push --tags` + +--- + +## 📂 Project Structure +- `proto/` - Protobuf definitions and generated files. +- `api/` - Auto-generated code for proto, grpc-gateway, and OpenAPI2 spec +- `pkg/config/` - Custom config, merged with go-app configuration +- `pkg/demo(http|grpc)/` - HTTP and GRPC server implementations +- `helm/` - Helm chart for deploying your application to Kubernetes. +- `.gitea/workflows/` - CI pipelines for automated builds and tests. + +--- + +## 🔥 Ready to build something awesome? Let's go! 🎉 diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..c73fc5f --- /dev/null +++ b/TODO.md @@ -0,0 +1,10 @@ +# Demo app TODO + +- [x] Create generic interface for implenting a service +- [x] Create config sample not called demo, so it is more easily reused +- [x] Update README for tagging/versioning/pipeline info +- [x] Update README for detail on installing protoc tools and make +- [x] Rename project +- [x] Finish grpc sample implementation +- [x] Add Dockerfile +- [x] Add gitea CI diff --git a/api/demo/app/v1alpha1/app.pb.go b/api/demo/app/v1alpha1/app.pb.go new file mode 100644 index 0000000..a8dd4f9 --- /dev/null +++ b/api/demo/app/v1alpha1/app.pb.go @@ -0,0 +1,212 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.6 +// protoc (unknown) +// source: demo/app/v1alpha1/app.proto + +package demo + +import ( + _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" + _ "google.golang.org/genproto/googleapis/api/annotations" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Options for random fact, in this case +// just a language +type GetDemoRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Language *string `protobuf:"bytes,1,opt,name=language,proto3,oneof" json:"language,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetDemoRequest) Reset() { + *x = GetDemoRequest{} + mi := &file_demo_app_v1alpha1_app_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetDemoRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetDemoRequest) ProtoMessage() {} + +func (x *GetDemoRequest) ProtoReflect() protoreflect.Message { + mi := &file_demo_app_v1alpha1_app_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetDemoRequest.ProtoReflect.Descriptor instead. +func (*GetDemoRequest) Descriptor() ([]byte, []int) { + return file_demo_app_v1alpha1_app_proto_rawDescGZIP(), []int{0} +} + +func (x *GetDemoRequest) GetLanguage() string { + if x != nil && x.Language != nil { + return *x.Language + } + return "" +} + +// Returns a randome fact, because this is a demo app +// so what else do we do? +type GetDemoResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Timestamp *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + Fact string `protobuf:"bytes,2,opt,name=fact,proto3" json:"fact,omitempty"` + Source string `protobuf:"bytes,3,opt,name=source,proto3" json:"source,omitempty"` + Language string `protobuf:"bytes,4,opt,name=language,proto3" json:"language,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetDemoResponse) Reset() { + *x = GetDemoResponse{} + mi := &file_demo_app_v1alpha1_app_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetDemoResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetDemoResponse) ProtoMessage() {} + +func (x *GetDemoResponse) ProtoReflect() protoreflect.Message { + mi := &file_demo_app_v1alpha1_app_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetDemoResponse.ProtoReflect.Descriptor instead. +func (*GetDemoResponse) Descriptor() ([]byte, []int) { + return file_demo_app_v1alpha1_app_proto_rawDescGZIP(), []int{1} +} + +func (x *GetDemoResponse) GetTimestamp() *timestamppb.Timestamp { + if x != nil { + return x.Timestamp + } + return nil +} + +func (x *GetDemoResponse) GetFact() string { + if x != nil { + return x.Fact + } + return "" +} + +func (x *GetDemoResponse) GetSource() string { + if x != nil { + return x.Source + } + return "" +} + +func (x *GetDemoResponse) GetLanguage() string { + if x != nil { + return x.Language + } + return "" +} + +var File_demo_app_v1alpha1_app_proto protoreflect.FileDescriptor + +const file_demo_app_v1alpha1_app_proto_rawDesc = "" + + "\n" + + "\x1bdemo/app/v1alpha1/app.proto\x12\x11demo.app.v1alpha1\x1a\x1bbuf/validate/validate.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"G\n" + + "\x0eGetDemoRequest\x12(\n" + + "\blanguage\x18\x01 \x01(\tB\a\xbaH\x04r\x02\x10\x02H\x00R\blanguage\x88\x01\x01B\v\n" + + "\t_language\"\x9c\x01\n" + + "\x0fGetDemoResponse\x128\n" + + "\ttimestamp\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampR\ttimestamp\x12\x12\n" + + "\x04fact\x18\x02 \x01(\tR\x04fact\x12\x16\n" + + "\x06source\x18\x03 \x01(\tR\x06source\x12#\n" + + "\blanguage\x18\x04 \x01(\tB\a\xbaH\x04r\x02\x10\x02R\blanguage2z\n" + + "\x0eDemoAppService\x12h\n" + + "\aGetDemo\x12!.demo.app.v1alpha1.GetDemoRequest\x1a\".demo.app.v1alpha1.GetDemoResponse\"\x16\x82\xd3\xe4\x93\x02\x10\x12\x0e/v1alpha1/demoB\xd5\x01\n" + + "\x15com.demo.app.v1alpha1B\bAppProtoP\x01ZLgitea.libretechconsulting.com/rmcguire/go-server-with-otel/api/v1alpha1/demo\xa2\x02\x03DAX\xaa\x02\x11Demo.App.V1alpha1\xca\x02\x11Demo\\App\\V1alpha1\xe2\x02\x1dDemo\\App\\V1alpha1\\GPBMetadata\xea\x02\x13Demo::App::V1alpha1b\x06proto3" + +var ( + file_demo_app_v1alpha1_app_proto_rawDescOnce sync.Once + file_demo_app_v1alpha1_app_proto_rawDescData []byte +) + +func file_demo_app_v1alpha1_app_proto_rawDescGZIP() []byte { + file_demo_app_v1alpha1_app_proto_rawDescOnce.Do(func() { + file_demo_app_v1alpha1_app_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_demo_app_v1alpha1_app_proto_rawDesc), len(file_demo_app_v1alpha1_app_proto_rawDesc))) + }) + return file_demo_app_v1alpha1_app_proto_rawDescData +} + +var file_demo_app_v1alpha1_app_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_demo_app_v1alpha1_app_proto_goTypes = []any{ + (*GetDemoRequest)(nil), // 0: demo.app.v1alpha1.GetDemoRequest + (*GetDemoResponse)(nil), // 1: demo.app.v1alpha1.GetDemoResponse + (*timestamppb.Timestamp)(nil), // 2: google.protobuf.Timestamp +} +var file_demo_app_v1alpha1_app_proto_depIdxs = []int32{ + 2, // 0: demo.app.v1alpha1.GetDemoResponse.timestamp:type_name -> google.protobuf.Timestamp + 0, // 1: demo.app.v1alpha1.DemoAppService.GetDemo:input_type -> demo.app.v1alpha1.GetDemoRequest + 1, // 2: demo.app.v1alpha1.DemoAppService.GetDemo:output_type -> demo.app.v1alpha1.GetDemoResponse + 2, // [2:3] is the sub-list for method output_type + 1, // [1:2] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_demo_app_v1alpha1_app_proto_init() } +func file_demo_app_v1alpha1_app_proto_init() { + if File_demo_app_v1alpha1_app_proto != nil { + return + } + file_demo_app_v1alpha1_app_proto_msgTypes[0].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_demo_app_v1alpha1_app_proto_rawDesc), len(file_demo_app_v1alpha1_app_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_demo_app_v1alpha1_app_proto_goTypes, + DependencyIndexes: file_demo_app_v1alpha1_app_proto_depIdxs, + MessageInfos: file_demo_app_v1alpha1_app_proto_msgTypes, + }.Build() + File_demo_app_v1alpha1_app_proto = out.File + file_demo_app_v1alpha1_app_proto_goTypes = nil + file_demo_app_v1alpha1_app_proto_depIdxs = nil +} diff --git a/api/demo/app/v1alpha1/app.pb.gw.go b/api/demo/app/v1alpha1/app.pb.gw.go new file mode 100644 index 0000000..8ac26a0 --- /dev/null +++ b/api/demo/app/v1alpha1/app.pb.gw.go @@ -0,0 +1,165 @@ +// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. +// source: demo/app/v1alpha1/app.proto + +/* +Package demo is a reverse proxy. + +It translates gRPC into RESTful JSON APIs. +*/ +package demo + +import ( + "context" + "errors" + "io" + "net/http" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" +) + +// Suppress "imported and not used" errors +var ( + _ codes.Code + _ io.Reader + _ status.Status + _ = errors.New + _ = runtime.String + _ = utilities.NewDoubleArray + _ = metadata.Join +) + +var filter_DemoAppService_GetDemo_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} + +func request_DemoAppService_GetDemo_0(ctx context.Context, marshaler runtime.Marshaler, client DemoAppServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetDemoRequest + metadata runtime.ServerMetadata + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_DemoAppService_GetDemo_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.GetDemo(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_DemoAppService_GetDemo_0(ctx context.Context, marshaler runtime.Marshaler, server DemoAppServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetDemoRequest + metadata runtime.ServerMetadata + ) + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_DemoAppService_GetDemo_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.GetDemo(ctx, &protoReq) + return msg, metadata, err +} + +// RegisterDemoAppServiceHandlerServer registers the http handlers for service DemoAppService to "mux". +// UnaryRPC :call DemoAppServiceServer directly. +// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. +// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterDemoAppServiceHandlerFromEndpoint instead. +// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call. +func RegisterDemoAppServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server DemoAppServiceServer) error { + mux.Handle(http.MethodGet, pattern_DemoAppService_GetDemo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/demo.app.v1alpha1.DemoAppService/GetDemo", runtime.WithHTTPPathPattern("/v1alpha1/demo")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_DemoAppService_GetDemo_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_DemoAppService_GetDemo_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + + return nil +} + +// RegisterDemoAppServiceHandlerFromEndpoint is same as RegisterDemoAppServiceHandler but +// automatically dials to "endpoint" and closes the connection when "ctx" gets done. +func RegisterDemoAppServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { + conn, err := grpc.NewClient(endpoint, opts...) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + return RegisterDemoAppServiceHandler(ctx, mux, conn) +} + +// RegisterDemoAppServiceHandler registers the http handlers for service DemoAppService to "mux". +// The handlers forward requests to the grpc endpoint over "conn". +func RegisterDemoAppServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return RegisterDemoAppServiceHandlerClient(ctx, mux, NewDemoAppServiceClient(conn)) +} + +// RegisterDemoAppServiceHandlerClient registers the http handlers for service DemoAppService +// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "DemoAppServiceClient". +// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "DemoAppServiceClient" +// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in +// "DemoAppServiceClient" to call the correct interceptors. This client ignores the HTTP middlewares. +func RegisterDemoAppServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client DemoAppServiceClient) error { + mux.Handle(http.MethodGet, pattern_DemoAppService_GetDemo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/demo.app.v1alpha1.DemoAppService/GetDemo", runtime.WithHTTPPathPattern("/v1alpha1/demo")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_DemoAppService_GetDemo_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_DemoAppService_GetDemo_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + return nil +} + +var ( + pattern_DemoAppService_GetDemo_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1alpha1", "demo"}, "")) +) + +var ( + forward_DemoAppService_GetDemo_0 = runtime.ForwardResponseMessage +) diff --git a/api/demo/app/v1alpha1/app_grpc.pb.go b/api/demo/app/v1alpha1/app_grpc.pb.go new file mode 100644 index 0000000..c1f3287 --- /dev/null +++ b/api/demo/app/v1alpha1/app_grpc.pb.go @@ -0,0 +1,119 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc (unknown) +// source: demo/app/v1alpha1/app.proto + +package demo + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + DemoAppService_GetDemo_FullMethodName = "/demo.app.v1alpha1.DemoAppService/GetDemo" +) + +// DemoAppServiceClient is the client API for DemoAppService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type DemoAppServiceClient interface { + GetDemo(ctx context.Context, in *GetDemoRequest, opts ...grpc.CallOption) (*GetDemoResponse, error) +} + +type demoAppServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewDemoAppServiceClient(cc grpc.ClientConnInterface) DemoAppServiceClient { + return &demoAppServiceClient{cc} +} + +func (c *demoAppServiceClient) GetDemo(ctx context.Context, in *GetDemoRequest, opts ...grpc.CallOption) (*GetDemoResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetDemoResponse) + err := c.cc.Invoke(ctx, DemoAppService_GetDemo_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// DemoAppServiceServer is the server API for DemoAppService service. +// All implementations should embed UnimplementedDemoAppServiceServer +// for forward compatibility. +type DemoAppServiceServer interface { + GetDemo(context.Context, *GetDemoRequest) (*GetDemoResponse, error) +} + +// UnimplementedDemoAppServiceServer should be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedDemoAppServiceServer struct{} + +func (UnimplementedDemoAppServiceServer) GetDemo(context.Context, *GetDemoRequest) (*GetDemoResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetDemo not implemented") +} +func (UnimplementedDemoAppServiceServer) testEmbeddedByValue() {} + +// UnsafeDemoAppServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to DemoAppServiceServer will +// result in compilation errors. +type UnsafeDemoAppServiceServer interface { + mustEmbedUnimplementedDemoAppServiceServer() +} + +func RegisterDemoAppServiceServer(s grpc.ServiceRegistrar, srv DemoAppServiceServer) { + // If the following call pancis, it indicates UnimplementedDemoAppServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&DemoAppService_ServiceDesc, srv) +} + +func _DemoAppService_GetDemo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetDemoRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DemoAppServiceServer).GetDemo(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DemoAppService_GetDemo_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DemoAppServiceServer).GetDemo(ctx, req.(*GetDemoRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// DemoAppService_ServiceDesc is the grpc.ServiceDesc for DemoAppService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var DemoAppService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "demo.app.v1alpha1.DemoAppService", + HandlerType: (*DemoAppServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "GetDemo", + Handler: _DemoAppService_GetDemo_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "demo/app/v1alpha1/app.proto", +} diff --git a/buf.gen.yaml b/buf.gen.yaml new file mode 100644 index 0000000..81f79b9 --- /dev/null +++ b/buf.gen.yaml @@ -0,0 +1,28 @@ +version: v2 +managed: + enabled: true + disable: + - file_option: go_package + module: buf.build/bufbuild/protovalidate +plugins: + - remote: buf.build/protocolbuffers/go + out: api + opt: + - paths=source_relative + - remote: buf.build/grpc/go + out: api + opt: + - paths=source_relative + - require_unimplemented_servers=false + - remote: buf.build/grpc-ecosystem/gateway + out: api + opt: + - paths=source_relative + - generate_unbound_methods=true + - remote: buf.build/grpc-ecosystem/openapiv2 + out: contrib/ + opt: + - merge_file_name=go-server-with-otel + - allow_merge=true +inputs: + - directory: proto diff --git a/buf.lock b/buf.lock new file mode 100644 index 0000000..22c5f3b --- /dev/null +++ b/buf.lock @@ -0,0 +1,12 @@ +# Generated by buf. DO NOT EDIT. +version: v2 +deps: + - name: buf.build/bufbuild/protovalidate + commit: 6c6e0d3c608e4549802254a2eee81bc8 + digest: b5:a7ca081f38656fc0f5aaa685cc111d3342876723851b47ca6b80cbb810cbb2380f8c444115c495ada58fa1f85eff44e68dc54a445761c195acdb5e8d9af675b6 + - name: buf.build/googleapis/googleapis + commit: 61b203b9a9164be9a834f58c37be6f62 + digest: b5:7811a98b35bd2e4ae5c3ac73c8b3d9ae429f3a790da15de188dc98fc2b77d6bb10e45711f14903af9553fa9821dff256054f2e4b7795789265bc476bec2f088c + - name: buf.build/grpc-ecosystem/grpc-gateway + commit: 4c5ba75caaf84e928b7137ae5c18c26a + digest: b5:c113e62fb3b29289af785866cae062b55ec8ae19ab3f08f3004098928fbca657730a06810b2012951294326b95669547194fa84476b9e9b688d4f8bf77a0691d diff --git a/buf.yaml b/buf.yaml new file mode 100644 index 0000000..db67900 --- /dev/null +++ b/buf.yaml @@ -0,0 +1,14 @@ +# For details on buf.yaml configuration, visit https://buf.build/docs/configuration/v2/buf-yaml +version: v2 +modules: + - path: proto +deps: + - buf.build/bufbuild/protovalidate + - buf.build/googleapis/googleapis + - buf.build/grpc-ecosystem/grpc-gateway +lint: + use: + - STANDARD +breaking: + use: + - FILE diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..77d91d7 --- /dev/null +++ b/config.yaml @@ -0,0 +1,30 @@ +# yaml-language-server: $schema=contrib/schema.json + +# Custom demo-app config +timezone: EST5EDT +opts: + factLang: en + factType: random + +# go-app config +name: Demo go-app +logging: + format: console + level: trace + enabled: true +otel: + enabled: true + stdoutEnabled: false +http: + enabled: true + listen: :8080 + logRequests: true +grpc: + enabled: true + enableReflection: true + listen: :8081 + grpcGatewayPath: /api + enableGRPCGateway: true + enableInstrumentation: true + enableProtovalidate: true + logRequests: true diff --git a/contrib/go-server-with-otel.swagger.json b/contrib/go-server-with-otel.swagger.json new file mode 100644 index 0000000..df72bf5 --- /dev/null +++ b/contrib/go-server-with-otel.swagger.json @@ -0,0 +1,99 @@ +{ + "swagger": "2.0", + "info": { + "title": "demo/app/v1alpha1/app.proto", + "version": "version not set" + }, + "tags": [ + { + "name": "DemoAppService" + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/v1alpha1/demo": { + "get": { + "operationId": "DemoAppService_GetDemo", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1alpha1GetDemoResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "language", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "DemoAppService" + ] + } + } + }, + "definitions": { + "protobufAny": { + "type": "object", + "properties": { + "@type": { + "type": "string" + } + }, + "additionalProperties": {} + }, + "rpcStatus": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + }, + "details": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/protobufAny" + } + } + } + }, + "v1alpha1GetDemoResponse": { + "type": "object", + "properties": { + "timestamp": { + "type": "string", + "format": "date-time" + }, + "fact": { + "type": "string" + }, + "source": { + "type": "string" + }, + "language": { + "type": "string" + } + }, + "title": "Returns a randome fact, because this is a demo app\nso what else do we do?" + } + } +} diff --git a/contrib/mcpinspector.json b/contrib/mcpinspector.json new file mode 100644 index 0000000..721804b --- /dev/null +++ b/contrib/mcpinspector.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "default-server": { + "type": "streamable-http", + "url": "http://localhost:8080/api/mcp/" + } + } +} diff --git a/contrib/schema.json b/contrib/schema.json new file mode 100644 index 0000000..ff54446 --- /dev/null +++ b/contrib/schema.json @@ -0,0 +1,154 @@ +{ + "definitions": { + "ConfigDemoOpts": { + "properties": { + "factLang": { + "default": "en", + "type": "string" + }, + "factType": { + "default": "random", + "enum": [ + "today", + "random" + ], + "type": "string" + } + }, + "type": "object" + }, + "ConfigGRPCConfig": { + "properties": { + "enableGRPCGateway": { + "default": true, + "type": "boolean" + }, + "enableInstrumentation": { + "type": "boolean" + }, + "enableProtovalidate": { + "description": "Add a chain unary and streaming interceptor for buf.build/go/protovalidate", + "default": false, + "type": "boolean" + }, + "enableReflection": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "grpcGatewayPath": { + "default": "/grpc-api", + "type": "string" + }, + "listen": { + "type": "string" + }, + "logRequests": { + "type": "boolean" + } + }, + "type": "object" + }, + "ConfigHTTPConfig": { + "properties": { + "enabled": { + "type": "boolean" + }, + "idleTimeout": { + "type": "string" + }, + "listen": { + "type": "string" + }, + "logExcludePathRegexps": { + "items": { + "type": "string" + }, + "type": "array" + }, + "logRequests": { + "type": "boolean" + }, + "readTimeout": { + "type": "string" + }, + "writeTimeout": { + "type": "string" + } + }, + "type": "object" + }, + "ConfigLogConfig": { + "properties": { + "enabled": { + "type": "boolean" + }, + "format": { + "type": "string" + }, + "level": { + "type": "string" + }, + "output": { + "type": "string" + }, + "timeFormat": { + "type": "string" + } + }, + "type": "object" + }, + "ConfigOTELConfig": { + "properties": { + "enabled": { + "type": "boolean" + }, + "metricIntervalSecs": { + "type": "integer" + }, + "prometheusEnabled": { + "type": "boolean" + }, + "prometheusPath": { + "type": "string" + }, + "stdoutEnabled": { + "type": "boolean" + } + }, + "type": "object" + } + }, + "properties": { + "environment": { + "type": "string" + }, + "grpc": { + "$ref": "#/definitions/ConfigGRPCConfig" + }, + "http": { + "$ref": "#/definitions/ConfigHTTPConfig" + }, + "logging": { + "$ref": "#/definitions/ConfigLogConfig" + }, + "name": { + "type": "string" + }, + "opts": { + "$ref": "#/definitions/ConfigDemoOpts" + }, + "otel": { + "$ref": "#/definitions/ConfigOTELConfig" + }, + "timezone": { + "default": "UTC", + "type": "string" + }, + "version": { + "type": "string" + } + }, + "type": "object" +} diff --git a/env-sample b/env-sample new file mode 100644 index 0000000..2cf253f --- /dev/null +++ b/env-sample @@ -0,0 +1,14 @@ +# App Config +APP_NAME="go-server-with-otel" +APP_LOG_LEVEL=trace ## For testing only +APP_LOG_FORMAT=console ## console, json +APP_LOG_TIME_FORMAT=long ## long, short, unix, rfc3339, off + +# App OTEL Config +APP_OTEL_STDOUT_ENABLED=true ## For testing only +APP_OTEL_METRIC_INTERVAL_SECS=15 + +# OTEL SDK Config +OTEL_EXPORTER_OTLP_ENDPOINT="otel-collector.otel.svc.cluster.local" # Set to your otel collector +OTEL_SERVICE_NAME="go-server-with-otel" +OTEL_RESOURCE_ATTRIBUTES="env=development,service.version=(devel)" diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b679cd7 --- /dev/null +++ b/go.mod @@ -0,0 +1,79 @@ +module gitea.libretechconsulting.com/rmcguire/go-server-with-otel + +go 1.25 + +require ( + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.8-20250717185734-6c6e0d3c608e.1 + gitea.libretechconsulting.com/rmcguire/go-app v0.12.3 + github.com/go-resty/resty/v2 v2.16.5 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 + github.com/modelcontextprotocol/go-sdk v0.3.1 + github.com/rs/zerolog v1.34.0 + go.opentelemetry.io/otel/trace v1.38.0 + google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 + google.golang.org/grpc v1.75.0 + google.golang.org/protobuf v1.36.8 + k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d +) + +require ( + github.com/caarlos0/env/v11 v11.3.1 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect + github.com/swaggest/jsonschema-go v0.3.78 // indirect + github.com/swaggest/refl v1.4.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect + go.opentelemetry.io/otel v1.38.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/prometheus v0.60.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 // indirect + go.opentelemetry.io/otel/sdk v1.38.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +require ( + buf.build/go/protovalidate v0.14.0 // indirect + cel.dev/expr v0.24.0 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/cel-go v0.26.1 // indirect + github.com/google/jsonschema-go v0.2.1-0.20250825175020-748c325cec76 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/client_golang v1.23.0 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.65.0 // indirect + github.com/prometheus/otlptranslator v0.0.2 // indirect + github.com/prometheus/procfs v0.17.0 // indirect + github.com/stoewer/go-strcase v1.3.1 // indirect + github.com/yosida95/uritemplate/v3 v3.0.2 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/proto/otlp v1.7.1 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect + golang.org/x/net v0.43.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.28.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 // indirect + google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect +) + +tool ( + github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway + github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2 + google.golang.org/grpc/cmd/protoc-gen-go-grpc + google.golang.org/protobuf/cmd/protoc-gen-go +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..66701ff --- /dev/null +++ b/go.sum @@ -0,0 +1,186 @@ +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.8-20250717185734-6c6e0d3c608e.1 h1:sjY1k5uszbIZfv11HO2keV4SLhNA47SabPO886v7Rvo= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.8-20250717185734-6c6e0d3c608e.1/go.mod h1:8EQ5GzyGJQ5tEIwMSxCl8RKJYsjCpAwkdcENoioXT6g= +buf.build/go/protovalidate v0.14.0 h1:kr/rC/no+DtRyYX+8KXLDxNnI1rINz0imk5K44ZpZ3A= +buf.build/go/protovalidate v0.14.0/go.mod h1:+F/oISho9MO7gJQNYC2VWLzcO1fTPmaTA08SDYJZncA= +cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= +cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +gitea.libretechconsulting.com/rmcguire/go-app v0.12.1 h1:OWXz8S3NAlBAjIxY7G6ydO18t5/JiJ8f3EMKDeppEg8= +gitea.libretechconsulting.com/rmcguire/go-app v0.12.1/go.mod h1:T8K2vbmt0mApScJDGqfmNlvX5gdJSImjArUMpgfYs8U= +gitea.libretechconsulting.com/rmcguire/go-app v0.12.3 h1:sgSpPx6fkWqfBGsCAHqLOIQ6a1s+4nsG9kW24k+WYI8= +gitea.libretechconsulting.com/rmcguire/go-app v0.12.3/go.mod h1:+zDxLaJgxfPVWzp2YQrUYBFN9A1c7hZgsQkysWz+lSI= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bool64/dev v0.2.39 h1:kP8DnMGlWXhGYJEZE/J0l/gVBdbuhoPGL+MJG4QbofE= +github.com/bool64/dev v0.2.39/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg= +github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E= +github.com/bool64/shared v0.1.5/go.mod h1:081yz68YC9jeFB3+Bbmno2RFWvGKv1lPKkMP6MHJlPs= +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/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/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/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM= +github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ= +github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/jsonschema-go v0.2.1-0.20250825175020-748c325cec76 h1:mBlBwtDebdDYr+zdop8N62a44g+Nbv7o2KjWyS1deR4= +github.com/google/jsonschema-go v0.2.1-0.20250825175020-748c325cec76/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +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/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248= +github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 h1:sGm2vDRFUrQJO/Veii4h4zG2vvqG6uWNkBHSTqXOZk0= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= +github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc= +github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +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/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modelcontextprotocol/go-sdk v0.3.1 h1:0z04yIPlSwTluuelCBaL+wUag4YeflIU2Fr4Icb7M+o= +github.com/modelcontextprotocol/go-sdk v0.3.1/go.mod h1:whv0wHnsTphwq7CTiKYHkLtwLC06WMoY2KpO+RB9yXQ= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc= +github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= +github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= +github.com/prometheus/otlptranslator v0.0.2 h1:+1CdeLVrRQ6Psmhnobldo0kTp96Rj80DRXRd5OSnMEQ= +github.com/prometheus/otlptranslator v0.0.2/go.mod h1:P8AwMgdD7XEr6QRUJ2QWLpiAZTgTE2UYgjlu3svompI= +github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= +github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= +github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs= +github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7o2xQ= +github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU= +github.com/swaggest/jsonschema-go v0.3.78 h1:5+YFQrLxOR8z6CHvgtZc42WRy/Q9zRQQ4HoAxlinlHw= +github.com/swaggest/jsonschema-go v0.3.78/go.mod h1:4nniXBuE+FIGkOGuidjOINMH7OEqZK3HCSbfDuLRI0g= +github.com/swaggest/refl v1.4.0 h1:CftOSdTqRqs100xpFOT/Rifss5xBV/CT0S/FN60Xe9k= +github.com/swaggest/refl v1.4.0/go.mod h1:4uUVFVfPJ0NSX9FPwMPspeHos9wPFlCMGoPRllUbpvA= +github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= +github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= +github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= +github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= +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.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= +go.opentelemetry.io/otel/exporters/prometheus v0.60.0 h1:cGtQxGvZbnrWdC2GyjZi0PDKVSLWP/Jocix3QWfXtbo= +go.opentelemetry.io/otel/exporters/prometheus v0.60.0/go.mod h1:hkd1EekxNo69PTV4OWFGZcKQiIqg0RfuWExcPKFvepk= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83wzcKzGGqAnnfLFyFe7RslekZuv+VI= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 h1:kJxSDN4SgWWTjG/hPp3O7LCGLcHXFlvS2/FFOrwL+SE= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0/go.mod h1:mgIOzS7iZeKJdeB8/NYHrJ48fdGc71Llo5bJ1J4DWUE= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= +go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0= +golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 h1:APHvLLYBhtZvsbnpkfknDZ7NyH4z5+ub/I0u8L3Oz6g= +google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1/go.mod h1:xUjFWUnWDpZ/C0Gu0qloASKFb6f8/QXiiXhSPFsD668= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 h1:pmJpJEvT846VzausCQ5d7KreSROcDqmO388w5YbnltA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og= +google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= +google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +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.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d h1:wAhiDyZ4Tdtt7e46e9M5ZSAJ/MnPGPs+Ki1gHw4w1R0= +k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= diff --git a/helm/.gitignore b/helm/.gitignore new file mode 100644 index 0000000..f791801 --- /dev/null +++ b/helm/.gitignore @@ -0,0 +1,2 @@ +charts/ +Chart.lock diff --git a/helm/Chart.yaml b/helm/Chart.yaml new file mode 100644 index 0000000..f6a1e74 --- /dev/null +++ b/helm/Chart.yaml @@ -0,0 +1,30 @@ +apiVersion: v2 +name: go-server-with-otel +description: Golang HTTP and GRPC server with OTEL + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.3 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "v0.7.0" + +dependencies: + - name: hull + repository: https://vidispine.github.io/hull + version: 1.32.2 + diff --git a/helm/library-hull.yaml b/helm/library-hull.yaml new file mode 100644 index 0000000..6d338db --- /dev/null +++ b/helm/library-hull.yaml @@ -0,0 +1,643 @@ +################################ +### values.yaml for HULL +### The basic pre-configuration takes place here. +### +### Do not change this file, use additional values.hull.yaml +### to overwrite the selected fields! +################################ + +################################################### +### CONFIG +config: + general: + rbac: true + fullnameOverride: "" + nameOverride: "" + namespaceOverride: "" + noObjectNamePrefixes: false + createImagePullSecretsFromRegistries: true + globalImageRegistryServer: "" + globalImageRegistryToFirstRegistrySecretServer: false + serialization: + configmap: + enabled: true + fileExtensions: + json: toPrettyJson + yml: toYaml + yaml: toYaml + secret: + enabled: true + fileExtensions: + json: toPrettyJson + yml: toYaml + yaml: toYaml + render: + passes: 3 + emptyLabels: false + emptyAnnotations: false + emptyTemplateLabels: false + emptyTemplateAnnotations: false + emptyHullObjects: false + postRender: + globalStringReplacements: + instanceKey: + enabled: false + string: _HULL_OBJECT_TYPE_DEFAULT_ + replacement: OBJECT_INSTANCE_KEY + instanceKeyResolved: + enabled: false + string: _HULL_OBJECT_TYPE_DEFAULT_ + replacement: OBJECT_INSTANCE_KEY_RESOLVED + instanceName: + enabled: false + string: _HULL_OBJECT_TYPE_DEFAULT_ + replacement: OBJECT_INSTANCE_NAME + errorChecks: + objectYamlValid: true + hullGetTransformationReferenceValid: true + containerImageValid: true + virtualFolderDataPathExists: true + virtualFolderDataInlineValid: false + debug: + renderBrokenHullGetTransformationReferences: false + renderNilWhenInlineIsNil: false + renderPathMissingWhenPathIsNonExistent: false + metadata: + labels: + common: + 'app.kubernetes.io/managed-by': + 'app.kubernetes.io/version': + 'app.kubernetes.io/part-of': + 'app.kubernetes.io/name': + 'app.kubernetes.io/instance': + 'app.kubernetes.io/component': + 'helm.sh/chart': + 'vidispine.hull/version': + custom: {} + annotations: + hashes: false + custom: {} + data: {} + specific: {} + templates: + pod: + global: {} + container: + global: {} +################################################### + +################################################### +### OBJECTS +objects: + +# NAMESPACE + namespace: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + staticName: true + annotations: {} + labels: {} +################################################### + +# CONFIGMAPS + configmap: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} +################################################### + +# SECRETS + secret: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} +################################################### + +# REGISTRIES + registry: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} +################################################### + +# SERVICEACCOUNTS + serviceaccount: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + default: + enabled: _HT?eq (dig "serviceAccountName" "" _HT*hull.config.templates.pod.global) "" + annotations: {} + labels: {} +################################################### + +# ROLES + role: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + rules: + _HULL_OBJECT_TYPE_DEFAULT_: {} + default: + enabled: _HT?eq (dig "serviceAccountName" "" _HT*hull.config.templates.pod.global) "" + rules: {} +################################################### + +# ROLEBINDINGS + rolebinding: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + default: + enabled: _HT?eq (dig "serviceAccountName" "" _HT*hull.config.templates.pod.global) "" + roleRef: + apiGroup: "rbac.authorization.k8s.io" + kind: "Role" + name: _HT^default + subjects: + - kind: ServiceAccount + name: _HT^default + namespace: _HT**Release.Namespace +################################################### + +# CLUSTERROLES + clusterrole: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + rules: + _HULL_OBJECT_TYPE_DEFAULT_: {} +################################################### + +# CLUSTERROLEBINDINGS + clusterrolebinding: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} +################################################### + +# CUSTOMRESOURCEDEFINITIONS (deprecated with Helm3) +# customresourcedefinitions: +# _HULL_OBJECT_TYPE_DEFAULT_: +# enabled: true +# annotations: {} +# labels: {} +################################################### + +# CUSTOMRESOURCES + customresource: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} +################################################### + +# PERSISTENTVOLUMECLAIMS + persistentvolumeclaim: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} +################################################### + +# PERSISTENTVOLUMES + persistentvolume: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} +################################################### + +# STORAGECLASSES + storageclass: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} +################################################### + +# SERVICES + service: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + ports: + _HULL_OBJECT_TYPE_DEFAULT_: {} + +################################################### + +# INGRESSES + ingress: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + tls: + _HULL_OBJECT_TYPE_DEFAULT_: {} + rules: + _HULL_OBJECT_TYPE_DEFAULT_: + http: + paths: + _HULL_OBJECT_TYPE_DEFAULT_: {} + +################################################### + +# INGRESSCLASSES + ingressclass: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + +################################################### + +# DEPLOYMENTS + deployment: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + templateAnnotations: {} + templateLabels: {} + pod: + initContainers: + _HULL_OBJECT_TYPE_DEFAULT_: + env: + _HULL_OBJECT_TYPE_DEFAULT_: {} + envFrom: + _HULL_OBJECT_TYPE_DEFAULT_: {} + volumeMounts: + _HULL_OBJECT_TYPE_DEFAULT_: {} + containers: + _HULL_OBJECT_TYPE_DEFAULT_: + env: + _HULL_OBJECT_TYPE_DEFAULT_: {} + envFrom: + _HULL_OBJECT_TYPE_DEFAULT_: {} + volumeMounts: + _HULL_OBJECT_TYPE_DEFAULT_: {} + volumes: + _HULL_OBJECT_TYPE_DEFAULT_: {} +################################################### + +# JOBS + job: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + templateAnnotations: {} + templateLabels: {} + pod: + initContainers: + _HULL_OBJECT_TYPE_DEFAULT_: + env: + _HULL_OBJECT_TYPE_DEFAULT_: {} + envFrom: + _HULL_OBJECT_TYPE_DEFAULT_: {} + volumeMounts: + _HULL_OBJECT_TYPE_DEFAULT_: {} + containers: + _HULL_OBJECT_TYPE_DEFAULT_: + env: + _HULL_OBJECT_TYPE_DEFAULT_: {} + envFrom: + _HULL_OBJECT_TYPE_DEFAULT_: {} + volumeMounts: + _HULL_OBJECT_TYPE_DEFAULT_: {} + volumes: + _HULL_OBJECT_TYPE_DEFAULT_: {} +################################################### + +# CRONJOBS + cronjob: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + job: + templateAnnotations: {} + templateLabels: {} + pod: + initContainers: + _HULL_OBJECT_TYPE_DEFAULT_: + env: + _HULL_OBJECT_TYPE_DEFAULT_: {} + envFrom: + _HULL_OBJECT_TYPE_DEFAULT_: {} + volumeMounts: + _HULL_OBJECT_TYPE_DEFAULT_: {} + containers: + _HULL_OBJECT_TYPE_DEFAULT_: + env: + _HULL_OBJECT_TYPE_DEFAULT_: {} + envFrom: + _HULL_OBJECT_TYPE_DEFAULT_: {} + volumeMounts: + _HULL_OBJECT_TYPE_DEFAULT_: {} + volumes: + _HULL_OBJECT_TYPE_DEFAULT_: {} +################################################### + +# DAEMONSETS + daemonset: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + templateAnnotations: {} + templateLabels: {} + pod: + initContainers: + _HULL_OBJECT_TYPE_DEFAULT_: + env: + _HULL_OBJECT_TYPE_DEFAULT_: {} + envFrom: + _HULL_OBJECT_TYPE_DEFAULT_: {} + volumeMounts: + _HULL_OBJECT_TYPE_DEFAULT_: {} + containers: + _HULL_OBJECT_TYPE_DEFAULT_: + env: + _HULL_OBJECT_TYPE_DEFAULT_: {} + envFrom: + _HULL_OBJECT_TYPE_DEFAULT_: {} + volumeMounts: + _HULL_OBJECT_TYPE_DEFAULT_: {} + volumes: + _HULL_OBJECT_TYPE_DEFAULT_: {} +################################################### + +# STATEFULSETS + statefulset: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + templateAnnotations: {} + templateLabels: {} + pod: + initContainers: + _HULL_OBJECT_TYPE_DEFAULT_: + env: + _HULL_OBJECT_TYPE_DEFAULT_: {} + envFrom: + _HULL_OBJECT_TYPE_DEFAULT_: {} + volumeMounts: + _HULL_OBJECT_TYPE_DEFAULT_: {} + containers: + _HULL_OBJECT_TYPE_DEFAULT_: + env: + _HULL_OBJECT_TYPE_DEFAULT_: {} + envFrom: + _HULL_OBJECT_TYPE_DEFAULT_: {} + volumeMounts: + _HULL_OBJECT_TYPE_DEFAULT_: {} + volumes: + _HULL_OBJECT_TYPE_DEFAULT_: {} +################################################### + +# SERVICEMONITORS + servicemonitor: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} +################################################### + +# HORIZONTALPODAUTOSCALER + horizontalpodautoscaler: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} +################################################### + +# PODDISRUPTIONBUDGET + poddisruptionbudget: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} +################################################### + +# PRIORITYCLASS + priorityclass: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} +################################################### + +# ENDPOINTS + endpoints: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} +################################################### + +# ENDPOINTSLICE + endpointslice: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} +################################################### + +# LIMITRANGE + limitrange: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} +################################################### + +# MUTATINGWEBHOOKCONFIGURATION + mutatingwebhookconfiguration: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + webhooks: + _HULL_OBJECT_TYPE_DEFAULT_: {} +################################################### + +# VALIDATINGWEBHOOKCONFIGURATION + validatingwebhookconfiguration: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + webhooks: + _HULL_OBJECT_TYPE_DEFAULT_: {} +################################################### + +# RESOURCEQUOTA + resourcequota: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} +################################################## + +# NETWORKPOLICY + networkpolicy: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} +################################################## + +# GATEWAY API - BACKENDLBPOLICY + backendlbpolicy: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + targetRefs: + _HULL_OBJECT_TYPE_DEFAULT_: {} + +################################################## + +# GATEWAY API - BACKENDTLSPOLICY + backendtlspolicy: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + targetRefs: + _HULL_OBJECT_TYPE_DEFAULT_: {} +################################################## + +# GATEWAY API - GATEWAYCLASS + gatewayclass: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} +################################################## + +# GATEWAY API - GATEWAY + gateway: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + addresses: + _HULL_OBJECT_TYPE_DEFAULT_: {} + listeners: + _HULL_OBJECT_TYPE_DEFAULT_: + tls: + certificateRefs: + _HULL_OBJECT_TYPE_DEFAULT_: {} + frontendValidation: + caCertificateRefs: + _HULL_OBJECT_TYPE_DEFAULT_: {} + allowedRoutes: + kinds: + _HULL_OBJECT_TYPE_DEFAULT_: {} +################################################## + +# GATEWAY API - GRPCROUTE + grpcroute: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + parentRefs: + _HULL_OBJECT_TYPE_DEFAULT_: {} + rules: + _HULL_OBJECT_TYPE_DEFAULT_: + matches: + _HULL_OBJECT_TYPE_DEFAULT_: {} + filters: + _HULL_OBJECT_TYPE_DEFAULT_: {} + backendRefs: + _HULL_OBJECT_TYPE_DEFAULT_: + filters: + _HULL_OBJECT_TYPE_DEFAULT_: {} +################################################## + +# GATEWAY API - REFERENCEGRANT + referencegrant: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + from: + _HULL_OBJECT_TYPE_DEFAULT_: {} + to: + _HULL_OBJECT_TYPE_DEFAULT_: {} +################################################## + +# GATEWAY API - TCPROUTE + tcproute: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + parentRefs: + _HULL_OBJECT_TYPE_DEFAULT_: {} + rules: + _HULL_OBJECT_TYPE_DEFAULT_: + backendRefs: + _HULL_OBJECT_TYPE_DEFAULT_: {} +################################################## + +# GATEWAY API - TLSROUTE + tlsroute: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + parentRefs: + _HULL_OBJECT_TYPE_DEFAULT_: {} + rules: + _HULL_OBJECT_TYPE_DEFAULT_: + backendRefs: + _HULL_OBJECT_TYPE_DEFAULT_: {} +################################################## + +# GATEWAY API - UDPROUTE + udproute: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + parentRefs: + _HULL_OBJECT_TYPE_DEFAULT_: {} + rules: + _HULL_OBJECT_TYPE_DEFAULT_: + backendRefs: + _HULL_OBJECT_TYPE_DEFAULT_: {} +################################################## + +# GATEWAY API - HTTPROUTE + httproute: + _HULL_OBJECT_TYPE_DEFAULT_: + enabled: true + annotations: {} + labels: {} + parentRefs: + _HULL_OBJECT_TYPE_DEFAULT_: {} + rules: + _HULL_OBJECT_TYPE_DEFAULT_: + matches: + _HULL_OBJECT_TYPE_DEFAULT_: {} + filters: + _HULL_OBJECT_TYPE_DEFAULT_: {} + backendRefs: + _HULL_OBJECT_TYPE_DEFAULT_: + filters: + _HULL_OBJECT_TYPE_DEFAULT_: {} +################################################## diff --git a/helm/templates/app.yaml b/helm/templates/app.yaml new file mode 100644 index 0000000..1739244 --- /dev/null +++ b/helm/templates/app.yaml @@ -0,0 +1 @@ +{{- include "hull.objects.prepare.all" (dict "HULL_ROOT_KEY" "hull" "ROOT_CONTEXT" $) }} diff --git a/helm/values.yaml b/helm/values.yaml new file mode 100644 index 0000000..07e988c --- /dev/null +++ b/helm/values.yaml @@ -0,0 +1,195 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/vidispine/hull/refs/heads/main/hull/values.schema.json +hull: + config: + ## go-server-with-otel custom settings (config.yaml) + appConfig: + # Custom app config + timezone: EST5EDT + opts: + factLang: en + factType: random + # go-app config + name: Demo go-app + logging: + format: json + level: debug + enabled: true + otel: + enabled: true + stdoutEnabled: false + http: + enabled: true + listen: :8080 + logRequests: true + grpc: + enabled: true + enableReflection: true + listen: :8081 + grpcGatewayPath: /api + enableGRPCGateway: true + enableInstrumentation: true + logRequests: true + ## Chart settings + settings: + resources: {} # Applies to the app container + repo: gitea.libretechconsulting.com/rmcguire/go-server-with-otel + tag: _HT**Chart.AppVersion + + httpPort: 8080 # Should match appConfig http.listen + grpcPort: 8081 # Should match appConfig grpc.listen + + # Use this as a shortcut, or create your own hull.objects.httproute + httproute: + enabled: false + hostnames: + - app.mydomain.com + gatewayName: istio-ingressgateway + gatewayNamespace: istio-system + + # Use this as a shortcut, or create your own hull.objects.grpcroute + grpcroute: + enabled: false + hostnames: + - app.mydomain.com + gatewayName: istio-ingressgateway + gatewayNamespace: istio-system + + otelServiceName: go-server-with-otel + otelResourceAttributes: app=go-server-with-otel + otlpEndpoint: http://otel.otel.svc.cluster.local:4317 # Replace me + + serviceType: ClusterIP + serviceLbIP: "" # Used if serviceTyps=LoadBalancer + + general: + rbac: false + render: + passes: 2 + # Applies to all objects + metadata: + labels: + custom: + app: _HT**Release.Name + version: _HT**Chart.AppVersion + + objects: + configmap: + config: + data: + config.yaml: + serialization: toYaml + inline: _HT*hull.config.appConfig + environment: + data: + OTEL_EXPORTER_OTLP_ENDPOINT: + serialization: none + inline: _HT*hull.config.settings.otlpEndpoint + OTEL_SERVICE_NAME: + serialization: none + inline: _HT*hull.config.settings.otelServiceName + OTEL_RESOURCE_ATTRIBUTES: + serialization: none + inline: _HT! + {{ printf "deployment.name=%s,%s" _HT**Release.Name _HT*hull.config.settings.otelResourceAttributes }} + serviceaccount: + default: + enabled: false + role: + default: + enabled: false + rolebinding: + default: + enabled: false + + deployment: + main: + pod: + containers: + main: + image: + repository: _HT*hull.config.settings.repo + tag: _HT*hull.config.settings.tag + imagePullPolicy: Always + args: + - -config + - /app/config.yaml + ports: + http: + containerPort: _HT*hull.config.settings.httpPort + grpc: + containerPort: _HT*hull.config.settings.grpcPort + envFrom: + main: + configMapRef: + name: environment + resources: _HT*hull.config.settings.resources + readinessProbe: + httpGet: + path: /health + port: 8080 + scheme: HTTP + periodSeconds: 10 + failureThreshold: 2 + livenessProbe: + httpGet: + path: /health + port: 8080 + scheme: HTTP + periodSeconds: 10 + failureThreshold: 2 + volumeMounts: + config: + name: config + mountPath: /app/config.yaml + subPath: config.yaml + volumes: + environment: + configMap: + name: environment + config: + configMap: + name: config + + service: + main: + type: _HT*hull.config.settings.serviceType + loadBalancerIP: _HT*hull.config.settings.serviceLbIP + ports: + http: + port: _HT*hull.config.settings.httpPort + targetPort: http + grpc: + port: _HT*hull.config.settings.grpcPort + targetPort: grpc + + httproute: + main: + enabled: _HT*hull.config.settings.httproute.enabled + hostnames: _HT*hull.config.settings.httproute.hostnames + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: _HT*hull.config.settings.httproute.gatewayName + namespace: _HT*hull.config.settings.httproute.gatewayNamespace + rules: + - backendRefs: + - group: "" + kind: Service + name: _HT^main + port: _HT*hull.config.settings.httpPort + + grpcroute: + main: + enabled: _HT*hull.config.settings.grpcroute.enabled + hostnames: _HT*hull.config.settings.grpcroute.hostnames + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: _HT*hull.config.settings.grpcroute.gatewayName + namespace: _HT*hull.config.settings.grpcroute.gatewayNamespace + rules: + - backendRefs: + - group: "" + kind: Service + name: _HT^main + port: _HT*hull.config.settings.grpcPort diff --git a/main.go b/main.go new file mode 100644 index 0000000..36c5c3a --- /dev/null +++ b/main.go @@ -0,0 +1,145 @@ +// This template contains a simple +// app with OTEL bootstrap that will create an +// HTTP server configured by environment that exports +// spans and metrics to an OTEL collector if configured +// to do so. Will also stand up a prometheus metrics +// endpoint. +// +// Configuration and logger stored in context +// Reference implementation of the provided packages +package main + +import ( + "context" + "flag" + "fmt" + "os" + "os/signal" + "sync" + "syscall" + "time" + + "github.com/rs/zerolog" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" + + "gitea.libretechconsulting.com/rmcguire/go-app/pkg/app" + "gitea.libretechconsulting.com/rmcguire/go-app/pkg/otel" + + "gitea.libretechconsulting.com/rmcguire/go-server-with-otel/pkg/config" + "gitea.libretechconsulting.com/rmcguire/go-server-with-otel/pkg/demo" + "gitea.libretechconsulting.com/rmcguire/go-server-with-otel/pkg/service" +) + +const ( + terminationGracePeriod = 30 * time.Second + appName = "demo-app" +) + +var ( + // TODO: Register your service.AppService implementers here + // This should be the only change necessary here + // NOTE: At least one service needs to add a dial option + // for transport credentials, otherwise you will have to do it here + appServices = []service.AppService{ + &demo.DemoService{}, + } + + flagSchema bool +) + +func main() { + ctx, cncl := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT) + defer cncl() + + // Load configuration and setup logging. The go-app framework + // will handle loading config and environment into our demo + // app config struct which embeds app.AooConfig + ctx, serviceConfig := app.MustLoadConfigInto(ctx, &config.ServiceConfig{}) + + // Print schema if that's all we have to do + if flagSchema { + printSchema() + os.Exit(0) + } + + // Prepare app + app := &app.App{ + AppContext: ctx, + } + app.InitOTEL() + + tracer := otel.GetTracer(ctx, appName) + ctx, span := tracer.Start(ctx, appName+".startup", trace.WithAttributes( + attribute.Int("appServices", len(appServices)), + )) + + log := zerolog.Ctx(ctx) + log.Debug().Any("mergedConfig", serviceConfig).Msg("App configuration prepared") + + // Prepare services + var err error + sdFuncs := make([]service.ShutdownFunc, len(appServices)) + for i, svc := range appServices { + sdFuncs[i], err = svc.Init(ctx, serviceConfig) + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + log.Fatal().Err(err).Send() + } + } + span.AddEvent("app services initialized") + + // Merge all service implementations and add to app + grpcSvcs, httpSvcs := service.MergeServices(ctx, appServices...) + app.GRPC = grpcSvcs + app.HTTP = httpSvcs + + span.SetAttributes( + attribute.Int("grpcServices", len(app.GRPC.Services)), + attribute.Int("httpServices", len(app.HTTP.Funcs)+len(app.HTTP.Handlers)), + ) + + // Launch app + app.MustRun() + + span.AddEvent("app started") + span.SetStatus(codes.Ok, "") + span.End() + + // Wait for app to complete and then perform internal shutdown + <-app.Done() + + log.Info().Int("numServices", len(appServices)).Msg("shutting down app services") + + sdCtx, cncl := context.WithTimeout(context.Background(), terminationGracePeriod) + defer cncl() + + // Call registered shutdown funcs from all services + var sdWg sync.WaitGroup + for _, sdFunc := range sdFuncs { + sdWg.Add(1) + go func() { + defer sdWg.Done() + svcName, err := sdFunc(sdCtx) + log.Err(err).Str("service", svcName).Msg("terminated") + }() + } + sdWg.Wait() +} + +// flag.Parse will be called by go-app +func init() { + flag.BoolVar(&flagSchema, "schema", false, "generate json schema and exit") +} + +func printSchema() { + bytes, err := app.CustomSchema(&config.ServiceConfig{}) + if err != nil { + panic(err) + } + + fmt.Println(string(bytes)) + os.Exit(0) +} diff --git a/pkg/config/custom.go b/pkg/config/custom.go new file mode 100644 index 0000000..362fb2c --- /dev/null +++ b/pkg/config/custom.go @@ -0,0 +1,31 @@ +// Package config contains ServiceConfig, which is intended +// to extend go-app/pkg/config.AppConfig. Add any custom fields +// here, and it will automatically be handled by go-app, including +// overrides by environment, and json schema generation +package config + +import ( + "gitea.libretechconsulting.com/rmcguire/go-app/pkg/config" +) + +type ServiceConfig struct { + Timezone string `yaml:"timezone" json:"timezone,omitempty" default:"UTC"` + Opts *DemoOpts `yaml:"opts" json:"opts,omitempty"` + + // Embeds go-app config, used by go-app to + // merge custom config into go-app config, and to produce + // a complete configuration json schema + *config.AppConfig +} + +type DemoOpts struct { + FactLang string `yaml:"factLang" json:"factLang,omitempty" default:"en"` + FactType FactType `yaml:"factType" json:"factType,omitempty" default:"random" enum:"today,random"` +} + +type FactType string + +const ( + TypeToday FactType = "today" + TypeRandom FactType = "random" +) diff --git a/pkg/demo/demo.go b/pkg/demo/demo.go new file mode 100644 index 0000000..bf41970 --- /dev/null +++ b/pkg/demo/demo.go @@ -0,0 +1,58 @@ +// Package demo provides a reference implementation +// of service.AppService. It packages out the GRPC and HTTP +// functionality, for the sake of structure. +package demo + +import ( + "context" + + optsgrpc "gitea.libretechconsulting.com/rmcguire/go-app/pkg/srv/grpc/opts" + optshttp "gitea.libretechconsulting.com/rmcguire/go-app/pkg/srv/http/opts" + + "gitea.libretechconsulting.com/rmcguire/go-server-with-otel/pkg/config" + "gitea.libretechconsulting.com/rmcguire/go-server-with-otel/pkg/demo/demogrpc" + "gitea.libretechconsulting.com/rmcguire/go-server-with-otel/pkg/demo/demohttp" + "gitea.libretechconsulting.com/rmcguire/go-server-with-otel/pkg/demo/demomcp" + "gitea.libretechconsulting.com/rmcguire/go-server-with-otel/pkg/service" +) + +type DemoService struct { + ctx context.Context + config *config.ServiceConfig + http *demohttp.DemoHTTPServer + grpc *demogrpc.DemoGRPCServer + mcp *demomcp.DemoMCPServer +} + +func (d *DemoService) Init(ctx context.Context, config *config.ServiceConfig, +) (service.ShutdownFunc, error) { + d.config = config + d.ctx = ctx + + // These don't HAVE to be split into separate packages + // This is, after all, a demo app. Just implement the interface. + d.http = demohttp.NewDemoHTTPServer(ctx, config) + d.grpc = demogrpc.NewDemoGRPCServer(ctx, config) + d.mcp = demomcp.NewDemoMCPServer(ctx, config) + + // TODO: This should actually do shutdown stuff + return func(_ context.Context) (string, error) { + return "DemoService", nil + }, nil +} + +func (d *DemoService) GetGRPC() *optsgrpc.AppGRPC { + return &optsgrpc.AppGRPC{ + Services: d.grpc.GetServices(), + GRPCDialOpts: d.grpc.GetDialOpts(), + } +} + +func (d *DemoService) GetHTTP() *optshttp.AppHTTP { + return &optshttp.AppHTTP{ + Ctx: d.ctx, + Funcs: d.http.GetHandleFuncs(), + Handlers: d.mcp.GetHandlers(), + HealthChecks: d.http.GetHealthCheckFuncs(), + } +} diff --git a/pkg/demo/demogrpc/demo.go b/pkg/demo/demogrpc/demo.go new file mode 100644 index 0000000..ccdb7ef --- /dev/null +++ b/pkg/demo/demogrpc/demo.go @@ -0,0 +1,86 @@ +package demogrpc + +import ( + "context" + "fmt" + + "github.com/go-resty/resty/v2" + "github.com/rs/zerolog" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" + + grpccodes "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "gitea.libretechconsulting.com/rmcguire/go-app/pkg/otel" + + pb "gitea.libretechconsulting.com/rmcguire/go-server-with-otel/api/demo/app/v1alpha1" + "gitea.libretechconsulting.com/rmcguire/go-server-with-otel/pkg/config" +) + +const ( + defaultFactType = config.TypeRandom + factsBaseURI = "https://uselessfacts.jsph.pl/api/v2/facts" +) + +type DemoGRPCServer struct { + tracer trace.Tracer + ctx context.Context + cfg *config.ServiceConfig + client *resty.Client + pb.UnimplementedDemoAppServiceServer +} + +func NewDemoGRPCServer(ctx context.Context, cfg *config.ServiceConfig) *DemoGRPCServer { + if cfg.Opts == nil { + cfg.Opts = &config.DemoOpts{} + } + + if cfg.Opts.FactType == "" { + cfg.Opts.FactType = config.TypeRandom + } + + return &DemoGRPCServer{ + ctx: ctx, + cfg: cfg, + tracer: otel.GetTracer(ctx, "demoGRPCServer"), + client: resty.New().SetBaseURL(factsBaseURI), + } +} + +func (d *DemoGRPCServer) GetDemo(ctx context.Context, req *pb.GetDemoRequest) ( + *pb.GetDemoResponse, error, +) { + ctx, span := d.tracer.Start(ctx, "getDemo", trace.WithAttributes( + attribute.String("language", req.GetLanguage()), + )) + defer span.End() + + r := d.client.R().SetContext(ctx) + + if req.GetLanguage() != "" { + r = r.SetQueryParam("language", req.GetLanguage()) + } + + fact := new(RandomFact) + r.SetResult(fact) + + resp, err := r.Get(fmt.Sprintf("/%s", d.cfg.Opts.FactType)) + if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + + zerolog.Ctx(d.ctx).Err(err).Send() + + return nil, status.Errorf(grpccodes.Unknown, "%s: %s", + err.Error(), string(resp.Body())) + } + + span.SetAttributes(attribute.String("fact", fact.Text)) + span.SetStatus(codes.Ok, "") + + zerolog.Ctx(d.ctx).Debug().Str("fact", fact.Text).Msg("retrieved fact") + + return fact.FactToProto(), nil +} diff --git a/pkg/demo/demogrpc/fact.go b/pkg/demo/demogrpc/fact.go new file mode 100644 index 0000000..8a1603b --- /dev/null +++ b/pkg/demo/demogrpc/fact.go @@ -0,0 +1,25 @@ +package demogrpc + +import ( + "google.golang.org/protobuf/types/known/timestamppb" + + pb "gitea.libretechconsulting.com/rmcguire/go-server-with-otel/api/demo/app/v1alpha1" +) + +type RandomFact struct { + ID string `json:"id,omitempty"` + Text string `json:"text,omitempty"` + Source string `json:"source,omitempty"` + SourceURL string `json:"source_url,omitempty"` + Language string `json:"language,omitempty"` + Permalink string `json:"permalink,omitempty"` +} + +func (f *RandomFact) FactToProto() *pb.GetDemoResponse { + return &pb.GetDemoResponse{ + Timestamp: timestamppb.Now(), + Fact: f.Text, + Source: f.SourceURL, + Language: f.Language, + } +} diff --git a/pkg/demo/demogrpc/server.go b/pkg/demo/demogrpc/server.go new file mode 100644 index 0000000..c8f6bac --- /dev/null +++ b/pkg/demo/demogrpc/server.go @@ -0,0 +1,30 @@ +package demogrpc + +import ( + "gitea.libretechconsulting.com/rmcguire/go-app/pkg/srv/grpc/opts" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + demoAppPb "gitea.libretechconsulting.com/rmcguire/go-server-with-otel/api/demo/app/v1alpha1" +) + +func (ds *DemoGRPCServer) GetDialOpts() []grpc.DialOption { + return []grpc.DialOption{ + // NOTE: Necessary for grpc-gateway to connect to grpc server + // Update if grpc service has credentials, tls, etc.. + grpc.WithTransportCredentials(insecure.NewCredentials()), + } +} + +func (ds *DemoGRPCServer) GetServices() []*opts.GRPCService { + return []*opts.GRPCService{ + { + Name: "Demo App", + Type: &demoAppPb.DemoAppService_ServiceDesc, + Service: ds, + GwRegistrationFuncs: []opts.GwRegistrationFunc{ + demoAppPb.RegisterDemoAppServiceHandler, + }, + }, + } +} diff --git a/pkg/demo/demohttp/server.go b/pkg/demo/demohttp/server.go new file mode 100644 index 0000000..a2603e8 --- /dev/null +++ b/pkg/demo/demohttp/server.go @@ -0,0 +1,44 @@ +package demohttp + +import ( + "context" + "net/http" + + "gitea.libretechconsulting.com/rmcguire/go-app/pkg/srv/http/opts" + + "gitea.libretechconsulting.com/rmcguire/go-server-with-otel/pkg/config" +) + +type DemoHTTPServer struct { + ctx context.Context + cfg *config.ServiceConfig +} + +func NewDemoHTTPServer(ctx context.Context, cfg *config.ServiceConfig) *DemoHTTPServer { + return &DemoHTTPServer{ + ctx: ctx, + cfg: cfg, + } +} + +func (dh *DemoHTTPServer) GetHandleFuncs() []opts.HTTPFunc { + // TODO: Implement real http handle funcs + return []opts.HTTPFunc{ + { + Path: "/test", + HandlerFunc: func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) + w.Write([]byte("unimplemented demo app")) + }, + }, + } +} + +func (dh *DemoHTTPServer) GetHealthCheckFuncs() []opts.HealthCheckFunc { + return []opts.HealthCheckFunc{ + func(ctx context.Context) error { + // TODO: Implement real health checks + return nil + }, + } +} diff --git a/pkg/demo/demomcp/server.go b/pkg/demo/demomcp/server.go new file mode 100644 index 0000000..84e45be --- /dev/null +++ b/pkg/demo/demomcp/server.go @@ -0,0 +1,70 @@ +/* +Package demomcp contains a simple reference implementation of returning a +HTTPHandler for an MCP server with a single tool used to retrieve random facts +*/ +package demomcp + +import ( + "context" + "net/http" + + "gitea.libretechconsulting.com/rmcguire/go-app/pkg/srv/http/opts" + "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/rs/zerolog" + + "gitea.libretechconsulting.com/rmcguire/go-server-with-otel/pkg/config" + "gitea.libretechconsulting.com/rmcguire/go-server-with-otel/pkg/demo/demogrpc" +) + +var DemoMCPImpl = &mcp.Implementation{ + Name: "Demo MCP Server", + Title: "Demo MCP", +} + +type DemoMCPServer struct { + ctx context.Context + cfg *config.ServiceConfig + log *zerolog.Logger + server *mcp.Server + demoGRPC *demogrpc.DemoGRPCServer +} + +func NewDemoMCPServer(ctx context.Context, cfg *config.ServiceConfig) *DemoMCPServer { + return &DemoMCPServer{ + ctx: ctx, + cfg: cfg, + log: zerolog.Ctx(ctx), + demoGRPC: demogrpc.NewDemoGRPCServer(ctx, cfg), + server: mcp.NewServer(DemoMCPImpl, &mcp.ServerOptions{ + Instructions: "Call this demo MCP tool if the user asks useless questions or wants a random fact", + HasTools: true, + }), + } +} + +func (d *DemoMCPServer) GetHandlers() []opts.HTTPHandler { + // NOTE: Add other tools here + d.addDemoRandomFactTool() + + demoHandler := mcp.NewStreamableHTTPHandler(func(*http.Request) *mcp.Server { + return d.server + }, &mcp.StreamableHTTPOptions{}) + + d.log.Debug().Msg("Demo MCP Tool Ready") + + return []opts.HTTPHandler{ + { + Prefix: "/api/mcp", + Handler: demoHandler, + }, + } +} + +func (d *DemoMCPServer) GetHealthCheckFuncs() []opts.HealthCheckFunc { + return []opts.HealthCheckFunc{ + func(ctx context.Context) error { + // TODO: Implement real health checks + return nil + }, + } +} diff --git a/pkg/demo/demomcp/tool.go b/pkg/demo/demomcp/tool.go new file mode 100644 index 0000000..c026a70 --- /dev/null +++ b/pkg/demo/demomcp/tool.go @@ -0,0 +1,60 @@ +package demomcp + +import ( + "context" + + "github.com/modelcontextprotocol/go-sdk/mcp" + "k8s.io/utils/ptr" + + demo "gitea.libretechconsulting.com/rmcguire/go-server-with-otel/api/demo/app/v1alpha1" +) + +var DemoTool = &mcp.Tool{ + Name: "Demo Tool", + Title: "Demo Random Fact Tool", + Description: "Returns a random fact for demo or boredom purposes", + Annotations: &mcp.ToolAnnotations{ + ReadOnlyHint: true, + OpenWorldHint: ptr.To(true), + }, +} + +type DemoToolParams struct { + Language string `json:"language,omitempty" jsonschema:"Random Fact Language Abbreviation (e.g. en)"` +} + +func (d *DemoMCPServer) addDemoRandomFactTool() { + d.server.AddTool( + mcp.ToolFor(DemoTool, d.demoToolHandler), + ) +} + +func (d *DemoMCPServer) demoToolHandler(ctx context.Context, req *mcp.CallToolRequest, args *DemoToolParams) ( + *mcp.CallToolResult, any, error, +) { + d.log.Debug().Str("language", args.Language).Msg("demo fact tool called") + + fact, err := d.demoGRPC.GetDemo(ctx, &demo.GetDemoRequest{ + Language: &args.Language, + }) + if err != nil { + return &mcp.CallToolResult{ + IsError: true, + Meta: mcp.Meta{ + "error": err, + }, + }, nil, err + } + + return &mcp.CallToolResult{ + Meta: mcp.Meta{ + "language": fact.GetLanguage(), + "source": fact.GetSource(), + }, + Content: []mcp.Content{ + &mcp.TextContent{ + Text: fact.GetFact(), + }, + }, + }, nil, nil +} diff --git a/pkg/service/service.go b/pkg/service/service.go new file mode 100644 index 0000000..e7f98ce --- /dev/null +++ b/pkg/service/service.go @@ -0,0 +1,63 @@ +// Package service defines generics around implementation of +// a new service that provides configuration for go-app +// +// The interface is meant to make the template more clear to implement +// without unnecessary changes in main.go +package service + +import ( + "context" + + "gitea.libretechconsulting.com/rmcguire/go-server-with-otel/pkg/config" + + optsgrpc "gitea.libretechconsulting.com/rmcguire/go-app/pkg/srv/grpc/opts" + optshttp "gitea.libretechconsulting.com/rmcguire/go-app/pkg/srv/http/opts" +) + +type ShutdownFunc func(ctx context.Context) (string, error) + +type AppService interface { + Init(ctx context.Context, config *config.ServiceConfig) (ShutdownFunc, error) + GetGRPC() *optsgrpc.AppGRPC + GetHTTP() *optshttp.AppHTTP +} + +// MergeServices is used if multiple services are served by this server +// with their own distinct packages and configuration +// +// You should be overriding the done chan, listener, or any other +// configuration used by the SERVER rather than the individual services. +func MergeServices(ctx context.Context, appServices ...AppService) (*optsgrpc.AppGRPC, *optshttp.AppHTTP) { + mergedGRPC := &optsgrpc.AppGRPC{} + for _, svc := range appServices { + mergedGRPC = mergeGRPC(mergedGRPC, svc.GetGRPC()) + } + + mergedHTTP := &optshttp.AppHTTP{} + for _, svc := range appServices { + mergedHTTP = mergeHTTP(mergedHTTP, svc.GetHTTP()) + } + mergedHTTP.Ctx = ctx + + return mergedGRPC, mergedHTTP +} + +func mergeHTTP(a *optshttp.AppHTTP, b *optshttp.AppHTTP) *optshttp.AppHTTP { + return &optshttp.AppHTTP{ + Funcs: append(a.Funcs, b.Funcs...), + Middleware: append(a.Middleware, b.Middleware...), + Handlers: append(a.Handlers, b.Handlers...), + HealthChecks: append(a.HealthChecks, b.HealthChecks...), + } +} + +func mergeGRPC(a *optsgrpc.AppGRPC, b *optsgrpc.AppGRPC) *optsgrpc.AppGRPC { + return &optsgrpc.AppGRPC{ + Services: append(a.Services, b.Services...), + GRPCOpts: append(a.GRPCOpts, b.GRPCOpts...), + UnaryInterceptors: append(a.UnaryInterceptors, b.UnaryInterceptors...), + StreamInterceptors: append(a.StreamInterceptors, b.StreamInterceptors...), + GRPCDialOpts: append(a.GRPCDialOpts, b.GRPCDialOpts...), + GRPCGatewayOpts: append(a.GRPCGatewayOpts, b.GRPCGatewayOpts...), + } +} diff --git a/proto/demo/app/v1alpha1/app.proto b/proto/demo/app/v1alpha1/app.proto new file mode 100644 index 0000000..e371fc6 --- /dev/null +++ b/proto/demo/app/v1alpha1/app.proto @@ -0,0 +1,29 @@ +syntax = "proto3"; +package demo.app.v1alpha1; + +import "buf/validate/validate.proto"; +import "google/api/annotations.proto"; +import "google/protobuf/timestamp.proto"; + +option go_package = "gitea.libretechconsulting.com/rmcguire/go-server-with-otel/api/v1alpha1/demo"; + +// Options for random fact, in this case +// just a language +message GetDemoRequest { + optional string language = 1 [(buf.validate.field).string.min_len = 2]; +} + +// Returns a randome fact, because this is a demo app +// so what else do we do? +message GetDemoResponse { + google.protobuf.Timestamp timestamp = 1; + string fact = 2; + string source = 3; + string language = 4 [(buf.validate.field).string.min_len = 2]; +} + +service DemoAppService { + rpc GetDemo(GetDemoRequest) returns (GetDemoResponse) { + option (google.api.http) = {get: "/v1alpha1/demo"}; // grpc-gateway endpoint + } +}