diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 0000000..eb8c9f1 --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,11 @@ +{ + "extends": [ + "config:best-practices", + "schedule:weekly", + ":combinePatchMinorReleases", + ":approveMajorUpdates" + ], + "labels": [ + "dependencies" + ] +} \ No newline at end of file diff --git a/.github/workflows/app.yaml b/.github/workflows/app.yaml new file mode 100644 index 0000000..4ff87d2 --- /dev/null +++ b/.github/workflows/app.yaml @@ -0,0 +1,96 @@ +--- +name: app + +on: [push] + +defaults: + run: + working-directory: ./app + +jobs: + prepare: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: jdx/mise-action@v2 + with: + version: 2024.10.7 + install: true + experimental: true + - uses: actions/setup-go@v5 + with: + go-version: '1.23.4' + cache-dependency-path: 'app/go.sum' + - run: go mod tidy + - run: task gen + - name: Save task cache + uses: actions/cache/save@v4 + with: + path: ./.task + key: task-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.task/checksum/*') }} + - name: Check git status + run: if [[ -n $(git status --porcelain) ]]; then git status --porcelain && exit 1; fi + + lint: + needs: [prepare] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Restore task cache + uses: actions/cache/restore@v4 + with: + path: ./.task + key: task-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.task/checksum/*') }} + - uses: jdx/mise-action@v2 + with: + version: 2024.10.7 + install: false + - uses: actions/setup-go@v5 + with: + go-version: '1.23.4' + cache-dependency-path: 'app/go.sum' + - uses: golangci/golangci-lint-action@v6 + with: + version: v1.61.0 + working-directory: ./app + args: --path-prefix=./ + + test: + needs: [prepare] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Restore task cache + uses: actions/cache/restore@v4 + with: + path: ./.task + key: task-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.task/checksum/*') }} + - uses: jdx/mise-action@v2 + with: + version: 2024.10.7 + install: false + - uses: actions/setup-go@v5 + with: + go-version: '1.23.4' + cache-dependency-path: 'app/go.sum' + - run: task test + + build: + needs: [prepare] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Restore task cache + uses: actions/cache/restore@v4 + with: + path: ./.task + key: task-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.task/checksum/*') }} + - uses: jdx/mise-action@v2 + with: + version: 2024.10.7 + install: false + - uses: actions/setup-go@v5 + with: + go-version: '1.23.4' + cache-dependency-path: 'app/go.sum' + - run: task build diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b018f68 --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# Misc +.DS_Store + +# Environment +.env +.env.local +.env.*.local + +# Editor +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# Go +*.exe +*.exe~ +*.dll +*.so +*.dylib +*.test +*.out +go.work + +# Dist +dist/ + +# Terraform +**/.terraform/* +*.tfstate +*.tfstate.* +*.tfvars +.terraform.tfstate.lock.info +!infra/spacelift/init/*.tfstate +!infra/spacelift/init/*.tfstate.* + +# Task +.task/ + +# Mise +.mise.local.toml +.mise.*.local.toml \ No newline at end of file diff --git a/.mise.toml b/.mise.toml new file mode 100644 index 0000000..2dff477 --- /dev/null +++ b/.mise.toml @@ -0,0 +1,13 @@ +[tools] +atlas = "0.29.0" +mkcert = "1.4.4" +go = "1.23.4" +golangci-lint = "1.62.2" +opentofu = "1.8.7" +protoc = "29.1" +protoc-gen-go = "1.35.2" +protoc-gen-go-grpc = "1.5.1" +task = "3.40.1" + +"go:github.com/sqlc-dev/sqlc/cmd/sqlc" = "1.27.0" +"go:goa.design/goa/v3/cmd/goa" = "3.19.1" diff --git a/Taskfile.yaml b/Taskfile.yaml new file mode 100644 index 0000000..4770261 --- /dev/null +++ b/Taskfile.yaml @@ -0,0 +1,151 @@ +--- +version: '3' + +vars: + VERSION: + sh: git describe --tags --exact-match HEAD 2>/dev/null || echo "0.0.0" + GIT_COMMIT: + sh: git rev-parse --short HEAD + LDFLAGS: + - '-X github.com/jace-ys/countup/internal/versioninfo.Version={{ .VERSION }}' + - '-X github.com/jace-ys/countup/internal/versioninfo.CommitSHA={{ .GIT_COMMIT }}' + +tasks: + run:server: + deps: ['gen'] + dir: app + cmds: + - go run ./cmd/countup/... server {{ .CLI_ARGS }} + env: + DEBUG: true + OTEL_GO_X_EXEMPLAR: true + OTEL_RESOURCE_ATTRIBUTES: tier=app,environment=local + DATABASE_CONNECTION_URI: postgresql://countup:countup@localhost:5432/countup + OAUTH_REDIRECT_URL: https://localhost:4043/login/google/callback + + run:client: + deps: ['gen'] + dir: app + cmds: + - go run ./cmd/countup-cli/... {{ .CLI_ARGS }} + + test: + deps: ['gen'] + dir: app + cmds: + - go test -race ./... + + lint: + deps: ['gen'] + dir: app + cmds: + - golangci-lint run ./... + + gen: + dir: app + cmds: + - task: gen:api:v1 + - task: gen:sqlc + + gen:api:*: + internal: true + dir: app + vars: + API_VERSION: '{{ index .MATCH 0 }}' + cmds: + - goa gen github.com/jace-ys/countup/api/{{ .API_VERSION }} -o api/{{ .API_VERSION }} + sources: + - api/{{ .API_VERSION }}/*.go + generates: + - api/{{ .API_VERSION }}/gen/**/* + + gen:sqlc: + internal: true + deps: ['migration:plan'] + dir: app + cmds: + - sqlc generate + sources: + - sqlc.yaml + - schema/*.sql + + migration:new: + dir: app + cmds: + - atlas migrate new --env local {{ .NAME }} + requires: + vars: [NAME] + + migration:plan: + dir: app + cmds: + - atlas migrate diff --env local {{ .NAME }} + sources: + - schema/schema.sql + - schema/migrations/*.sql + generates: + - schema/migrations/*.sql + + migration:checksum: + dir: app + cmds: + - atlas migrate hash --env local + + build: + deps: ['gen'] + dir: app + cmds: + - task: build:server + - task: build:client + - task: build:image + + build:server: + internal: true + dir: app + cmds: + - go build -ldflags='{{ .LDFLAGS | join " " }}' -o ./dist/ ./cmd/countup/... + + build:client: + internal: true + dir: app + cmds: + - go build -ldflags='{{ .LDFLAGS | join " " }}' -o ./dist/ ./cmd/countup-cli/... + + build:image: + internal: true + dir: app + cmds: + - docker build --build-arg LDFLAGS='{{ .LDFLAGS | join " " }}' -t jace-ys/countup:{{ .VERSION }} . + + certs:traefik: + dir: infra/environments/local/compose/traefik/certs + cmds: + - mkcert -install + - mkcert -cert-file traefik.cert -key-file traefik.key localhost 127.0.0.1 ::1 + status: + - test -f traefik.cert + - test -f traefik.key + + compose: + ignore_error: true + deps: ['gen', 'certs:traefik'] + cmds: + - docker compose --profile apps up --build {{ .CLI_ARGS }} + - defer: {task: 'compose:down'} + + compose:infra: + ignore_error: true + deps: ['gen', 'certs:traefik'] + cmds: + - docker compose up {{ .CLI_ARGS }} + - defer: {task: 'compose:down'} + + compose:down: + ignore_error: true + cmds: + - docker compose down -v --remove-orphans + + spacelift:init: + dir: infra/spacelift/init + cmds: + - tofu apply diff --git a/app/.golangci.yaml b/app/.golangci.yaml new file mode 100644 index 0000000..0878943 --- /dev/null +++ b/app/.golangci.yaml @@ -0,0 +1,139 @@ +--- +linters-settings: + cyclop: + package-average: 10.0 + + errcheck: + check-type-assertions: true + check-blank: true + exclude-functions: + - (github.com/jackc/pgx/v5.Tx).Rollback + + exhaustive: + check: + - switch + - map + + funlen: + lines: 100 + statements: 60 + ignore-comments: true + + gci: + sections: + - standard + - default + - localmodule + + gocognit: + min-complexity: 20 + + gocritic: + disabled-checks: + - singleCaseSwitch + + govet: + enable-all: true + disable: + - fieldalignment + - shadow + + nakedret: + max-func-lines: 0 + + perfsprint: + strconcat: false + + wrapcheck: + ignoreSigs: + - .Errorf( + - errors.New( + - errors.Unwrap( + - errors.Join( + - .Wrap( + - .Wrapf( + - .WithMessage( + - .WithMessagef( + - .WithStack( + - (context.Context).Err() + - (*github.com/jace-ys/countup/internal/postgres.Pool).WithinTx + +linters: + disable-all: true + enable: + - errcheck + - gosimple + - govet + - ineffassign + - staticcheck + - unused + + - asasalint + - asciicheck + - bidichk + - bodyclose + - canonicalheader + - copyloopvar + - cyclop + - decorder + - durationcheck + - errname + - errorlint + - exhaustive + - fatcontext + - funlen + - gci + - ginkgolinter + - gocheckcompilerdirectives + - gochecksumtype + - gocognit + - goconst + - gocritic + - gocyclo + - godot + - godox + - goimports + - gomoddirectives + - gomodguard + - goprintffuncname + - gosec + - intrange + - loggercheck + - makezero + - mirror + - musttag + - nakedret + - nestif + - nilerr + - nilnil + - noctx + - nolintlint + - nosprintfhostport + - perfsprint + - prealloc + - predeclared + - promlinter + - protogetter + - reassign + - spancheck + - sqlclosecheck + - tenv + - testableexamples + - testifylint + - testpackage + - tparallel + - unconvert + - usestdlibvars + - wastedassign + - whitespace + - wrapcheck + +issues: + exclude-use-default: true + max-issues-per-linter: 0 + max-same-issues: 0 + + exclude-dirs: + - ^api/v[0-9]+/gen$ + - ^cmd/countup-cli$ + - ^internal/handler/teapot$ diff --git a/app/Dockerfile b/app/Dockerfile new file mode 100644 index 0000000..a9a3053 --- /dev/null +++ b/app/Dockerfile @@ -0,0 +1,16 @@ +FROM golang:1.23.4 AS builder + +ARG LDFLAGS +ENV LDFLAGS=${LDFLAGS} + +WORKDIR /src +COPY go.mod go.sum ./ +RUN go mod download +COPY . ./ +RUN CGO_ENABLED=0 go build -ldflags="${LDFLAGS}" -o dist/ ./cmd/countup/... + +FROM gcr.io/distroless/static-debian12 +WORKDIR /app +USER nonroot:nonroot +COPY --from=builder --chown=nonroot:nonroot /src/dist /app/bin +ENTRYPOINT ["/app/bin/countup"] \ No newline at end of file diff --git a/app/api/v1/constants.go b/app/api/v1/constants.go new file mode 100644 index 0000000..7902b03 --- /dev/null +++ b/app/api/v1/constants.go @@ -0,0 +1,33 @@ +package apiv1 + +import ( + "embed" + + . "goa.design/goa/v3/dsl" +) + +//go:embed gen/http/*.json gen/http/*.yaml +var OpenAPIFS embed.FS + +const ( + AuthScopeAPIUser = "api.user" +) + +const ( + ErrCodeUnauthorized = "unauthorized" + ErrCodeForbidden = "forbidden" + ErrCodeIncompleteAuthInfo = "incomplete_auth_info" + ErrCodeExistingIncrementRequest = "existing_increment_request" +) + +const ( + CookieNameWebSession = "session_cookie:countup.session" +) + +var CounterInfo = ResultType("application/vnd.countup.counter-info`", "CounterInfo", func() { + Field(1, "count", Int32) + Field(2, "last_increment_by", String) + Field(3, "last_increment_at", String) + Field(4, "next_finalize_at", String) + Required("count", "last_increment_by", "last_increment_at", "next_finalize_at") +}) diff --git a/app/api/v1/countup.go b/app/api/v1/countup.go new file mode 100644 index 0000000..57e3968 --- /dev/null +++ b/app/api/v1/countup.go @@ -0,0 +1,268 @@ +package apiv1 + +import ( + . "goa.design/goa/v3/dsl" +) + +var _ = API("countup", func() { + Title("Count Up") + Description("A production-ready Go service deployed on Kubernetes") + Version("1.0.0") + Server("countup", func() { + Services("api", "web", "teapot") + Host("local-http", func() { + URI("http://localhost:8080") + }) + Host("local-grpc", func() { + URI("grpc://localhost:8081") + }) + }) +}) + +var JWTAuth = JWTSecurity("jwt", func() { + Scope(AuthScopeAPIUser) +}) + +var _ = Service("api", func() { + Security(JWTAuth, func() { + Scope(AuthScopeAPIUser) + }) + + Error(ErrCodeUnauthorized) + Error(ErrCodeForbidden) + + HTTP(func() { + Path("/api/v1") + Response(ErrCodeUnauthorized, StatusUnauthorized) + Response(ErrCodeForbidden, StatusForbidden) + }) + + GRPC(func() { + Response(ErrCodeUnauthorized, CodeUnauthenticated) + Response(ErrCodeForbidden, CodePermissionDenied) + }) + + Method("AuthToken", func() { + NoSecurity() + + Error(ErrCodeIncompleteAuthInfo) + + Payload(func() { + Field(1, "provider", String, func() { + Enum("google") + }) + Field(2, "access_token", String) + Required("provider", "access_token") + }) + + Result(func() { + Field(1, "token", String) + Required("token") + }) + + HTTP(func() { + POST("/auth/token") + Response(StatusOK) + Response(ErrCodeIncompleteAuthInfo, StatusUnauthorized) + }) + + GRPC(func() { + Response(CodeOK) + Response(ErrCodeIncompleteAuthInfo, CodePermissionDenied) + }) + }) + + Method("CounterGet", func() { + NoSecurity() + + Result(CounterInfo) + + HTTP(func() { + GET("/counter") + Response(StatusOK) + }) + + GRPC(func() { + Response(CodeOK) + }) + }) + + Method("CounterIncrement", func() { + Error(ErrCodeExistingIncrementRequest, func() { + Temporary() + }) + + Payload(func() { + TokenField(1, "token", String) + }) + + Result(CounterInfo) + + HTTP(func() { + POST("/counter") + Response(StatusAccepted) + Response(ErrCodeExistingIncrementRequest, StatusTooManyRequests) + }) + + GRPC(func() { + Response(CodeOK) + Response(ErrCodeExistingIncrementRequest, CodeAlreadyExists) + }) + }) + + Files("/openapi.json", "gen/http/openapi3.json") +}) + +var _ = Service("web", func() { + Error(ErrCodeUnauthorized) + + HTTP(func() { + Path("/") + Response(ErrCodeUnauthorized, StatusUnauthorized) + }) + + withSessionCookie := func() { + Cookie(CookieNameWebSession) + CookieSameSite(CookieSameSiteLax) + CookieMaxAge(86400) + CookieHTTPOnly() + CookieSecure() + CookiePath("/") + } + + Method("Index", func() { + Result(Bytes) + HTTP(func() { + GET("/") + Response(StatusOK, func() { + ContentType("text/html") + }) + }) + }) + + Method("Another", func() { + Result(Bytes) + HTTP(func() { + GET("/another") + Response(StatusOK, func() { + ContentType("text/html") + }) + }) + }) + + Method("LoginGoogle", func() { + Result(func() { + Attribute("redirect_url", String) + Attribute("session_cookie", String) + Required("redirect_url", "session_cookie") + }) + + HTTP(func() { + GET("/login/google") + Response(StatusFound, func() { + Header("redirect_url:Location", String) + withSessionCookie() + }) + }) + }) + + Method("LoginGoogleCallback", func() { + Payload(func() { + Attribute("code", String) + Attribute("state", String) + Attribute("session_cookie", String) + Required("code", "state", "session_cookie") + }) + + Result(func() { + Attribute("redirect_url", String) + Attribute("session_cookie", String) + Required("redirect_url", "session_cookie") + }) + + HTTP(func() { + GET("/login/google/callback") + Params(func() { + Param("code", String) + Param("state", String) + Required("code", "state") + }) + Cookie(CookieNameWebSession) + Response(StatusFound, func() { + Header("redirect_url:Location", String) + withSessionCookie() + }) + }) + }) + + Method("Logout", func() { + Payload(func() { + Attribute("session_cookie", String) + Required("session_cookie") + }) + + Result(func() { + Attribute("redirect_url", String) + Attribute("session_cookie", String) + Required("redirect_url", "session_cookie") + }) + + HTTP(func() { + GET("/logout") + Cookie(CookieNameWebSession) + Response(StatusFound, func() { + Header("redirect_url:Location", String) + withSessionCookie() + }) + }) + }) + + Method("SessionToken", func() { + Payload(func() { + Attribute("session_cookie", String) + Required("session_cookie") + }) + + Result(func() { + Attribute("token", String) + Required("token") + }) + + HTTP(func() { + GET("/session/token") + Cookie(CookieNameWebSession) + Response(StatusOK, func() { + ContentType("application/json") + }) + }) + }) + + Files("/static/*", "static/") +}) + +var _ = Service("teapot", func() { + Error("unwell") + + Method("Echo", func() { + Payload(func() { + Field(1, "text", String) + Required("text") + }) + + Result(func() { + Field(1, "text", String) + Required("text") + }) + + HTTP(func() { + POST("/echo") + Response(StatusOK) + }) + + GRPC(func() { + Response(CodeOK) + }) + }) + + Files("/openapi.json", "gen/http/openapi3.json") +}) diff --git a/app/api/v1/gen/api/client.go b/app/api/v1/gen/api/client.go new file mode 100644 index 0000000..70aac8c --- /dev/null +++ b/app/api/v1/gen/api/client.go @@ -0,0 +1,74 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// api client +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package api + +import ( + "context" + + goa "goa.design/goa/v3/pkg" +) + +// Client is the "api" service client. +type Client struct { + AuthTokenEndpoint goa.Endpoint + CounterGetEndpoint goa.Endpoint + CounterIncrementEndpoint goa.Endpoint +} + +// NewClient initializes a "api" service client given the endpoints. +func NewClient(authToken, counterGet, counterIncrement goa.Endpoint) *Client { + return &Client{ + AuthTokenEndpoint: authToken, + CounterGetEndpoint: counterGet, + CounterIncrementEndpoint: counterIncrement, + } +} + +// AuthToken calls the "AuthToken" endpoint of the "api" service. +// AuthToken may return the following errors: +// - "incomplete_auth_info" (type *goa.ServiceError) +// - "unauthorized" (type *goa.ServiceError) +// - "forbidden" (type *goa.ServiceError) +// - error: internal error +func (c *Client) AuthToken(ctx context.Context, p *AuthTokenPayload) (res *AuthTokenResult, err error) { + var ires any + ires, err = c.AuthTokenEndpoint(ctx, p) + if err != nil { + return + } + return ires.(*AuthTokenResult), nil +} + +// CounterGet calls the "CounterGet" endpoint of the "api" service. +// CounterGet may return the following errors: +// - "unauthorized" (type *goa.ServiceError) +// - "forbidden" (type *goa.ServiceError) +// - error: internal error +func (c *Client) CounterGet(ctx context.Context) (res *CounterInfo, err error) { + var ires any + ires, err = c.CounterGetEndpoint(ctx, nil) + if err != nil { + return + } + return ires.(*CounterInfo), nil +} + +// CounterIncrement calls the "CounterIncrement" endpoint of the "api" service. +// CounterIncrement may return the following errors: +// - "existing_increment_request" (type *goa.ServiceError) +// - "unauthorized" (type *goa.ServiceError) +// - "forbidden" (type *goa.ServiceError) +// - error: internal error +func (c *Client) CounterIncrement(ctx context.Context, p *CounterIncrementPayload) (res *CounterInfo, err error) { + var ires any + ires, err = c.CounterIncrementEndpoint(ctx, p) + if err != nil { + return + } + return ires.(*CounterInfo), nil +} diff --git a/app/api/v1/gen/api/endpoints.go b/app/api/v1/gen/api/endpoints.go new file mode 100644 index 0000000..f3dbb93 --- /dev/null +++ b/app/api/v1/gen/api/endpoints.go @@ -0,0 +1,90 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// api endpoints +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package api + +import ( + "context" + + goa "goa.design/goa/v3/pkg" + "goa.design/goa/v3/security" +) + +// Endpoints wraps the "api" service endpoints. +type Endpoints struct { + AuthToken goa.Endpoint + CounterGet goa.Endpoint + CounterIncrement goa.Endpoint +} + +// NewEndpoints wraps the methods of the "api" service with endpoints. +func NewEndpoints(s Service) *Endpoints { + // Casting service to Auther interface + a := s.(Auther) + return &Endpoints{ + AuthToken: NewAuthTokenEndpoint(s), + CounterGet: NewCounterGetEndpoint(s), + CounterIncrement: NewCounterIncrementEndpoint(s, a.JWTAuth), + } +} + +// Use applies the given middleware to all the "api" service endpoints. +func (e *Endpoints) Use(m func(goa.Endpoint) goa.Endpoint) { + e.AuthToken = m(e.AuthToken) + e.CounterGet = m(e.CounterGet) + e.CounterIncrement = m(e.CounterIncrement) +} + +// NewAuthTokenEndpoint returns an endpoint function that calls the method +// "AuthToken" of service "api". +func NewAuthTokenEndpoint(s Service) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + p := req.(*AuthTokenPayload) + return s.AuthToken(ctx, p) + } +} + +// NewCounterGetEndpoint returns an endpoint function that calls the method +// "CounterGet" of service "api". +func NewCounterGetEndpoint(s Service) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + res, err := s.CounterGet(ctx) + if err != nil { + return nil, err + } + vres := NewViewedCounterInfo(res, "default") + return vres, nil + } +} + +// NewCounterIncrementEndpoint returns an endpoint function that calls the +// method "CounterIncrement" of service "api". +func NewCounterIncrementEndpoint(s Service, authJWTFn security.AuthJWTFunc) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + p := req.(*CounterIncrementPayload) + var err error + sc := security.JWTScheme{ + Name: "jwt", + Scopes: []string{"api.user"}, + RequiredScopes: []string{"api.user"}, + } + var token string + if p.Token != nil { + token = *p.Token + } + ctx, err = authJWTFn(ctx, token, &sc) + if err != nil { + return nil, err + } + res, err := s.CounterIncrement(ctx, p) + if err != nil { + return nil, err + } + vres := NewViewedCounterInfo(res, "default") + return vres, nil + } +} diff --git a/app/api/v1/gen/api/service.go b/app/api/v1/gen/api/service.go new file mode 100644 index 0000000..d2688fb --- /dev/null +++ b/app/api/v1/gen/api/service.go @@ -0,0 +1,137 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// api service +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package api + +import ( + "context" + + apiviews "github.com/jace-ys/countup/api/v1/gen/api/views" + goa "goa.design/goa/v3/pkg" + "goa.design/goa/v3/security" +) + +// Service is the api service interface. +type Service interface { + // AuthToken implements AuthToken. + AuthToken(context.Context, *AuthTokenPayload) (res *AuthTokenResult, err error) + // CounterGet implements CounterGet. + CounterGet(context.Context) (res *CounterInfo, err error) + // CounterIncrement implements CounterIncrement. + CounterIncrement(context.Context, *CounterIncrementPayload) (res *CounterInfo, err error) +} + +// Auther defines the authorization functions to be implemented by the service. +type Auther interface { + // JWTAuth implements the authorization logic for the JWT security scheme. + JWTAuth(ctx context.Context, token string, schema *security.JWTScheme) (context.Context, error) +} + +// APIName is the name of the API as defined in the design. +const APIName = "countup" + +// APIVersion is the version of the API as defined in the design. +const APIVersion = "1.0.0" + +// ServiceName is the name of the service as defined in the design. This is the +// same value that is set in the endpoint request contexts under the ServiceKey +// key. +const ServiceName = "api" + +// MethodNames lists the service method names as defined in the design. These +// are the same values that are set in the endpoint request contexts under the +// MethodKey key. +var MethodNames = [3]string{"AuthToken", "CounterGet", "CounterIncrement"} + +// AuthTokenPayload is the payload type of the api service AuthToken method. +type AuthTokenPayload struct { + Provider string + AccessToken string +} + +// AuthTokenResult is the result type of the api service AuthToken method. +type AuthTokenResult struct { + Token string +} + +// CounterIncrementPayload is the payload type of the api service +// CounterIncrement method. +type CounterIncrementPayload struct { + Token *string +} + +// CounterInfo is the result type of the api service CounterGet method. +type CounterInfo struct { + Count int32 + LastIncrementBy string + LastIncrementAt string + NextFinalizeAt string +} + +// MakeUnauthorized builds a goa.ServiceError from an error. +func MakeUnauthorized(err error) *goa.ServiceError { + return goa.NewServiceError(err, "unauthorized", false, false, false) +} + +// MakeForbidden builds a goa.ServiceError from an error. +func MakeForbidden(err error) *goa.ServiceError { + return goa.NewServiceError(err, "forbidden", false, false, false) +} + +// MakeIncompleteAuthInfo builds a goa.ServiceError from an error. +func MakeIncompleteAuthInfo(err error) *goa.ServiceError { + return goa.NewServiceError(err, "incomplete_auth_info", false, false, false) +} + +// MakeExistingIncrementRequest builds a goa.ServiceError from an error. +func MakeExistingIncrementRequest(err error) *goa.ServiceError { + return goa.NewServiceError(err, "existing_increment_request", false, true, false) +} + +// NewCounterInfo initializes result type CounterInfo from viewed result type +// CounterInfo. +func NewCounterInfo(vres *apiviews.CounterInfo) *CounterInfo { + return newCounterInfo(vres.Projected) +} + +// NewViewedCounterInfo initializes viewed result type CounterInfo from result +// type CounterInfo using the given view. +func NewViewedCounterInfo(res *CounterInfo, view string) *apiviews.CounterInfo { + p := newCounterInfoView(res) + return &apiviews.CounterInfo{Projected: p, View: "default"} +} + +// newCounterInfo converts projected type CounterInfo to service type +// CounterInfo. +func newCounterInfo(vres *apiviews.CounterInfoView) *CounterInfo { + res := &CounterInfo{} + if vres.Count != nil { + res.Count = *vres.Count + } + if vres.LastIncrementBy != nil { + res.LastIncrementBy = *vres.LastIncrementBy + } + if vres.LastIncrementAt != nil { + res.LastIncrementAt = *vres.LastIncrementAt + } + if vres.NextFinalizeAt != nil { + res.NextFinalizeAt = *vres.NextFinalizeAt + } + return res +} + +// newCounterInfoView projects result type CounterInfo to projected type +// CounterInfoView using the "default" view. +func newCounterInfoView(res *CounterInfo) *apiviews.CounterInfoView { + vres := &apiviews.CounterInfoView{ + Count: &res.Count, + LastIncrementBy: &res.LastIncrementBy, + LastIncrementAt: &res.LastIncrementAt, + NextFinalizeAt: &res.NextFinalizeAt, + } + return vres +} diff --git a/app/api/v1/gen/api/views/view.go b/app/api/v1/gen/api/views/view.go new file mode 100644 index 0000000..9453fa3 --- /dev/null +++ b/app/api/v1/gen/api/views/view.go @@ -0,0 +1,71 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// api views +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package views + +import ( + goa "goa.design/goa/v3/pkg" +) + +// CounterInfo is the viewed result type that is projected based on a view. +type CounterInfo struct { + // Type to project + Projected *CounterInfoView + // View to render + View string +} + +// CounterInfoView is a type that runs validations on a projected type. +type CounterInfoView struct { + Count *int32 + LastIncrementBy *string + LastIncrementAt *string + NextFinalizeAt *string +} + +var ( + // CounterInfoMap is a map indexing the attribute names of CounterInfo by view + // name. + CounterInfoMap = map[string][]string{ + "default": { + "count", + "last_increment_by", + "last_increment_at", + "next_finalize_at", + }, + } +) + +// ValidateCounterInfo runs the validations defined on the viewed result type +// CounterInfo. +func ValidateCounterInfo(result *CounterInfo) (err error) { + switch result.View { + case "default", "": + err = ValidateCounterInfoView(result.Projected) + default: + err = goa.InvalidEnumValueError("view", result.View, []any{"default"}) + } + return +} + +// ValidateCounterInfoView runs the validations defined on CounterInfoView +// using the "default" view. +func ValidateCounterInfoView(result *CounterInfoView) (err error) { + if result.Count == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("count", "result")) + } + if result.LastIncrementBy == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("last_increment_by", "result")) + } + if result.LastIncrementAt == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("last_increment_at", "result")) + } + if result.NextFinalizeAt == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("next_finalize_at", "result")) + } + return +} diff --git a/app/api/v1/gen/grpc/api/client/cli.go b/app/api/v1/gen/grpc/api/client/cli.go new file mode 100644 index 0000000..e744c2a --- /dev/null +++ b/app/api/v1/gen/grpc/api/client/cli.go @@ -0,0 +1,52 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// api gRPC client CLI support package +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package client + +import ( + "encoding/json" + "fmt" + + api "github.com/jace-ys/countup/api/v1/gen/api" + apipb "github.com/jace-ys/countup/api/v1/gen/grpc/api/pb" +) + +// BuildAuthTokenPayload builds the payload for the api AuthToken endpoint from +// CLI flags. +func BuildAuthTokenPayload(apiAuthTokenMessage string) (*api.AuthTokenPayload, error) { + var err error + var message apipb.AuthTokenRequest + { + if apiAuthTokenMessage != "" { + err = json.Unmarshal([]byte(apiAuthTokenMessage), &message) + if err != nil { + return nil, fmt.Errorf("invalid JSON for message, \nerror: %s, \nexample of valid JSON:\n%s", err, "'{\n \"access_token\": \"Est sunt voluptatem reprehenderit neque.\",\n \"provider\": \"google\"\n }'") + } + } + } + v := &api.AuthTokenPayload{ + Provider: message.Provider, + AccessToken: message.AccessToken, + } + + return v, nil +} + +// BuildCounterIncrementPayload builds the payload for the api CounterIncrement +// endpoint from CLI flags. +func BuildCounterIncrementPayload(apiCounterIncrementToken string) (*api.CounterIncrementPayload, error) { + var token *string + { + if apiCounterIncrementToken != "" { + token = &apiCounterIncrementToken + } + } + v := &api.CounterIncrementPayload{} + v.Token = token + + return v, nil +} diff --git a/app/api/v1/gen/grpc/api/client/client.go b/app/api/v1/gen/grpc/api/client/client.go new file mode 100644 index 0000000..f08ac6e --- /dev/null +++ b/app/api/v1/gen/grpc/api/client/client.go @@ -0,0 +1,88 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// api gRPC client +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package client + +import ( + "context" + + apipb "github.com/jace-ys/countup/api/v1/gen/grpc/api/pb" + goagrpc "goa.design/goa/v3/grpc" + goapb "goa.design/goa/v3/grpc/pb" + goa "goa.design/goa/v3/pkg" + "google.golang.org/grpc" +) + +// Client lists the service endpoint gRPC clients. +type Client struct { + grpccli apipb.APIClient + opts []grpc.CallOption +} // NewClient instantiates gRPC client for all the api service servers. +func NewClient(cc *grpc.ClientConn, opts ...grpc.CallOption) *Client { + return &Client{ + grpccli: apipb.NewAPIClient(cc), + opts: opts, + } +} // AuthToken calls the "AuthToken" function in apipb.APIClient interface. +func (c *Client) AuthToken() goa.Endpoint { + return func(ctx context.Context, v any) (any, error) { + inv := goagrpc.NewInvoker( + BuildAuthTokenFunc(c.grpccli, c.opts...), + EncodeAuthTokenRequest, + DecodeAuthTokenResponse) + res, err := inv.Invoke(ctx, v) + if err != nil { + resp := goagrpc.DecodeError(err) + switch message := resp.(type) { + case *goapb.ErrorResponse: + return nil, goagrpc.NewServiceError(message) + default: + return nil, goa.Fault(err.Error()) + } + } + return res, nil + } +} // CounterGet calls the "CounterGet" function in apipb.APIClient interface. +func (c *Client) CounterGet() goa.Endpoint { + return func(ctx context.Context, v any) (any, error) { + inv := goagrpc.NewInvoker( + BuildCounterGetFunc(c.grpccli, c.opts...), + nil, + DecodeCounterGetResponse) + res, err := inv.Invoke(ctx, v) + if err != nil { + resp := goagrpc.DecodeError(err) + switch message := resp.(type) { + case *goapb.ErrorResponse: + return nil, goagrpc.NewServiceError(message) + default: + return nil, goa.Fault(err.Error()) + } + } + return res, nil + } +} // CounterIncrement calls the "CounterIncrement" function in apipb.APIClient +// interface. +func (c *Client) CounterIncrement() goa.Endpoint { + return func(ctx context.Context, v any) (any, error) { + inv := goagrpc.NewInvoker( + BuildCounterIncrementFunc(c.grpccli, c.opts...), + EncodeCounterIncrementRequest, + DecodeCounterIncrementResponse) + res, err := inv.Invoke(ctx, v) + if err != nil { + resp := goagrpc.DecodeError(err) + switch message := resp.(type) { + case *goapb.ErrorResponse: + return nil, goagrpc.NewServiceError(message) + default: + return nil, goa.Fault(err.Error()) + } + } + return res, nil + } +} diff --git a/app/api/v1/gen/grpc/api/client/encode_decode.go b/app/api/v1/gen/grpc/api/client/encode_decode.go new file mode 100644 index 0000000..9e4e2ce --- /dev/null +++ b/app/api/v1/gen/grpc/api/client/encode_decode.go @@ -0,0 +1,130 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// api gRPC client encoders and decoders +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package client + +import ( + "context" + + api "github.com/jace-ys/countup/api/v1/gen/api" + apiviews "github.com/jace-ys/countup/api/v1/gen/api/views" + apipb "github.com/jace-ys/countup/api/v1/gen/grpc/api/pb" + goagrpc "goa.design/goa/v3/grpc" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" +) + +// BuildAuthTokenFunc builds the remote method to invoke for "api" service +// "AuthToken" endpoint. +func BuildAuthTokenFunc(grpccli apipb.APIClient, cliopts ...grpc.CallOption) goagrpc.RemoteFunc { + return func(ctx context.Context, reqpb any, opts ...grpc.CallOption) (any, error) { + for _, opt := range cliopts { + opts = append(opts, opt) + } + if reqpb != nil { + return grpccli.AuthToken(ctx, reqpb.(*apipb.AuthTokenRequest), opts...) + } + return grpccli.AuthToken(ctx, &apipb.AuthTokenRequest{}, opts...) + } +} + +// EncodeAuthTokenRequest encodes requests sent to api AuthToken endpoint. +func EncodeAuthTokenRequest(ctx context.Context, v any, md *metadata.MD) (any, error) { + payload, ok := v.(*api.AuthTokenPayload) + if !ok { + return nil, goagrpc.ErrInvalidType("api", "AuthToken", "*api.AuthTokenPayload", v) + } + return NewProtoAuthTokenRequest(payload), nil +} + +// DecodeAuthTokenResponse decodes responses from the api AuthToken endpoint. +func DecodeAuthTokenResponse(ctx context.Context, v any, hdr, trlr metadata.MD) (any, error) { + message, ok := v.(*apipb.AuthTokenResponse) + if !ok { + return nil, goagrpc.ErrInvalidType("api", "AuthToken", "*apipb.AuthTokenResponse", v) + } + res := NewAuthTokenResult(message) + return res, nil +} // BuildCounterGetFunc builds the remote method to invoke for "api" service +// "CounterGet" endpoint. +func BuildCounterGetFunc(grpccli apipb.APIClient, cliopts ...grpc.CallOption) goagrpc.RemoteFunc { + return func(ctx context.Context, reqpb any, opts ...grpc.CallOption) (any, error) { + for _, opt := range cliopts { + opts = append(opts, opt) + } + if reqpb != nil { + return grpccli.CounterGet(ctx, reqpb.(*apipb.CounterGetRequest), opts...) + } + return grpccli.CounterGet(ctx, &apipb.CounterGetRequest{}, opts...) + } +} + +// DecodeCounterGetResponse decodes responses from the api CounterGet endpoint. +func DecodeCounterGetResponse(ctx context.Context, v any, hdr, trlr metadata.MD) (any, error) { + var view string + { + if vals := hdr.Get("goa-view"); len(vals) > 0 { + view = vals[0] + } + } + message, ok := v.(*apipb.CounterGetResponse) + if !ok { + return nil, goagrpc.ErrInvalidType("api", "CounterGet", "*apipb.CounterGetResponse", v) + } + res := NewCounterGetResult(message) + vres := &apiviews.CounterInfo{Projected: res, View: view} + if err := apiviews.ValidateCounterInfo(vres); err != nil { + return nil, err + } + return api.NewCounterInfo(vres), nil +} // BuildCounterIncrementFunc builds the remote method to invoke for "api" +// service "CounterIncrement" endpoint. +func BuildCounterIncrementFunc(grpccli apipb.APIClient, cliopts ...grpc.CallOption) goagrpc.RemoteFunc { + return func(ctx context.Context, reqpb any, opts ...grpc.CallOption) (any, error) { + for _, opt := range cliopts { + opts = append(opts, opt) + } + if reqpb != nil { + return grpccli.CounterIncrement(ctx, reqpb.(*apipb.CounterIncrementRequest), opts...) + } + return grpccli.CounterIncrement(ctx, &apipb.CounterIncrementRequest{}, opts...) + } +} + +// EncodeCounterIncrementRequest encodes requests sent to api CounterIncrement +// endpoint. +func EncodeCounterIncrementRequest(ctx context.Context, v any, md *metadata.MD) (any, error) { + payload, ok := v.(*api.CounterIncrementPayload) + if !ok { + return nil, goagrpc.ErrInvalidType("api", "CounterIncrement", "*api.CounterIncrementPayload", v) + } + if payload.Token != nil { + (*md).Append("authorization", *payload.Token) + } + return NewProtoCounterIncrementRequest(), nil +} + +// DecodeCounterIncrementResponse decodes responses from the api +// CounterIncrement endpoint. +func DecodeCounterIncrementResponse(ctx context.Context, v any, hdr, trlr metadata.MD) (any, error) { + var view string + { + if vals := hdr.Get("goa-view"); len(vals) > 0 { + view = vals[0] + } + } + message, ok := v.(*apipb.CounterIncrementResponse) + if !ok { + return nil, goagrpc.ErrInvalidType("api", "CounterIncrement", "*apipb.CounterIncrementResponse", v) + } + res := NewCounterIncrementResult(message) + vres := &apiviews.CounterInfo{Projected: res, View: view} + if err := apiviews.ValidateCounterInfo(vres); err != nil { + return nil, err + } + return api.NewCounterInfo(vres), nil +} diff --git a/app/api/v1/gen/grpc/api/client/types.go b/app/api/v1/gen/grpc/api/client/types.go new file mode 100644 index 0000000..b407715 --- /dev/null +++ b/app/api/v1/gen/grpc/api/client/types.go @@ -0,0 +1,71 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// api gRPC client types +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package client + +import ( + api "github.com/jace-ys/countup/api/v1/gen/api" + apiviews "github.com/jace-ys/countup/api/v1/gen/api/views" + apipb "github.com/jace-ys/countup/api/v1/gen/grpc/api/pb" +) + +// NewProtoAuthTokenRequest builds the gRPC request type from the payload of +// the "AuthToken" endpoint of the "api" service. +func NewProtoAuthTokenRequest(payload *api.AuthTokenPayload) *apipb.AuthTokenRequest { + message := &apipb.AuthTokenRequest{ + Provider: payload.Provider, + AccessToken: payload.AccessToken, + } + return message +} + +// NewAuthTokenResult builds the result type of the "AuthToken" endpoint of the +// "api" service from the gRPC response type. +func NewAuthTokenResult(message *apipb.AuthTokenResponse) *api.AuthTokenResult { + result := &api.AuthTokenResult{ + Token: message.Token, + } + return result +} + +// NewProtoCounterGetRequest builds the gRPC request type from the payload of +// the "CounterGet" endpoint of the "api" service. +func NewProtoCounterGetRequest() *apipb.CounterGetRequest { + message := &apipb.CounterGetRequest{} + return message +} + +// NewCounterGetResult builds the result type of the "CounterGet" endpoint of +// the "api" service from the gRPC response type. +func NewCounterGetResult(message *apipb.CounterGetResponse) *apiviews.CounterInfoView { + result := &apiviews.CounterInfoView{ + Count: &message.Count, + LastIncrementBy: &message.LastIncrementBy, + LastIncrementAt: &message.LastIncrementAt, + NextFinalizeAt: &message.NextFinalizeAt, + } + return result +} + +// NewProtoCounterIncrementRequest builds the gRPC request type from the +// payload of the "CounterIncrement" endpoint of the "api" service. +func NewProtoCounterIncrementRequest() *apipb.CounterIncrementRequest { + message := &apipb.CounterIncrementRequest{} + return message +} + +// NewCounterIncrementResult builds the result type of the "CounterIncrement" +// endpoint of the "api" service from the gRPC response type. +func NewCounterIncrementResult(message *apipb.CounterIncrementResponse) *apiviews.CounterInfoView { + result := &apiviews.CounterInfoView{ + Count: &message.Count, + LastIncrementBy: &message.LastIncrementBy, + LastIncrementAt: &message.LastIncrementAt, + NextFinalizeAt: &message.NextFinalizeAt, + } + return result +} diff --git a/app/api/v1/gen/grpc/api/pb/goagen_v1_api.pb.go b/app/api/v1/gen/grpc/api/pb/goagen_v1_api.pb.go new file mode 100644 index 0000000..5c3c291 --- /dev/null +++ b/app/api/v1/gen/grpc/api/pb/goagen_v1_api.pb.go @@ -0,0 +1,451 @@ +// Code generated with goa v3.19.1, DO NOT EDIT. +// +// api protocol buffer definition +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.35.2 +// protoc v5.29.1 +// source: goagen_v1_api.proto + +package apipb + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +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) +) + +type AuthTokenRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Provider string `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` + AccessToken string `protobuf:"bytes,2,opt,name=access_token,json=accessToken,proto3" json:"access_token,omitempty"` +} + +func (x *AuthTokenRequest) Reset() { + *x = AuthTokenRequest{} + mi := &file_goagen_v1_api_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthTokenRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthTokenRequest) ProtoMessage() {} + +func (x *AuthTokenRequest) ProtoReflect() protoreflect.Message { + mi := &file_goagen_v1_api_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 AuthTokenRequest.ProtoReflect.Descriptor instead. +func (*AuthTokenRequest) Descriptor() ([]byte, []int) { + return file_goagen_v1_api_proto_rawDescGZIP(), []int{0} +} + +func (x *AuthTokenRequest) GetProvider() string { + if x != nil { + return x.Provider + } + return "" +} + +func (x *AuthTokenRequest) GetAccessToken() string { + if x != nil { + return x.AccessToken + } + return "" +} + +type AuthTokenResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` +} + +func (x *AuthTokenResponse) Reset() { + *x = AuthTokenResponse{} + mi := &file_goagen_v1_api_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthTokenResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthTokenResponse) ProtoMessage() {} + +func (x *AuthTokenResponse) ProtoReflect() protoreflect.Message { + mi := &file_goagen_v1_api_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 AuthTokenResponse.ProtoReflect.Descriptor instead. +func (*AuthTokenResponse) Descriptor() ([]byte, []int) { + return file_goagen_v1_api_proto_rawDescGZIP(), []int{1} +} + +func (x *AuthTokenResponse) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + +type CounterGetRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *CounterGetRequest) Reset() { + *x = CounterGetRequest{} + mi := &file_goagen_v1_api_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CounterGetRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CounterGetRequest) ProtoMessage() {} + +func (x *CounterGetRequest) ProtoReflect() protoreflect.Message { + mi := &file_goagen_v1_api_proto_msgTypes[2] + 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 CounterGetRequest.ProtoReflect.Descriptor instead. +func (*CounterGetRequest) Descriptor() ([]byte, []int) { + return file_goagen_v1_api_proto_rawDescGZIP(), []int{2} +} + +type CounterGetResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Count int32 `protobuf:"zigzag32,1,opt,name=count,proto3" json:"count,omitempty"` + LastIncrementBy string `protobuf:"bytes,2,opt,name=last_increment_by,json=lastIncrementBy,proto3" json:"last_increment_by,omitempty"` + LastIncrementAt string `protobuf:"bytes,3,opt,name=last_increment_at,json=lastIncrementAt,proto3" json:"last_increment_at,omitempty"` + NextFinalizeAt string `protobuf:"bytes,4,opt,name=next_finalize_at,json=nextFinalizeAt,proto3" json:"next_finalize_at,omitempty"` +} + +func (x *CounterGetResponse) Reset() { + *x = CounterGetResponse{} + mi := &file_goagen_v1_api_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CounterGetResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CounterGetResponse) ProtoMessage() {} + +func (x *CounterGetResponse) ProtoReflect() protoreflect.Message { + mi := &file_goagen_v1_api_proto_msgTypes[3] + 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 CounterGetResponse.ProtoReflect.Descriptor instead. +func (*CounterGetResponse) Descriptor() ([]byte, []int) { + return file_goagen_v1_api_proto_rawDescGZIP(), []int{3} +} + +func (x *CounterGetResponse) GetCount() int32 { + if x != nil { + return x.Count + } + return 0 +} + +func (x *CounterGetResponse) GetLastIncrementBy() string { + if x != nil { + return x.LastIncrementBy + } + return "" +} + +func (x *CounterGetResponse) GetLastIncrementAt() string { + if x != nil { + return x.LastIncrementAt + } + return "" +} + +func (x *CounterGetResponse) GetNextFinalizeAt() string { + if x != nil { + return x.NextFinalizeAt + } + return "" +} + +type CounterIncrementRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *CounterIncrementRequest) Reset() { + *x = CounterIncrementRequest{} + mi := &file_goagen_v1_api_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CounterIncrementRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CounterIncrementRequest) ProtoMessage() {} + +func (x *CounterIncrementRequest) ProtoReflect() protoreflect.Message { + mi := &file_goagen_v1_api_proto_msgTypes[4] + 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 CounterIncrementRequest.ProtoReflect.Descriptor instead. +func (*CounterIncrementRequest) Descriptor() ([]byte, []int) { + return file_goagen_v1_api_proto_rawDescGZIP(), []int{4} +} + +type CounterIncrementResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Count int32 `protobuf:"zigzag32,1,opt,name=count,proto3" json:"count,omitempty"` + LastIncrementBy string `protobuf:"bytes,2,opt,name=last_increment_by,json=lastIncrementBy,proto3" json:"last_increment_by,omitempty"` + LastIncrementAt string `protobuf:"bytes,3,opt,name=last_increment_at,json=lastIncrementAt,proto3" json:"last_increment_at,omitempty"` + NextFinalizeAt string `protobuf:"bytes,4,opt,name=next_finalize_at,json=nextFinalizeAt,proto3" json:"next_finalize_at,omitempty"` +} + +func (x *CounterIncrementResponse) Reset() { + *x = CounterIncrementResponse{} + mi := &file_goagen_v1_api_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CounterIncrementResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CounterIncrementResponse) ProtoMessage() {} + +func (x *CounterIncrementResponse) ProtoReflect() protoreflect.Message { + mi := &file_goagen_v1_api_proto_msgTypes[5] + 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 CounterIncrementResponse.ProtoReflect.Descriptor instead. +func (*CounterIncrementResponse) Descriptor() ([]byte, []int) { + return file_goagen_v1_api_proto_rawDescGZIP(), []int{5} +} + +func (x *CounterIncrementResponse) GetCount() int32 { + if x != nil { + return x.Count + } + return 0 +} + +func (x *CounterIncrementResponse) GetLastIncrementBy() string { + if x != nil { + return x.LastIncrementBy + } + return "" +} + +func (x *CounterIncrementResponse) GetLastIncrementAt() string { + if x != nil { + return x.LastIncrementAt + } + return "" +} + +func (x *CounterIncrementResponse) GetNextFinalizeAt() string { + if x != nil { + return x.NextFinalizeAt + } + return "" +} + +var File_goagen_v1_api_proto protoreflect.FileDescriptor + +var file_goagen_v1_api_proto_rawDesc = []byte{ + 0x0a, 0x13, 0x67, 0x6f, 0x61, 0x67, 0x65, 0x6e, 0x5f, 0x76, 0x31, 0x5f, 0x61, 0x70, 0x69, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x61, 0x70, 0x69, 0x22, 0x51, 0x0a, 0x10, 0x41, 0x75, + 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, + 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x63, + 0x63, 0x65, 0x73, 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0b, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x29, 0x0a, + 0x11, 0x41, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x13, 0x0a, 0x11, 0x43, 0x6f, 0x75, 0x6e, + 0x74, 0x65, 0x72, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xac, 0x01, + 0x0a, 0x12, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x11, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x2a, 0x0a, 0x11, 0x6c, 0x61, + 0x73, 0x74, 0x5f, 0x69, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x62, 0x79, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x6c, 0x61, 0x73, 0x74, 0x49, 0x6e, 0x63, 0x72, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x79, 0x12, 0x2a, 0x0a, 0x11, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x69, + 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0f, 0x6c, 0x61, 0x73, 0x74, 0x49, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x41, 0x74, 0x12, 0x28, 0x0a, 0x10, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x66, 0x69, 0x6e, 0x61, 0x6c, + 0x69, 0x7a, 0x65, 0x5f, 0x61, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6e, 0x65, + 0x78, 0x74, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x41, 0x74, 0x22, 0x19, 0x0a, 0x17, + 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xb2, 0x01, 0x0a, 0x18, 0x43, 0x6f, 0x75, 0x6e, + 0x74, 0x65, 0x72, 0x49, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x11, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x2a, 0x0a, 0x11, 0x6c, 0x61, + 0x73, 0x74, 0x5f, 0x69, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x62, 0x79, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x6c, 0x61, 0x73, 0x74, 0x49, 0x6e, 0x63, 0x72, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x79, 0x12, 0x2a, 0x0a, 0x11, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x69, + 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0f, 0x6c, 0x61, 0x73, 0x74, 0x49, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x41, 0x74, 0x12, 0x28, 0x0a, 0x10, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x66, 0x69, 0x6e, 0x61, 0x6c, + 0x69, 0x7a, 0x65, 0x5f, 0x61, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6e, 0x65, + 0x78, 0x74, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x41, 0x74, 0x32, 0xd1, 0x01, 0x0a, + 0x03, 0x41, 0x50, 0x49, 0x12, 0x3a, 0x0a, 0x09, 0x41, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, + 0x6e, 0x12, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, + 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x41, + 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x3d, 0x0a, 0x0a, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x47, 0x65, 0x74, 0x12, 0x16, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x47, 0x65, 0x74, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x6f, 0x75, + 0x6e, 0x74, 0x65, 0x72, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x4f, 0x0a, 0x10, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x63, 0x72, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x12, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x65, + 0x72, 0x49, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x49, + 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x61, 0x70, 0x69, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, +} + +var ( + file_goagen_v1_api_proto_rawDescOnce sync.Once + file_goagen_v1_api_proto_rawDescData = file_goagen_v1_api_proto_rawDesc +) + +func file_goagen_v1_api_proto_rawDescGZIP() []byte { + file_goagen_v1_api_proto_rawDescOnce.Do(func() { + file_goagen_v1_api_proto_rawDescData = protoimpl.X.CompressGZIP(file_goagen_v1_api_proto_rawDescData) + }) + return file_goagen_v1_api_proto_rawDescData +} + +var file_goagen_v1_api_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_goagen_v1_api_proto_goTypes = []any{ + (*AuthTokenRequest)(nil), // 0: api.AuthTokenRequest + (*AuthTokenResponse)(nil), // 1: api.AuthTokenResponse + (*CounterGetRequest)(nil), // 2: api.CounterGetRequest + (*CounterGetResponse)(nil), // 3: api.CounterGetResponse + (*CounterIncrementRequest)(nil), // 4: api.CounterIncrementRequest + (*CounterIncrementResponse)(nil), // 5: api.CounterIncrementResponse +} +var file_goagen_v1_api_proto_depIdxs = []int32{ + 0, // 0: api.API.AuthToken:input_type -> api.AuthTokenRequest + 2, // 1: api.API.CounterGet:input_type -> api.CounterGetRequest + 4, // 2: api.API.CounterIncrement:input_type -> api.CounterIncrementRequest + 1, // 3: api.API.AuthToken:output_type -> api.AuthTokenResponse + 3, // 4: api.API.CounterGet:output_type -> api.CounterGetResponse + 5, // 5: api.API.CounterIncrement:output_type -> api.CounterIncrementResponse + 3, // [3:6] is the sub-list for method output_type + 0, // [0:3] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_goagen_v1_api_proto_init() } +func file_goagen_v1_api_proto_init() { + if File_goagen_v1_api_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_goagen_v1_api_proto_rawDesc, + NumEnums: 0, + NumMessages: 6, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_goagen_v1_api_proto_goTypes, + DependencyIndexes: file_goagen_v1_api_proto_depIdxs, + MessageInfos: file_goagen_v1_api_proto_msgTypes, + }.Build() + File_goagen_v1_api_proto = out.File + file_goagen_v1_api_proto_rawDesc = nil + file_goagen_v1_api_proto_goTypes = nil + file_goagen_v1_api_proto_depIdxs = nil +} diff --git a/app/api/v1/gen/grpc/api/pb/goagen_v1_api.proto b/app/api/v1/gen/grpc/api/pb/goagen_v1_api.proto new file mode 100644 index 0000000..66d283e --- /dev/null +++ b/app/api/v1/gen/grpc/api/pb/goagen_v1_api.proto @@ -0,0 +1,51 @@ +// Code generated with goa v3.19.1, DO NOT EDIT. +// +// api protocol buffer definition +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +syntax = "proto3"; + +package api; + +option go_package = "/apipb"; + +// Service is the api service interface. +service API { + // AuthToken implements AuthToken. + rpc AuthToken (AuthTokenRequest) returns (AuthTokenResponse); + // CounterGet implements CounterGet. + rpc CounterGet (CounterGetRequest) returns (CounterGetResponse); + // CounterIncrement implements CounterIncrement. + rpc CounterIncrement (CounterIncrementRequest) returns (CounterIncrementResponse); +} + +message AuthTokenRequest { + string provider = 1; + string access_token = 2; +} + +message AuthTokenResponse { + string token = 1; +} + +message CounterGetRequest { +} + +message CounterGetResponse { + sint32 count = 1; + string last_increment_by = 2; + string last_increment_at = 3; + string next_finalize_at = 4; +} + +message CounterIncrementRequest { +} + +message CounterIncrementResponse { + sint32 count = 1; + string last_increment_by = 2; + string last_increment_at = 3; + string next_finalize_at = 4; +} diff --git a/app/api/v1/gen/grpc/api/pb/goagen_v1_api_grpc.pb.go b/app/api/v1/gen/grpc/api/pb/goagen_v1_api_grpc.pb.go new file mode 100644 index 0000000..ae5d734 --- /dev/null +++ b/app/api/v1/gen/grpc/api/pb/goagen_v1_api_grpc.pb.go @@ -0,0 +1,214 @@ +// Code generated with goa v3.19.1, DO NOT EDIT. +// +// api protocol buffer definition +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v5.29.1 +// source: goagen_v1_api.proto + +package apipb + +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 ( + API_AuthToken_FullMethodName = "/api.API/AuthToken" + API_CounterGet_FullMethodName = "/api.API/CounterGet" + API_CounterIncrement_FullMethodName = "/api.API/CounterIncrement" +) + +// APIClient is the client API for API 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. +// +// Service is the api service interface. +type APIClient interface { + // AuthToken implements AuthToken. + AuthToken(ctx context.Context, in *AuthTokenRequest, opts ...grpc.CallOption) (*AuthTokenResponse, error) + // CounterGet implements CounterGet. + CounterGet(ctx context.Context, in *CounterGetRequest, opts ...grpc.CallOption) (*CounterGetResponse, error) + // CounterIncrement implements CounterIncrement. + CounterIncrement(ctx context.Context, in *CounterIncrementRequest, opts ...grpc.CallOption) (*CounterIncrementResponse, error) +} + +type aPIClient struct { + cc grpc.ClientConnInterface +} + +func NewAPIClient(cc grpc.ClientConnInterface) APIClient { + return &aPIClient{cc} +} + +func (c *aPIClient) AuthToken(ctx context.Context, in *AuthTokenRequest, opts ...grpc.CallOption) (*AuthTokenResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(AuthTokenResponse) + err := c.cc.Invoke(ctx, API_AuthToken_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *aPIClient) CounterGet(ctx context.Context, in *CounterGetRequest, opts ...grpc.CallOption) (*CounterGetResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CounterGetResponse) + err := c.cc.Invoke(ctx, API_CounterGet_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *aPIClient) CounterIncrement(ctx context.Context, in *CounterIncrementRequest, opts ...grpc.CallOption) (*CounterIncrementResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CounterIncrementResponse) + err := c.cc.Invoke(ctx, API_CounterIncrement_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// APIServer is the server API for API service. +// All implementations must embed UnimplementedAPIServer +// for forward compatibility. +// +// Service is the api service interface. +type APIServer interface { + // AuthToken implements AuthToken. + AuthToken(context.Context, *AuthTokenRequest) (*AuthTokenResponse, error) + // CounterGet implements CounterGet. + CounterGet(context.Context, *CounterGetRequest) (*CounterGetResponse, error) + // CounterIncrement implements CounterIncrement. + CounterIncrement(context.Context, *CounterIncrementRequest) (*CounterIncrementResponse, error) + mustEmbedUnimplementedAPIServer() +} + +// UnimplementedAPIServer must 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 UnimplementedAPIServer struct{} + +func (UnimplementedAPIServer) AuthToken(context.Context, *AuthTokenRequest) (*AuthTokenResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method AuthToken not implemented") +} +func (UnimplementedAPIServer) CounterGet(context.Context, *CounterGetRequest) (*CounterGetResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CounterGet not implemented") +} +func (UnimplementedAPIServer) CounterIncrement(context.Context, *CounterIncrementRequest) (*CounterIncrementResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CounterIncrement not implemented") +} +func (UnimplementedAPIServer) mustEmbedUnimplementedAPIServer() {} +func (UnimplementedAPIServer) testEmbeddedByValue() {} + +// UnsafeAPIServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to APIServer will +// result in compilation errors. +type UnsafeAPIServer interface { + mustEmbedUnimplementedAPIServer() +} + +func RegisterAPIServer(s grpc.ServiceRegistrar, srv APIServer) { + // If the following call pancis, it indicates UnimplementedAPIServer 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(&API_ServiceDesc, srv) +} + +func _API_AuthToken_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AuthTokenRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).AuthToken(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: API_AuthToken_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).AuthToken(ctx, req.(*AuthTokenRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _API_CounterGet_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CounterGetRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).CounterGet(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: API_CounterGet_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).CounterGet(ctx, req.(*CounterGetRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _API_CounterIncrement_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CounterIncrementRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).CounterIncrement(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: API_CounterIncrement_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).CounterIncrement(ctx, req.(*CounterIncrementRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// API_ServiceDesc is the grpc.ServiceDesc for API service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var API_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "api.API", + HandlerType: (*APIServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "AuthToken", + Handler: _API_AuthToken_Handler, + }, + { + MethodName: "CounterGet", + Handler: _API_CounterGet_Handler, + }, + { + MethodName: "CounterIncrement", + Handler: _API_CounterIncrement_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "goagen_v1_api.proto", +} diff --git a/app/api/v1/gen/grpc/api/server/encode_decode.go b/app/api/v1/gen/grpc/api/server/encode_decode.go new file mode 100644 index 0000000..ad87d9a --- /dev/null +++ b/app/api/v1/gen/grpc/api/server/encode_decode.go @@ -0,0 +1,107 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// api gRPC server encoders and decoders +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package server + +import ( + "context" + "strings" + + api "github.com/jace-ys/countup/api/v1/gen/api" + apiviews "github.com/jace-ys/countup/api/v1/gen/api/views" + apipb "github.com/jace-ys/countup/api/v1/gen/grpc/api/pb" + goagrpc "goa.design/goa/v3/grpc" + "google.golang.org/grpc/metadata" +) + +// EncodeAuthTokenResponse encodes responses from the "api" service "AuthToken" +// endpoint. +func EncodeAuthTokenResponse(ctx context.Context, v any, hdr, trlr *metadata.MD) (any, error) { + result, ok := v.(*api.AuthTokenResult) + if !ok { + return nil, goagrpc.ErrInvalidType("api", "AuthToken", "*api.AuthTokenResult", v) + } + resp := NewProtoAuthTokenResponse(result) + return resp, nil +} + +// DecodeAuthTokenRequest decodes requests sent to "api" service "AuthToken" +// endpoint. +func DecodeAuthTokenRequest(ctx context.Context, v any, md metadata.MD) (any, error) { + var ( + message *apipb.AuthTokenRequest + ok bool + ) + { + if message, ok = v.(*apipb.AuthTokenRequest); !ok { + return nil, goagrpc.ErrInvalidType("api", "AuthToken", "*apipb.AuthTokenRequest", v) + } + if err := ValidateAuthTokenRequest(message); err != nil { + return nil, err + } + } + var payload *api.AuthTokenPayload + { + payload = NewAuthTokenPayload(message) + } + return payload, nil +} + +// EncodeCounterGetResponse encodes responses from the "api" service +// "CounterGet" endpoint. +func EncodeCounterGetResponse(ctx context.Context, v any, hdr, trlr *metadata.MD) (any, error) { + vres, ok := v.(*apiviews.CounterInfo) + if !ok { + return nil, goagrpc.ErrInvalidType("api", "CounterGet", "*apiviews.CounterInfo", v) + } + result := vres.Projected + (*hdr).Append("goa-view", vres.View) + resp := NewProtoCounterGetResponse(result) + return resp, nil +} + +// EncodeCounterIncrementResponse encodes responses from the "api" service +// "CounterIncrement" endpoint. +func EncodeCounterIncrementResponse(ctx context.Context, v any, hdr, trlr *metadata.MD) (any, error) { + vres, ok := v.(*apiviews.CounterInfo) + if !ok { + return nil, goagrpc.ErrInvalidType("api", "CounterIncrement", "*apiviews.CounterInfo", v) + } + result := vres.Projected + (*hdr).Append("goa-view", vres.View) + resp := NewProtoCounterIncrementResponse(result) + return resp, nil +} + +// DecodeCounterIncrementRequest decodes requests sent to "api" service +// "CounterIncrement" endpoint. +func DecodeCounterIncrementRequest(ctx context.Context, v any, md metadata.MD) (any, error) { + var ( + token *string + err error + ) + { + if vals := md.Get("authorization"); len(vals) > 0 { + token = &vals[0] + } + } + if err != nil { + return nil, err + } + var payload *api.CounterIncrementPayload + { + payload = NewCounterIncrementPayload(token) + if payload.Token != nil { + if strings.Contains(*payload.Token, " ") { + // Remove authorization scheme prefix (e.g. "Bearer") + cred := strings.SplitN(*payload.Token, " ", 2)[1] + payload.Token = &cred + } + } + } + return payload, nil +} diff --git a/app/api/v1/gen/grpc/api/server/server.go b/app/api/v1/gen/grpc/api/server/server.go new file mode 100644 index 0000000..705d5bf --- /dev/null +++ b/app/api/v1/gen/grpc/api/server/server.go @@ -0,0 +1,128 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// api gRPC server +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package server + +import ( + "context" + "errors" + + api "github.com/jace-ys/countup/api/v1/gen/api" + apipb "github.com/jace-ys/countup/api/v1/gen/grpc/api/pb" + goagrpc "goa.design/goa/v3/grpc" + goa "goa.design/goa/v3/pkg" + "google.golang.org/grpc/codes" +) + +// Server implements the apipb.APIServer interface. +type Server struct { + AuthTokenH goagrpc.UnaryHandler + CounterGetH goagrpc.UnaryHandler + CounterIncrementH goagrpc.UnaryHandler + apipb.UnimplementedAPIServer +} + +// New instantiates the server struct with the api service endpoints. +func New(e *api.Endpoints, uh goagrpc.UnaryHandler) *Server { + return &Server{ + AuthTokenH: NewAuthTokenHandler(e.AuthToken, uh), + CounterGetH: NewCounterGetHandler(e.CounterGet, uh), + CounterIncrementH: NewCounterIncrementHandler(e.CounterIncrement, uh), + } +} + +// NewAuthTokenHandler creates a gRPC handler which serves the "api" service +// "AuthToken" endpoint. +func NewAuthTokenHandler(endpoint goa.Endpoint, h goagrpc.UnaryHandler) goagrpc.UnaryHandler { + if h == nil { + h = goagrpc.NewUnaryHandler(endpoint, DecodeAuthTokenRequest, EncodeAuthTokenResponse) + } + return h +} + +// AuthToken implements the "AuthToken" method in apipb.APIServer interface. +func (s *Server) AuthToken(ctx context.Context, message *apipb.AuthTokenRequest) (*apipb.AuthTokenResponse, error) { + ctx = context.WithValue(ctx, goa.MethodKey, "AuthToken") + ctx = context.WithValue(ctx, goa.ServiceKey, "api") + resp, err := s.AuthTokenH.Handle(ctx, message) + if err != nil { + var en goa.GoaErrorNamer + if errors.As(err, &en) { + switch en.GoaErrorName() { + case "incomplete_auth_info": + return nil, goagrpc.NewStatusError(codes.PermissionDenied, err, goagrpc.NewErrorResponse(err)) + case "unauthorized": + return nil, goagrpc.NewStatusError(codes.Unauthenticated, err, goagrpc.NewErrorResponse(err)) + case "forbidden": + return nil, goagrpc.NewStatusError(codes.PermissionDenied, err, goagrpc.NewErrorResponse(err)) + } + } + return nil, goagrpc.EncodeError(err) + } + return resp.(*apipb.AuthTokenResponse), nil +} + +// NewCounterGetHandler creates a gRPC handler which serves the "api" service +// "CounterGet" endpoint. +func NewCounterGetHandler(endpoint goa.Endpoint, h goagrpc.UnaryHandler) goagrpc.UnaryHandler { + if h == nil { + h = goagrpc.NewUnaryHandler(endpoint, nil, EncodeCounterGetResponse) + } + return h +} + +// CounterGet implements the "CounterGet" method in apipb.APIServer interface. +func (s *Server) CounterGet(ctx context.Context, message *apipb.CounterGetRequest) (*apipb.CounterGetResponse, error) { + ctx = context.WithValue(ctx, goa.MethodKey, "CounterGet") + ctx = context.WithValue(ctx, goa.ServiceKey, "api") + resp, err := s.CounterGetH.Handle(ctx, message) + if err != nil { + var en goa.GoaErrorNamer + if errors.As(err, &en) { + switch en.GoaErrorName() { + case "unauthorized": + return nil, goagrpc.NewStatusError(codes.Unauthenticated, err, goagrpc.NewErrorResponse(err)) + case "forbidden": + return nil, goagrpc.NewStatusError(codes.PermissionDenied, err, goagrpc.NewErrorResponse(err)) + } + } + return nil, goagrpc.EncodeError(err) + } + return resp.(*apipb.CounterGetResponse), nil +} + +// NewCounterIncrementHandler creates a gRPC handler which serves the "api" +// service "CounterIncrement" endpoint. +func NewCounterIncrementHandler(endpoint goa.Endpoint, h goagrpc.UnaryHandler) goagrpc.UnaryHandler { + if h == nil { + h = goagrpc.NewUnaryHandler(endpoint, DecodeCounterIncrementRequest, EncodeCounterIncrementResponse) + } + return h +} + +// CounterIncrement implements the "CounterIncrement" method in apipb.APIServer +// interface. +func (s *Server) CounterIncrement(ctx context.Context, message *apipb.CounterIncrementRequest) (*apipb.CounterIncrementResponse, error) { + ctx = context.WithValue(ctx, goa.MethodKey, "CounterIncrement") + ctx = context.WithValue(ctx, goa.ServiceKey, "api") + resp, err := s.CounterIncrementH.Handle(ctx, message) + if err != nil { + var en goa.GoaErrorNamer + if errors.As(err, &en) { + switch en.GoaErrorName() { + case "existing_increment_request": + return nil, goagrpc.NewStatusError(codes.AlreadyExists, err, goagrpc.NewErrorResponse(err)) + case "unauthorized": + return nil, goagrpc.NewStatusError(codes.Unauthenticated, err, goagrpc.NewErrorResponse(err)) + case "forbidden": + return nil, goagrpc.NewStatusError(codes.PermissionDenied, err, goagrpc.NewErrorResponse(err)) + } + } + return nil, goagrpc.EncodeError(err) + } + return resp.(*apipb.CounterIncrementResponse), nil +} diff --git a/app/api/v1/gen/grpc/api/server/types.go b/app/api/v1/gen/grpc/api/server/types.go new file mode 100644 index 0000000..7b3a921 --- /dev/null +++ b/app/api/v1/gen/grpc/api/server/types.go @@ -0,0 +1,74 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// api gRPC server types +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package server + +import ( + api "github.com/jace-ys/countup/api/v1/gen/api" + apiviews "github.com/jace-ys/countup/api/v1/gen/api/views" + apipb "github.com/jace-ys/countup/api/v1/gen/grpc/api/pb" + goa "goa.design/goa/v3/pkg" +) + +// NewAuthTokenPayload builds the payload of the "AuthToken" endpoint of the +// "api" service from the gRPC request type. +func NewAuthTokenPayload(message *apipb.AuthTokenRequest) *api.AuthTokenPayload { + v := &api.AuthTokenPayload{ + Provider: message.Provider, + AccessToken: message.AccessToken, + } + return v +} + +// NewProtoAuthTokenResponse builds the gRPC response type from the result of +// the "AuthToken" endpoint of the "api" service. +func NewProtoAuthTokenResponse(result *api.AuthTokenResult) *apipb.AuthTokenResponse { + message := &apipb.AuthTokenResponse{ + Token: result.Token, + } + return message +} + +// NewProtoCounterGetResponse builds the gRPC response type from the result of +// the "CounterGet" endpoint of the "api" service. +func NewProtoCounterGetResponse(result *apiviews.CounterInfoView) *apipb.CounterGetResponse { + message := &apipb.CounterGetResponse{ + Count: *result.Count, + LastIncrementBy: *result.LastIncrementBy, + LastIncrementAt: *result.LastIncrementAt, + NextFinalizeAt: *result.NextFinalizeAt, + } + return message +} + +// NewCounterIncrementPayload builds the payload of the "CounterIncrement" +// endpoint of the "api" service from the gRPC request type. +func NewCounterIncrementPayload(token *string) *api.CounterIncrementPayload { + v := &api.CounterIncrementPayload{} + v.Token = token + return v +} + +// NewProtoCounterIncrementResponse builds the gRPC response type from the +// result of the "CounterIncrement" endpoint of the "api" service. +func NewProtoCounterIncrementResponse(result *apiviews.CounterInfoView) *apipb.CounterIncrementResponse { + message := &apipb.CounterIncrementResponse{ + Count: *result.Count, + LastIncrementBy: *result.LastIncrementBy, + LastIncrementAt: *result.LastIncrementAt, + NextFinalizeAt: *result.NextFinalizeAt, + } + return message +} + +// ValidateAuthTokenRequest runs the validations defined on AuthTokenRequest. +func ValidateAuthTokenRequest(message *apipb.AuthTokenRequest) (err error) { + if !(message.Provider == "google") { + err = goa.MergeErrors(err, goa.InvalidEnumValueError("message.provider", message.Provider, []any{"google"})) + } + return +} diff --git a/app/api/v1/gen/grpc/cli/countup/cli.go b/app/api/v1/gen/grpc/cli/countup/cli.go new file mode 100644 index 0000000..e3ff6d9 --- /dev/null +++ b/app/api/v1/gen/grpc/cli/countup/cli.go @@ -0,0 +1,243 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// countup gRPC client CLI support package +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package cli + +import ( + "flag" + "fmt" + "os" + + apic "github.com/jace-ys/countup/api/v1/gen/grpc/api/client" + teapotc "github.com/jace-ys/countup/api/v1/gen/grpc/teapot/client" + goa "goa.design/goa/v3/pkg" + grpc "google.golang.org/grpc" +) + +// UsageCommands returns the set of commands and sub-commands using the format +// +// command (subcommand1|subcommand2|...) +func UsageCommands() string { + return `api (auth-token|counter-get|counter-increment) +teapot echo +` +} + +// UsageExamples produces an example of a valid invocation of the CLI tool. +func UsageExamples() string { + return os.Args[0] + ` api auth-token --message '{ + "access_token": "Est sunt voluptatem reprehenderit neque.", + "provider": "google" + }'` + "\n" + + os.Args[0] + ` teapot echo --message '{ + "text": "Assumenda mollitia deleniti expedita." + }'` + "\n" + + "" +} + +// ParseEndpoint returns the endpoint and payload as specified on the command +// line. +func ParseEndpoint(cc *grpc.ClientConn, opts ...grpc.CallOption) (goa.Endpoint, any, error) { + var ( + apiFlags = flag.NewFlagSet("api", flag.ContinueOnError) + + apiAuthTokenFlags = flag.NewFlagSet("auth-token", flag.ExitOnError) + apiAuthTokenMessageFlag = apiAuthTokenFlags.String("message", "", "") + + apiCounterGetFlags = flag.NewFlagSet("counter-get", flag.ExitOnError) + + apiCounterIncrementFlags = flag.NewFlagSet("counter-increment", flag.ExitOnError) + apiCounterIncrementTokenFlag = apiCounterIncrementFlags.String("token", "", "") + + teapotFlags = flag.NewFlagSet("teapot", flag.ContinueOnError) + + teapotEchoFlags = flag.NewFlagSet("echo", flag.ExitOnError) + teapotEchoMessageFlag = teapotEchoFlags.String("message", "", "") + ) + apiFlags.Usage = apiUsage + apiAuthTokenFlags.Usage = apiAuthTokenUsage + apiCounterGetFlags.Usage = apiCounterGetUsage + apiCounterIncrementFlags.Usage = apiCounterIncrementUsage + + teapotFlags.Usage = teapotUsage + teapotEchoFlags.Usage = teapotEchoUsage + + if err := flag.CommandLine.Parse(os.Args[1:]); err != nil { + return nil, nil, err + } + + if flag.NArg() < 2 { // two non flag args are required: SERVICE and ENDPOINT (aka COMMAND) + return nil, nil, fmt.Errorf("not enough arguments") + } + + var ( + svcn string + svcf *flag.FlagSet + ) + { + svcn = flag.Arg(0) + switch svcn { + case "api": + svcf = apiFlags + case "teapot": + svcf = teapotFlags + default: + return nil, nil, fmt.Errorf("unknown service %q", svcn) + } + } + if err := svcf.Parse(flag.Args()[1:]); err != nil { + return nil, nil, err + } + + var ( + epn string + epf *flag.FlagSet + ) + { + epn = svcf.Arg(0) + switch svcn { + case "api": + switch epn { + case "auth-token": + epf = apiAuthTokenFlags + + case "counter-get": + epf = apiCounterGetFlags + + case "counter-increment": + epf = apiCounterIncrementFlags + + } + + case "teapot": + switch epn { + case "echo": + epf = teapotEchoFlags + + } + + } + } + if epf == nil { + return nil, nil, fmt.Errorf("unknown %q endpoint %q", svcn, epn) + } + + // Parse endpoint flags if any + if svcf.NArg() > 1 { + if err := epf.Parse(svcf.Args()[1:]); err != nil { + return nil, nil, err + } + } + + var ( + data any + endpoint goa.Endpoint + err error + ) + { + switch svcn { + case "api": + c := apic.NewClient(cc, opts...) + switch epn { + case "auth-token": + endpoint = c.AuthToken() + data, err = apic.BuildAuthTokenPayload(*apiAuthTokenMessageFlag) + case "counter-get": + endpoint = c.CounterGet() + case "counter-increment": + endpoint = c.CounterIncrement() + data, err = apic.BuildCounterIncrementPayload(*apiCounterIncrementTokenFlag) + } + case "teapot": + c := teapotc.NewClient(cc, opts...) + switch epn { + case "echo": + endpoint = c.Echo() + data, err = teapotc.BuildEchoPayload(*teapotEchoMessageFlag) + } + } + } + if err != nil { + return nil, nil, err + } + + return endpoint, data, nil +} // apiUsage displays the usage of the api command and its subcommands. +func apiUsage() { + fmt.Fprintf(os.Stderr, `Service is the api service interface. +Usage: + %[1]s [globalflags] api COMMAND [flags] + +COMMAND: + auth-token: AuthToken implements AuthToken. + counter-get: CounterGet implements CounterGet. + counter-increment: CounterIncrement implements CounterIncrement. + +Additional help: + %[1]s api COMMAND --help +`, os.Args[0]) +} +func apiAuthTokenUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] api auth-token -message JSON + +AuthToken implements AuthToken. + -message JSON: + +Example: + %[1]s api auth-token --message '{ + "access_token": "Est sunt voluptatem reprehenderit neque.", + "provider": "google" + }' +`, os.Args[0]) +} + +func apiCounterGetUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] api counter-get + +CounterGet implements CounterGet. + +Example: + %[1]s api counter-get +`, os.Args[0]) +} + +func apiCounterIncrementUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] api counter-increment -token STRING + +CounterIncrement implements CounterIncrement. + -token STRING: + +Example: + %[1]s api counter-increment --token "Minima dolorem id." +`, os.Args[0]) +} + +// teapotUsage displays the usage of the teapot command and its subcommands. +func teapotUsage() { + fmt.Fprintf(os.Stderr, `Service is the teapot service interface. +Usage: + %[1]s [globalflags] teapot COMMAND [flags] + +COMMAND: + echo: Echo implements Echo. + +Additional help: + %[1]s teapot COMMAND --help +`, os.Args[0]) +} +func teapotEchoUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] teapot echo -message JSON + +Echo implements Echo. + -message JSON: + +Example: + %[1]s teapot echo --message '{ + "text": "Assumenda mollitia deleniti expedita." + }' +`, os.Args[0]) +} diff --git a/app/api/v1/gen/grpc/teapot/client/cli.go b/app/api/v1/gen/grpc/teapot/client/cli.go new file mode 100644 index 0000000..e2f599d --- /dev/null +++ b/app/api/v1/gen/grpc/teapot/client/cli.go @@ -0,0 +1,36 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// teapot gRPC client CLI support package +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package client + +import ( + "encoding/json" + "fmt" + + teapotpb "github.com/jace-ys/countup/api/v1/gen/grpc/teapot/pb" + teapot "github.com/jace-ys/countup/api/v1/gen/teapot" +) + +// BuildEchoPayload builds the payload for the teapot Echo endpoint from CLI +// flags. +func BuildEchoPayload(teapotEchoMessage string) (*teapot.EchoPayload, error) { + var err error + var message teapotpb.EchoRequest + { + if teapotEchoMessage != "" { + err = json.Unmarshal([]byte(teapotEchoMessage), &message) + if err != nil { + return nil, fmt.Errorf("invalid JSON for message, \nerror: %s, \nexample of valid JSON:\n%s", err, "'{\n \"text\": \"Assumenda mollitia deleniti expedita.\"\n }'") + } + } + } + v := &teapot.EchoPayload{ + Text: message.Text, + } + + return v, nil +} diff --git a/app/api/v1/gen/grpc/teapot/client/client.go b/app/api/v1/gen/grpc/teapot/client/client.go new file mode 100644 index 0000000..76fc289 --- /dev/null +++ b/app/api/v1/gen/grpc/teapot/client/client.go @@ -0,0 +1,42 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// teapot gRPC client +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package client + +import ( + "context" + + teapotpb "github.com/jace-ys/countup/api/v1/gen/grpc/teapot/pb" + goagrpc "goa.design/goa/v3/grpc" + goa "goa.design/goa/v3/pkg" + "google.golang.org/grpc" +) + +// Client lists the service endpoint gRPC clients. +type Client struct { + grpccli teapotpb.TeapotClient + opts []grpc.CallOption +} // NewClient instantiates gRPC client for all the teapot service servers. +func NewClient(cc *grpc.ClientConn, opts ...grpc.CallOption) *Client { + return &Client{ + grpccli: teapotpb.NewTeapotClient(cc), + opts: opts, + } +} // Echo calls the "Echo" function in teapotpb.TeapotClient interface. +func (c *Client) Echo() goa.Endpoint { + return func(ctx context.Context, v any) (any, error) { + inv := goagrpc.NewInvoker( + BuildEchoFunc(c.grpccli, c.opts...), + EncodeEchoRequest, + DecodeEchoResponse) + res, err := inv.Invoke(ctx, v) + if err != nil { + return nil, goa.Fault(err.Error()) + } + return res, nil + } +} diff --git a/app/api/v1/gen/grpc/teapot/client/encode_decode.go b/app/api/v1/gen/grpc/teapot/client/encode_decode.go new file mode 100644 index 0000000..a56daec --- /dev/null +++ b/app/api/v1/gen/grpc/teapot/client/encode_decode.go @@ -0,0 +1,51 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// teapot gRPC client encoders and decoders +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package client + +import ( + "context" + + teapotpb "github.com/jace-ys/countup/api/v1/gen/grpc/teapot/pb" + teapot "github.com/jace-ys/countup/api/v1/gen/teapot" + goagrpc "goa.design/goa/v3/grpc" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" +) + +// BuildEchoFunc builds the remote method to invoke for "teapot" service "Echo" +// endpoint. +func BuildEchoFunc(grpccli teapotpb.TeapotClient, cliopts ...grpc.CallOption) goagrpc.RemoteFunc { + return func(ctx context.Context, reqpb any, opts ...grpc.CallOption) (any, error) { + for _, opt := range cliopts { + opts = append(opts, opt) + } + if reqpb != nil { + return grpccli.Echo(ctx, reqpb.(*teapotpb.EchoRequest), opts...) + } + return grpccli.Echo(ctx, &teapotpb.EchoRequest{}, opts...) + } +} + +// EncodeEchoRequest encodes requests sent to teapot Echo endpoint. +func EncodeEchoRequest(ctx context.Context, v any, md *metadata.MD) (any, error) { + payload, ok := v.(*teapot.EchoPayload) + if !ok { + return nil, goagrpc.ErrInvalidType("teapot", "Echo", "*teapot.EchoPayload", v) + } + return NewProtoEchoRequest(payload), nil +} + +// DecodeEchoResponse decodes responses from the teapot Echo endpoint. +func DecodeEchoResponse(ctx context.Context, v any, hdr, trlr metadata.MD) (any, error) { + message, ok := v.(*teapotpb.EchoResponse) + if !ok { + return nil, goagrpc.ErrInvalidType("teapot", "Echo", "*teapotpb.EchoResponse", v) + } + res := NewEchoResult(message) + return res, nil +} diff --git a/app/api/v1/gen/grpc/teapot/client/types.go b/app/api/v1/gen/grpc/teapot/client/types.go new file mode 100644 index 0000000..369d39e --- /dev/null +++ b/app/api/v1/gen/grpc/teapot/client/types.go @@ -0,0 +1,31 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// teapot gRPC client types +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package client + +import ( + teapotpb "github.com/jace-ys/countup/api/v1/gen/grpc/teapot/pb" + teapot "github.com/jace-ys/countup/api/v1/gen/teapot" +) + +// NewProtoEchoRequest builds the gRPC request type from the payload of the +// "Echo" endpoint of the "teapot" service. +func NewProtoEchoRequest(payload *teapot.EchoPayload) *teapotpb.EchoRequest { + message := &teapotpb.EchoRequest{ + Text: payload.Text, + } + return message +} + +// NewEchoResult builds the result type of the "Echo" endpoint of the "teapot" +// service from the gRPC response type. +func NewEchoResult(message *teapotpb.EchoResponse) *teapot.EchoResult { + result := &teapot.EchoResult{ + Text: message.Text, + } + return result +} diff --git a/app/api/v1/gen/grpc/teapot/pb/goagen_v1_teapot.pb.go b/app/api/v1/gen/grpc/teapot/pb/goagen_v1_teapot.pb.go new file mode 100644 index 0000000..7a22f81 --- /dev/null +++ b/app/api/v1/gen/grpc/teapot/pb/goagen_v1_teapot.pb.go @@ -0,0 +1,187 @@ +// Code generated with goa v3.19.1, DO NOT EDIT. +// +// teapot protocol buffer definition +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.35.2 +// protoc v5.29.1 +// source: goagen_v1_teapot.proto + +package teapotpb + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +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) +) + +type EchoRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Text string `protobuf:"bytes,1,opt,name=text,proto3" json:"text,omitempty"` +} + +func (x *EchoRequest) Reset() { + *x = EchoRequest{} + mi := &file_goagen_v1_teapot_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EchoRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EchoRequest) ProtoMessage() {} + +func (x *EchoRequest) ProtoReflect() protoreflect.Message { + mi := &file_goagen_v1_teapot_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 EchoRequest.ProtoReflect.Descriptor instead. +func (*EchoRequest) Descriptor() ([]byte, []int) { + return file_goagen_v1_teapot_proto_rawDescGZIP(), []int{0} +} + +func (x *EchoRequest) GetText() string { + if x != nil { + return x.Text + } + return "" +} + +type EchoResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Text string `protobuf:"bytes,1,opt,name=text,proto3" json:"text,omitempty"` +} + +func (x *EchoResponse) Reset() { + *x = EchoResponse{} + mi := &file_goagen_v1_teapot_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EchoResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EchoResponse) ProtoMessage() {} + +func (x *EchoResponse) ProtoReflect() protoreflect.Message { + mi := &file_goagen_v1_teapot_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 EchoResponse.ProtoReflect.Descriptor instead. +func (*EchoResponse) Descriptor() ([]byte, []int) { + return file_goagen_v1_teapot_proto_rawDescGZIP(), []int{1} +} + +func (x *EchoResponse) GetText() string { + if x != nil { + return x.Text + } + return "" +} + +var File_goagen_v1_teapot_proto protoreflect.FileDescriptor + +var file_goagen_v1_teapot_proto_rawDesc = []byte{ + 0x0a, 0x16, 0x67, 0x6f, 0x61, 0x67, 0x65, 0x6e, 0x5f, 0x76, 0x31, 0x5f, 0x74, 0x65, 0x61, 0x70, + 0x6f, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06, 0x74, 0x65, 0x61, 0x70, 0x6f, 0x74, + 0x22, 0x21, 0x0a, 0x0b, 0x45, 0x63, 0x68, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x12, 0x0a, 0x04, 0x74, 0x65, 0x78, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, + 0x65, 0x78, 0x74, 0x22, 0x22, 0x0a, 0x0c, 0x45, 0x63, 0x68, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x78, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x74, 0x65, 0x78, 0x74, 0x32, 0x3b, 0x0a, 0x06, 0x54, 0x65, 0x61, 0x70, 0x6f, + 0x74, 0x12, 0x31, 0x0a, 0x04, 0x45, 0x63, 0x68, 0x6f, 0x12, 0x13, 0x2e, 0x74, 0x65, 0x61, 0x70, + 0x6f, 0x74, 0x2e, 0x45, 0x63, 0x68, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, + 0x2e, 0x74, 0x65, 0x61, 0x70, 0x6f, 0x74, 0x2e, 0x45, 0x63, 0x68, 0x6f, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x0b, 0x5a, 0x09, 0x2f, 0x74, 0x65, 0x61, 0x70, 0x6f, 0x74, 0x70, + 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_goagen_v1_teapot_proto_rawDescOnce sync.Once + file_goagen_v1_teapot_proto_rawDescData = file_goagen_v1_teapot_proto_rawDesc +) + +func file_goagen_v1_teapot_proto_rawDescGZIP() []byte { + file_goagen_v1_teapot_proto_rawDescOnce.Do(func() { + file_goagen_v1_teapot_proto_rawDescData = protoimpl.X.CompressGZIP(file_goagen_v1_teapot_proto_rawDescData) + }) + return file_goagen_v1_teapot_proto_rawDescData +} + +var file_goagen_v1_teapot_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_goagen_v1_teapot_proto_goTypes = []any{ + (*EchoRequest)(nil), // 0: teapot.EchoRequest + (*EchoResponse)(nil), // 1: teapot.EchoResponse +} +var file_goagen_v1_teapot_proto_depIdxs = []int32{ + 0, // 0: teapot.Teapot.Echo:input_type -> teapot.EchoRequest + 1, // 1: teapot.Teapot.Echo:output_type -> teapot.EchoResponse + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_goagen_v1_teapot_proto_init() } +func file_goagen_v1_teapot_proto_init() { + if File_goagen_v1_teapot_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_goagen_v1_teapot_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_goagen_v1_teapot_proto_goTypes, + DependencyIndexes: file_goagen_v1_teapot_proto_depIdxs, + MessageInfos: file_goagen_v1_teapot_proto_msgTypes, + }.Build() + File_goagen_v1_teapot_proto = out.File + file_goagen_v1_teapot_proto_rawDesc = nil + file_goagen_v1_teapot_proto_goTypes = nil + file_goagen_v1_teapot_proto_depIdxs = nil +} diff --git a/app/api/v1/gen/grpc/teapot/pb/goagen_v1_teapot.proto b/app/api/v1/gen/grpc/teapot/pb/goagen_v1_teapot.proto new file mode 100644 index 0000000..664824e --- /dev/null +++ b/app/api/v1/gen/grpc/teapot/pb/goagen_v1_teapot.proto @@ -0,0 +1,26 @@ +// Code generated with goa v3.19.1, DO NOT EDIT. +// +// teapot protocol buffer definition +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +syntax = "proto3"; + +package teapot; + +option go_package = "/teapotpb"; + +// Service is the teapot service interface. +service Teapot { + // Echo implements Echo. + rpc Echo (EchoRequest) returns (EchoResponse); +} + +message EchoRequest { + string text = 1; +} + +message EchoResponse { + string text = 1; +} diff --git a/app/api/v1/gen/grpc/teapot/pb/goagen_v1_teapot_grpc.pb.go b/app/api/v1/gen/grpc/teapot/pb/goagen_v1_teapot_grpc.pb.go new file mode 100644 index 0000000..16b7501 --- /dev/null +++ b/app/api/v1/gen/grpc/teapot/pb/goagen_v1_teapot_grpc.pb.go @@ -0,0 +1,134 @@ +// Code generated with goa v3.19.1, DO NOT EDIT. +// +// teapot protocol buffer definition +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v5.29.1 +// source: goagen_v1_teapot.proto + +package teapotpb + +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 ( + Teapot_Echo_FullMethodName = "/teapot.Teapot/Echo" +) + +// TeapotClient is the client API for Teapot 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. +// +// Service is the teapot service interface. +type TeapotClient interface { + // Echo implements Echo. + Echo(ctx context.Context, in *EchoRequest, opts ...grpc.CallOption) (*EchoResponse, error) +} + +type teapotClient struct { + cc grpc.ClientConnInterface +} + +func NewTeapotClient(cc grpc.ClientConnInterface) TeapotClient { + return &teapotClient{cc} +} + +func (c *teapotClient) Echo(ctx context.Context, in *EchoRequest, opts ...grpc.CallOption) (*EchoResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(EchoResponse) + err := c.cc.Invoke(ctx, Teapot_Echo_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// TeapotServer is the server API for Teapot service. +// All implementations must embed UnimplementedTeapotServer +// for forward compatibility. +// +// Service is the teapot service interface. +type TeapotServer interface { + // Echo implements Echo. + Echo(context.Context, *EchoRequest) (*EchoResponse, error) + mustEmbedUnimplementedTeapotServer() +} + +// UnimplementedTeapotServer must 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 UnimplementedTeapotServer struct{} + +func (UnimplementedTeapotServer) Echo(context.Context, *EchoRequest) (*EchoResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Echo not implemented") +} +func (UnimplementedTeapotServer) mustEmbedUnimplementedTeapotServer() {} +func (UnimplementedTeapotServer) testEmbeddedByValue() {} + +// UnsafeTeapotServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to TeapotServer will +// result in compilation errors. +type UnsafeTeapotServer interface { + mustEmbedUnimplementedTeapotServer() +} + +func RegisterTeapotServer(s grpc.ServiceRegistrar, srv TeapotServer) { + // If the following call pancis, it indicates UnimplementedTeapotServer 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(&Teapot_ServiceDesc, srv) +} + +func _Teapot_Echo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(EchoRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(TeapotServer).Echo(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Teapot_Echo_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TeapotServer).Echo(ctx, req.(*EchoRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// Teapot_ServiceDesc is the grpc.ServiceDesc for Teapot service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Teapot_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "teapot.Teapot", + HandlerType: (*TeapotServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Echo", + Handler: _Teapot_Echo_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "goagen_v1_teapot.proto", +} diff --git a/app/api/v1/gen/grpc/teapot/server/encode_decode.go b/app/api/v1/gen/grpc/teapot/server/encode_decode.go new file mode 100644 index 0000000..b1dc5d2 --- /dev/null +++ b/app/api/v1/gen/grpc/teapot/server/encode_decode.go @@ -0,0 +1,46 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// teapot gRPC server encoders and decoders +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package server + +import ( + "context" + + teapotpb "github.com/jace-ys/countup/api/v1/gen/grpc/teapot/pb" + teapot "github.com/jace-ys/countup/api/v1/gen/teapot" + goagrpc "goa.design/goa/v3/grpc" + "google.golang.org/grpc/metadata" +) + +// EncodeEchoResponse encodes responses from the "teapot" service "Echo" +// endpoint. +func EncodeEchoResponse(ctx context.Context, v any, hdr, trlr *metadata.MD) (any, error) { + result, ok := v.(*teapot.EchoResult) + if !ok { + return nil, goagrpc.ErrInvalidType("teapot", "Echo", "*teapot.EchoResult", v) + } + resp := NewProtoEchoResponse(result) + return resp, nil +} + +// DecodeEchoRequest decodes requests sent to "teapot" service "Echo" endpoint. +func DecodeEchoRequest(ctx context.Context, v any, md metadata.MD) (any, error) { + var ( + message *teapotpb.EchoRequest + ok bool + ) + { + if message, ok = v.(*teapotpb.EchoRequest); !ok { + return nil, goagrpc.ErrInvalidType("teapot", "Echo", "*teapotpb.EchoRequest", v) + } + } + var payload *teapot.EchoPayload + { + payload = NewEchoPayload(message) + } + return payload, nil +} diff --git a/app/api/v1/gen/grpc/teapot/server/server.go b/app/api/v1/gen/grpc/teapot/server/server.go new file mode 100644 index 0000000..ac1bcc4 --- /dev/null +++ b/app/api/v1/gen/grpc/teapot/server/server.go @@ -0,0 +1,50 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// teapot gRPC server +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package server + +import ( + "context" + + teapotpb "github.com/jace-ys/countup/api/v1/gen/grpc/teapot/pb" + teapot "github.com/jace-ys/countup/api/v1/gen/teapot" + goagrpc "goa.design/goa/v3/grpc" + goa "goa.design/goa/v3/pkg" +) + +// Server implements the teapotpb.TeapotServer interface. +type Server struct { + EchoH goagrpc.UnaryHandler + teapotpb.UnimplementedTeapotServer +} + +// New instantiates the server struct with the teapot service endpoints. +func New(e *teapot.Endpoints, uh goagrpc.UnaryHandler) *Server { + return &Server{ + EchoH: NewEchoHandler(e.Echo, uh), + } +} + +// NewEchoHandler creates a gRPC handler which serves the "teapot" service +// "Echo" endpoint. +func NewEchoHandler(endpoint goa.Endpoint, h goagrpc.UnaryHandler) goagrpc.UnaryHandler { + if h == nil { + h = goagrpc.NewUnaryHandler(endpoint, DecodeEchoRequest, EncodeEchoResponse) + } + return h +} + +// Echo implements the "Echo" method in teapotpb.TeapotServer interface. +func (s *Server) Echo(ctx context.Context, message *teapotpb.EchoRequest) (*teapotpb.EchoResponse, error) { + ctx = context.WithValue(ctx, goa.MethodKey, "Echo") + ctx = context.WithValue(ctx, goa.ServiceKey, "teapot") + resp, err := s.EchoH.Handle(ctx, message) + if err != nil { + return nil, goagrpc.EncodeError(err) + } + return resp.(*teapotpb.EchoResponse), nil +} diff --git a/app/api/v1/gen/grpc/teapot/server/types.go b/app/api/v1/gen/grpc/teapot/server/types.go new file mode 100644 index 0000000..284ed0b --- /dev/null +++ b/app/api/v1/gen/grpc/teapot/server/types.go @@ -0,0 +1,31 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// teapot gRPC server types +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package server + +import ( + teapotpb "github.com/jace-ys/countup/api/v1/gen/grpc/teapot/pb" + teapot "github.com/jace-ys/countup/api/v1/gen/teapot" +) + +// NewEchoPayload builds the payload of the "Echo" endpoint of the "teapot" +// service from the gRPC request type. +func NewEchoPayload(message *teapotpb.EchoRequest) *teapot.EchoPayload { + v := &teapot.EchoPayload{ + Text: message.Text, + } + return v +} + +// NewProtoEchoResponse builds the gRPC response type from the result of the +// "Echo" endpoint of the "teapot" service. +func NewProtoEchoResponse(result *teapot.EchoResult) *teapotpb.EchoResponse { + message := &teapotpb.EchoResponse{ + Text: result.Text, + } + return message +} diff --git a/app/api/v1/gen/http/api/client/cli.go b/app/api/v1/gen/http/api/client/cli.go new file mode 100644 index 0000000..0856ab4 --- /dev/null +++ b/app/api/v1/gen/http/api/client/cli.go @@ -0,0 +1,56 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// api HTTP client CLI support package +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package client + +import ( + "encoding/json" + "fmt" + + api "github.com/jace-ys/countup/api/v1/gen/api" + goa "goa.design/goa/v3/pkg" +) + +// BuildAuthTokenPayload builds the payload for the api AuthToken endpoint from +// CLI flags. +func BuildAuthTokenPayload(apiAuthTokenBody string) (*api.AuthTokenPayload, error) { + var err error + var body AuthTokenRequestBody + { + err = json.Unmarshal([]byte(apiAuthTokenBody), &body) + if err != nil { + return nil, fmt.Errorf("invalid JSON for body, \nerror: %s, \nexample of valid JSON:\n%s", err, "'{\n \"access_token\": \"Itaque unde qui ut molestiae et omnis.\",\n \"provider\": \"google\"\n }'") + } + if !(body.Provider == "google") { + err = goa.MergeErrors(err, goa.InvalidEnumValueError("body.provider", body.Provider, []any{"google"})) + } + if err != nil { + return nil, err + } + } + v := &api.AuthTokenPayload{ + Provider: body.Provider, + AccessToken: body.AccessToken, + } + + return v, nil +} + +// BuildCounterIncrementPayload builds the payload for the api CounterIncrement +// endpoint from CLI flags. +func BuildCounterIncrementPayload(apiCounterIncrementToken string) (*api.CounterIncrementPayload, error) { + var token *string + { + if apiCounterIncrementToken != "" { + token = &apiCounterIncrementToken + } + } + v := &api.CounterIncrementPayload{} + v.Token = token + + return v, nil +} diff --git a/app/api/v1/gen/http/api/client/client.go b/app/api/v1/gen/http/api/client/client.go new file mode 100644 index 0000000..6d15d36 --- /dev/null +++ b/app/api/v1/gen/http/api/client/client.go @@ -0,0 +1,128 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// api client HTTP transport +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package client + +import ( + "context" + "net/http" + + goahttp "goa.design/goa/v3/http" + goa "goa.design/goa/v3/pkg" +) + +// Client lists the api service endpoint HTTP clients. +type Client struct { + // AuthToken Doer is the HTTP client used to make requests to the AuthToken + // endpoint. + AuthTokenDoer goahttp.Doer + + // CounterGet Doer is the HTTP client used to make requests to the CounterGet + // endpoint. + CounterGetDoer goahttp.Doer + + // CounterIncrement Doer is the HTTP client used to make requests to the + // CounterIncrement endpoint. + CounterIncrementDoer goahttp.Doer + + // RestoreResponseBody controls whether the response bodies are reset after + // decoding so they can be read again. + RestoreResponseBody bool + + scheme string + host string + encoder func(*http.Request) goahttp.Encoder + decoder func(*http.Response) goahttp.Decoder +} + +// NewClient instantiates HTTP clients for all the api service servers. +func NewClient( + scheme string, + host string, + doer goahttp.Doer, + enc func(*http.Request) goahttp.Encoder, + dec func(*http.Response) goahttp.Decoder, + restoreBody bool, +) *Client { + return &Client{ + AuthTokenDoer: doer, + CounterGetDoer: doer, + CounterIncrementDoer: doer, + RestoreResponseBody: restoreBody, + scheme: scheme, + host: host, + decoder: dec, + encoder: enc, + } +} + +// AuthToken returns an endpoint that makes HTTP requests to the api service +// AuthToken server. +func (c *Client) AuthToken() goa.Endpoint { + var ( + encodeRequest = EncodeAuthTokenRequest(c.encoder) + decodeResponse = DecodeAuthTokenResponse(c.decoder, c.RestoreResponseBody) + ) + return func(ctx context.Context, v any) (any, error) { + req, err := c.BuildAuthTokenRequest(ctx, v) + if err != nil { + return nil, err + } + err = encodeRequest(req, v) + if err != nil { + return nil, err + } + resp, err := c.AuthTokenDoer.Do(req) + if err != nil { + return nil, goahttp.ErrRequestError("api", "AuthToken", err) + } + return decodeResponse(resp) + } +} + +// CounterGet returns an endpoint that makes HTTP requests to the api service +// CounterGet server. +func (c *Client) CounterGet() goa.Endpoint { + var ( + decodeResponse = DecodeCounterGetResponse(c.decoder, c.RestoreResponseBody) + ) + return func(ctx context.Context, v any) (any, error) { + req, err := c.BuildCounterGetRequest(ctx, v) + if err != nil { + return nil, err + } + resp, err := c.CounterGetDoer.Do(req) + if err != nil { + return nil, goahttp.ErrRequestError("api", "CounterGet", err) + } + return decodeResponse(resp) + } +} + +// CounterIncrement returns an endpoint that makes HTTP requests to the api +// service CounterIncrement server. +func (c *Client) CounterIncrement() goa.Endpoint { + var ( + encodeRequest = EncodeCounterIncrementRequest(c.encoder) + decodeResponse = DecodeCounterIncrementResponse(c.decoder, c.RestoreResponseBody) + ) + return func(ctx context.Context, v any) (any, error) { + req, err := c.BuildCounterIncrementRequest(ctx, v) + if err != nil { + return nil, err + } + err = encodeRequest(req, v) + if err != nil { + return nil, err + } + resp, err := c.CounterIncrementDoer.Do(req) + if err != nil { + return nil, goahttp.ErrRequestError("api", "CounterIncrement", err) + } + return decodeResponse(resp) + } +} diff --git a/app/api/v1/gen/http/api/client/encode_decode.go b/app/api/v1/gen/http/api/client/encode_decode.go new file mode 100644 index 0000000..ed587ad --- /dev/null +++ b/app/api/v1/gen/http/api/client/encode_decode.go @@ -0,0 +1,359 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// api HTTP client encoders and decoders +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package client + +import ( + "bytes" + "context" + "io" + "net/http" + "net/url" + "strings" + + api "github.com/jace-ys/countup/api/v1/gen/api" + apiviews "github.com/jace-ys/countup/api/v1/gen/api/views" + goahttp "goa.design/goa/v3/http" +) + +// BuildAuthTokenRequest instantiates a HTTP request object with method and +// path set to call the "api" service "AuthToken" endpoint +func (c *Client) BuildAuthTokenRequest(ctx context.Context, v any) (*http.Request, error) { + u := &url.URL{Scheme: c.scheme, Host: c.host, Path: AuthTokenAPIPath()} + req, err := http.NewRequest("POST", u.String(), nil) + if err != nil { + return nil, goahttp.ErrInvalidURL("api", "AuthToken", u.String(), err) + } + if ctx != nil { + req = req.WithContext(ctx) + } + + return req, nil +} + +// EncodeAuthTokenRequest returns an encoder for requests sent to the api +// AuthToken server. +func EncodeAuthTokenRequest(encoder func(*http.Request) goahttp.Encoder) func(*http.Request, any) error { + return func(req *http.Request, v any) error { + p, ok := v.(*api.AuthTokenPayload) + if !ok { + return goahttp.ErrInvalidType("api", "AuthToken", "*api.AuthTokenPayload", v) + } + body := NewAuthTokenRequestBody(p) + if err := encoder(req).Encode(&body); err != nil { + return goahttp.ErrEncodingError("api", "AuthToken", err) + } + return nil + } +} + +// DecodeAuthTokenResponse returns a decoder for responses returned by the api +// AuthToken endpoint. restoreBody controls whether the response body should be +// restored after having been read. +// DecodeAuthTokenResponse may return the following errors: +// - "incomplete_auth_info" (type *goa.ServiceError): http.StatusUnauthorized +// - "unauthorized" (type *goa.ServiceError): http.StatusUnauthorized +// - "forbidden" (type *goa.ServiceError): http.StatusForbidden +// - error: internal error +func DecodeAuthTokenResponse(decoder func(*http.Response) goahttp.Decoder, restoreBody bool) func(*http.Response) (any, error) { + return func(resp *http.Response) (any, error) { + if restoreBody { + b, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + defer func() { + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + }() + } else { + defer resp.Body.Close() + } + switch resp.StatusCode { + case http.StatusOK: + var ( + body AuthTokenResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("api", "AuthToken", err) + } + err = ValidateAuthTokenResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("api", "AuthToken", err) + } + res := NewAuthTokenResultOK(&body) + return res, nil + case http.StatusUnauthorized: + en := resp.Header.Get("goa-error") + switch en { + case "incomplete_auth_info": + var ( + body AuthTokenIncompleteAuthInfoResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("api", "AuthToken", err) + } + err = ValidateAuthTokenIncompleteAuthInfoResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("api", "AuthToken", err) + } + return nil, NewAuthTokenIncompleteAuthInfo(&body) + case "unauthorized": + var ( + body AuthTokenUnauthorizedResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("api", "AuthToken", err) + } + err = ValidateAuthTokenUnauthorizedResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("api", "AuthToken", err) + } + return nil, NewAuthTokenUnauthorized(&body) + default: + body, _ := io.ReadAll(resp.Body) + return nil, goahttp.ErrInvalidResponse("api", "AuthToken", resp.StatusCode, string(body)) + } + case http.StatusForbidden: + var ( + body AuthTokenForbiddenResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("api", "AuthToken", err) + } + err = ValidateAuthTokenForbiddenResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("api", "AuthToken", err) + } + return nil, NewAuthTokenForbidden(&body) + default: + body, _ := io.ReadAll(resp.Body) + return nil, goahttp.ErrInvalidResponse("api", "AuthToken", resp.StatusCode, string(body)) + } + } +} + +// BuildCounterGetRequest instantiates a HTTP request object with method and +// path set to call the "api" service "CounterGet" endpoint +func (c *Client) BuildCounterGetRequest(ctx context.Context, v any) (*http.Request, error) { + u := &url.URL{Scheme: c.scheme, Host: c.host, Path: CounterGetAPIPath()} + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return nil, goahttp.ErrInvalidURL("api", "CounterGet", u.String(), err) + } + if ctx != nil { + req = req.WithContext(ctx) + } + + return req, nil +} + +// DecodeCounterGetResponse returns a decoder for responses returned by the api +// CounterGet endpoint. restoreBody controls whether the response body should +// be restored after having been read. +// DecodeCounterGetResponse may return the following errors: +// - "unauthorized" (type *goa.ServiceError): http.StatusUnauthorized +// - "forbidden" (type *goa.ServiceError): http.StatusForbidden +// - error: internal error +func DecodeCounterGetResponse(decoder func(*http.Response) goahttp.Decoder, restoreBody bool) func(*http.Response) (any, error) { + return func(resp *http.Response) (any, error) { + if restoreBody { + b, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + defer func() { + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + }() + } else { + defer resp.Body.Close() + } + switch resp.StatusCode { + case http.StatusOK: + var ( + body CounterGetResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("api", "CounterGet", err) + } + p := NewCounterGetCounterInfoOK(&body) + view := "default" + vres := &apiviews.CounterInfo{Projected: p, View: view} + if err = apiviews.ValidateCounterInfo(vres); err != nil { + return nil, goahttp.ErrValidationError("api", "CounterGet", err) + } + res := api.NewCounterInfo(vres) + return res, nil + case http.StatusUnauthorized: + var ( + body CounterGetUnauthorizedResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("api", "CounterGet", err) + } + err = ValidateCounterGetUnauthorizedResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("api", "CounterGet", err) + } + return nil, NewCounterGetUnauthorized(&body) + case http.StatusForbidden: + var ( + body CounterGetForbiddenResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("api", "CounterGet", err) + } + err = ValidateCounterGetForbiddenResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("api", "CounterGet", err) + } + return nil, NewCounterGetForbidden(&body) + default: + body, _ := io.ReadAll(resp.Body) + return nil, goahttp.ErrInvalidResponse("api", "CounterGet", resp.StatusCode, string(body)) + } + } +} + +// BuildCounterIncrementRequest instantiates a HTTP request object with method +// and path set to call the "api" service "CounterIncrement" endpoint +func (c *Client) BuildCounterIncrementRequest(ctx context.Context, v any) (*http.Request, error) { + u := &url.URL{Scheme: c.scheme, Host: c.host, Path: CounterIncrementAPIPath()} + req, err := http.NewRequest("POST", u.String(), nil) + if err != nil { + return nil, goahttp.ErrInvalidURL("api", "CounterIncrement", u.String(), err) + } + if ctx != nil { + req = req.WithContext(ctx) + } + + return req, nil +} + +// EncodeCounterIncrementRequest returns an encoder for requests sent to the +// api CounterIncrement server. +func EncodeCounterIncrementRequest(encoder func(*http.Request) goahttp.Encoder) func(*http.Request, any) error { + return func(req *http.Request, v any) error { + p, ok := v.(*api.CounterIncrementPayload) + if !ok { + return goahttp.ErrInvalidType("api", "CounterIncrement", "*api.CounterIncrementPayload", v) + } + if p.Token != nil { + head := *p.Token + if !strings.Contains(head, " ") { + req.Header.Set("Authorization", "Bearer "+head) + } else { + req.Header.Set("Authorization", head) + } + } + return nil + } +} + +// DecodeCounterIncrementResponse returns a decoder for responses returned by +// the api CounterIncrement endpoint. restoreBody controls whether the response +// body should be restored after having been read. +// DecodeCounterIncrementResponse may return the following errors: +// - "existing_increment_request" (type *goa.ServiceError): http.StatusTooManyRequests +// - "unauthorized" (type *goa.ServiceError): http.StatusUnauthorized +// - "forbidden" (type *goa.ServiceError): http.StatusForbidden +// - error: internal error +func DecodeCounterIncrementResponse(decoder func(*http.Response) goahttp.Decoder, restoreBody bool) func(*http.Response) (any, error) { + return func(resp *http.Response) (any, error) { + if restoreBody { + b, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + defer func() { + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + }() + } else { + defer resp.Body.Close() + } + switch resp.StatusCode { + case http.StatusAccepted: + var ( + body CounterIncrementResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("api", "CounterIncrement", err) + } + p := NewCounterIncrementCounterInfoAccepted(&body) + view := "default" + vres := &apiviews.CounterInfo{Projected: p, View: view} + if err = apiviews.ValidateCounterInfo(vres); err != nil { + return nil, goahttp.ErrValidationError("api", "CounterIncrement", err) + } + res := api.NewCounterInfo(vres) + return res, nil + case http.StatusTooManyRequests: + var ( + body CounterIncrementExistingIncrementRequestResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("api", "CounterIncrement", err) + } + err = ValidateCounterIncrementExistingIncrementRequestResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("api", "CounterIncrement", err) + } + return nil, NewCounterIncrementExistingIncrementRequest(&body) + case http.StatusUnauthorized: + var ( + body CounterIncrementUnauthorizedResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("api", "CounterIncrement", err) + } + err = ValidateCounterIncrementUnauthorizedResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("api", "CounterIncrement", err) + } + return nil, NewCounterIncrementUnauthorized(&body) + case http.StatusForbidden: + var ( + body CounterIncrementForbiddenResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("api", "CounterIncrement", err) + } + err = ValidateCounterIncrementForbiddenResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("api", "CounterIncrement", err) + } + return nil, NewCounterIncrementForbidden(&body) + default: + body, _ := io.ReadAll(resp.Body) + return nil, goahttp.ErrInvalidResponse("api", "CounterIncrement", resp.StatusCode, string(body)) + } + } +} diff --git a/app/api/v1/gen/http/api/client/paths.go b/app/api/v1/gen/http/api/client/paths.go new file mode 100644 index 0000000..8f92ed6 --- /dev/null +++ b/app/api/v1/gen/http/api/client/paths.go @@ -0,0 +1,23 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// HTTP request path constructors for the api service. +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package client + +// AuthTokenAPIPath returns the URL path to the api service AuthToken HTTP endpoint. +func AuthTokenAPIPath() string { + return "/api/v1/auth/token" +} + +// CounterGetAPIPath returns the URL path to the api service CounterGet HTTP endpoint. +func CounterGetAPIPath() string { + return "/api/v1/counter" +} + +// CounterIncrementAPIPath returns the URL path to the api service CounterIncrement HTTP endpoint. +func CounterIncrementAPIPath() string { + return "/api/v1/counter" +} diff --git a/app/api/v1/gen/http/api/client/types.go b/app/api/v1/gen/http/api/client/types.go new file mode 100644 index 0000000..8fc1ba6 --- /dev/null +++ b/app/api/v1/gen/http/api/client/types.go @@ -0,0 +1,558 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// api HTTP client types +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package client + +import ( + api "github.com/jace-ys/countup/api/v1/gen/api" + apiviews "github.com/jace-ys/countup/api/v1/gen/api/views" + goa "goa.design/goa/v3/pkg" +) + +// AuthTokenRequestBody is the type of the "api" service "AuthToken" endpoint +// HTTP request body. +type AuthTokenRequestBody struct { + Provider string `form:"provider" json:"provider" xml:"provider"` + AccessToken string `form:"access_token" json:"access_token" xml:"access_token"` +} + +// AuthTokenResponseBody is the type of the "api" service "AuthToken" endpoint +// HTTP response body. +type AuthTokenResponseBody struct { + Token *string `form:"token,omitempty" json:"token,omitempty" xml:"token,omitempty"` +} + +// CounterGetResponseBody is the type of the "api" service "CounterGet" +// endpoint HTTP response body. +type CounterGetResponseBody struct { + Count *int32 `form:"count,omitempty" json:"count,omitempty" xml:"count,omitempty"` + LastIncrementBy *string `form:"last_increment_by,omitempty" json:"last_increment_by,omitempty" xml:"last_increment_by,omitempty"` + LastIncrementAt *string `form:"last_increment_at,omitempty" json:"last_increment_at,omitempty" xml:"last_increment_at,omitempty"` + NextFinalizeAt *string `form:"next_finalize_at,omitempty" json:"next_finalize_at,omitempty" xml:"next_finalize_at,omitempty"` +} + +// CounterIncrementResponseBody is the type of the "api" service +// "CounterIncrement" endpoint HTTP response body. +type CounterIncrementResponseBody struct { + Count *int32 `form:"count,omitempty" json:"count,omitempty" xml:"count,omitempty"` + LastIncrementBy *string `form:"last_increment_by,omitempty" json:"last_increment_by,omitempty" xml:"last_increment_by,omitempty"` + LastIncrementAt *string `form:"last_increment_at,omitempty" json:"last_increment_at,omitempty" xml:"last_increment_at,omitempty"` + NextFinalizeAt *string `form:"next_finalize_at,omitempty" json:"next_finalize_at,omitempty" xml:"next_finalize_at,omitempty"` +} + +// AuthTokenIncompleteAuthInfoResponseBody is the type of the "api" service +// "AuthToken" endpoint HTTP response body for the "incomplete_auth_info" error. +type AuthTokenIncompleteAuthInfoResponseBody struct { + // Name is the name of this class of errors. + Name *string `form:"name,omitempty" json:"name,omitempty" xml:"name,omitempty"` + // ID is a unique identifier for this particular occurrence of the problem. + ID *string `form:"id,omitempty" json:"id,omitempty" xml:"id,omitempty"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` + // Is the error temporary? + Temporary *bool `form:"temporary,omitempty" json:"temporary,omitempty" xml:"temporary,omitempty"` + // Is the error a timeout? + Timeout *bool `form:"timeout,omitempty" json:"timeout,omitempty" xml:"timeout,omitempty"` + // Is the error a server-side fault? + Fault *bool `form:"fault,omitempty" json:"fault,omitempty" xml:"fault,omitempty"` +} + +// AuthTokenUnauthorizedResponseBody is the type of the "api" service +// "AuthToken" endpoint HTTP response body for the "unauthorized" error. +type AuthTokenUnauthorizedResponseBody struct { + // Name is the name of this class of errors. + Name *string `form:"name,omitempty" json:"name,omitempty" xml:"name,omitempty"` + // ID is a unique identifier for this particular occurrence of the problem. + ID *string `form:"id,omitempty" json:"id,omitempty" xml:"id,omitempty"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` + // Is the error temporary? + Temporary *bool `form:"temporary,omitempty" json:"temporary,omitempty" xml:"temporary,omitempty"` + // Is the error a timeout? + Timeout *bool `form:"timeout,omitempty" json:"timeout,omitempty" xml:"timeout,omitempty"` + // Is the error a server-side fault? + Fault *bool `form:"fault,omitempty" json:"fault,omitempty" xml:"fault,omitempty"` +} + +// AuthTokenForbiddenResponseBody is the type of the "api" service "AuthToken" +// endpoint HTTP response body for the "forbidden" error. +type AuthTokenForbiddenResponseBody struct { + // Name is the name of this class of errors. + Name *string `form:"name,omitempty" json:"name,omitempty" xml:"name,omitempty"` + // ID is a unique identifier for this particular occurrence of the problem. + ID *string `form:"id,omitempty" json:"id,omitempty" xml:"id,omitempty"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` + // Is the error temporary? + Temporary *bool `form:"temporary,omitempty" json:"temporary,omitempty" xml:"temporary,omitempty"` + // Is the error a timeout? + Timeout *bool `form:"timeout,omitempty" json:"timeout,omitempty" xml:"timeout,omitempty"` + // Is the error a server-side fault? + Fault *bool `form:"fault,omitempty" json:"fault,omitempty" xml:"fault,omitempty"` +} + +// CounterGetUnauthorizedResponseBody is the type of the "api" service +// "CounterGet" endpoint HTTP response body for the "unauthorized" error. +type CounterGetUnauthorizedResponseBody struct { + // Name is the name of this class of errors. + Name *string `form:"name,omitempty" json:"name,omitempty" xml:"name,omitempty"` + // ID is a unique identifier for this particular occurrence of the problem. + ID *string `form:"id,omitempty" json:"id,omitempty" xml:"id,omitempty"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` + // Is the error temporary? + Temporary *bool `form:"temporary,omitempty" json:"temporary,omitempty" xml:"temporary,omitempty"` + // Is the error a timeout? + Timeout *bool `form:"timeout,omitempty" json:"timeout,omitempty" xml:"timeout,omitempty"` + // Is the error a server-side fault? + Fault *bool `form:"fault,omitempty" json:"fault,omitempty" xml:"fault,omitempty"` +} + +// CounterGetForbiddenResponseBody is the type of the "api" service +// "CounterGet" endpoint HTTP response body for the "forbidden" error. +type CounterGetForbiddenResponseBody struct { + // Name is the name of this class of errors. + Name *string `form:"name,omitempty" json:"name,omitempty" xml:"name,omitempty"` + // ID is a unique identifier for this particular occurrence of the problem. + ID *string `form:"id,omitempty" json:"id,omitempty" xml:"id,omitempty"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` + // Is the error temporary? + Temporary *bool `form:"temporary,omitempty" json:"temporary,omitempty" xml:"temporary,omitempty"` + // Is the error a timeout? + Timeout *bool `form:"timeout,omitempty" json:"timeout,omitempty" xml:"timeout,omitempty"` + // Is the error a server-side fault? + Fault *bool `form:"fault,omitempty" json:"fault,omitempty" xml:"fault,omitempty"` +} + +// CounterIncrementExistingIncrementRequestResponseBody is the type of the +// "api" service "CounterIncrement" endpoint HTTP response body for the +// "existing_increment_request" error. +type CounterIncrementExistingIncrementRequestResponseBody struct { + // Name is the name of this class of errors. + Name *string `form:"name,omitempty" json:"name,omitempty" xml:"name,omitempty"` + // ID is a unique identifier for this particular occurrence of the problem. + ID *string `form:"id,omitempty" json:"id,omitempty" xml:"id,omitempty"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` + // Is the error temporary? + Temporary *bool `form:"temporary,omitempty" json:"temporary,omitempty" xml:"temporary,omitempty"` + // Is the error a timeout? + Timeout *bool `form:"timeout,omitempty" json:"timeout,omitempty" xml:"timeout,omitempty"` + // Is the error a server-side fault? + Fault *bool `form:"fault,omitempty" json:"fault,omitempty" xml:"fault,omitempty"` +} + +// CounterIncrementUnauthorizedResponseBody is the type of the "api" service +// "CounterIncrement" endpoint HTTP response body for the "unauthorized" error. +type CounterIncrementUnauthorizedResponseBody struct { + // Name is the name of this class of errors. + Name *string `form:"name,omitempty" json:"name,omitempty" xml:"name,omitempty"` + // ID is a unique identifier for this particular occurrence of the problem. + ID *string `form:"id,omitempty" json:"id,omitempty" xml:"id,omitempty"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` + // Is the error temporary? + Temporary *bool `form:"temporary,omitempty" json:"temporary,omitempty" xml:"temporary,omitempty"` + // Is the error a timeout? + Timeout *bool `form:"timeout,omitempty" json:"timeout,omitempty" xml:"timeout,omitempty"` + // Is the error a server-side fault? + Fault *bool `form:"fault,omitempty" json:"fault,omitempty" xml:"fault,omitempty"` +} + +// CounterIncrementForbiddenResponseBody is the type of the "api" service +// "CounterIncrement" endpoint HTTP response body for the "forbidden" error. +type CounterIncrementForbiddenResponseBody struct { + // Name is the name of this class of errors. + Name *string `form:"name,omitempty" json:"name,omitempty" xml:"name,omitempty"` + // ID is a unique identifier for this particular occurrence of the problem. + ID *string `form:"id,omitempty" json:"id,omitempty" xml:"id,omitempty"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` + // Is the error temporary? + Temporary *bool `form:"temporary,omitempty" json:"temporary,omitempty" xml:"temporary,omitempty"` + // Is the error a timeout? + Timeout *bool `form:"timeout,omitempty" json:"timeout,omitempty" xml:"timeout,omitempty"` + // Is the error a server-side fault? + Fault *bool `form:"fault,omitempty" json:"fault,omitempty" xml:"fault,omitempty"` +} + +// NewAuthTokenRequestBody builds the HTTP request body from the payload of the +// "AuthToken" endpoint of the "api" service. +func NewAuthTokenRequestBody(p *api.AuthTokenPayload) *AuthTokenRequestBody { + body := &AuthTokenRequestBody{ + Provider: p.Provider, + AccessToken: p.AccessToken, + } + return body +} + +// NewAuthTokenResultOK builds a "api" service "AuthToken" endpoint result from +// a HTTP "OK" response. +func NewAuthTokenResultOK(body *AuthTokenResponseBody) *api.AuthTokenResult { + v := &api.AuthTokenResult{ + Token: *body.Token, + } + + return v +} + +// NewAuthTokenIncompleteAuthInfo builds a api service AuthToken endpoint +// incomplete_auth_info error. +func NewAuthTokenIncompleteAuthInfo(body *AuthTokenIncompleteAuthInfoResponseBody) *goa.ServiceError { + v := &goa.ServiceError{ + Name: *body.Name, + ID: *body.ID, + Message: *body.Message, + Temporary: *body.Temporary, + Timeout: *body.Timeout, + Fault: *body.Fault, + } + + return v +} + +// NewAuthTokenUnauthorized builds a api service AuthToken endpoint +// unauthorized error. +func NewAuthTokenUnauthorized(body *AuthTokenUnauthorizedResponseBody) *goa.ServiceError { + v := &goa.ServiceError{ + Name: *body.Name, + ID: *body.ID, + Message: *body.Message, + Temporary: *body.Temporary, + Timeout: *body.Timeout, + Fault: *body.Fault, + } + + return v +} + +// NewAuthTokenForbidden builds a api service AuthToken endpoint forbidden +// error. +func NewAuthTokenForbidden(body *AuthTokenForbiddenResponseBody) *goa.ServiceError { + v := &goa.ServiceError{ + Name: *body.Name, + ID: *body.ID, + Message: *body.Message, + Temporary: *body.Temporary, + Timeout: *body.Timeout, + Fault: *body.Fault, + } + + return v +} + +// NewCounterGetCounterInfoOK builds a "api" service "CounterGet" endpoint +// result from a HTTP "OK" response. +func NewCounterGetCounterInfoOK(body *CounterGetResponseBody) *apiviews.CounterInfoView { + v := &apiviews.CounterInfoView{ + Count: body.Count, + LastIncrementBy: body.LastIncrementBy, + LastIncrementAt: body.LastIncrementAt, + NextFinalizeAt: body.NextFinalizeAt, + } + + return v +} + +// NewCounterGetUnauthorized builds a api service CounterGet endpoint +// unauthorized error. +func NewCounterGetUnauthorized(body *CounterGetUnauthorizedResponseBody) *goa.ServiceError { + v := &goa.ServiceError{ + Name: *body.Name, + ID: *body.ID, + Message: *body.Message, + Temporary: *body.Temporary, + Timeout: *body.Timeout, + Fault: *body.Fault, + } + + return v +} + +// NewCounterGetForbidden builds a api service CounterGet endpoint forbidden +// error. +func NewCounterGetForbidden(body *CounterGetForbiddenResponseBody) *goa.ServiceError { + v := &goa.ServiceError{ + Name: *body.Name, + ID: *body.ID, + Message: *body.Message, + Temporary: *body.Temporary, + Timeout: *body.Timeout, + Fault: *body.Fault, + } + + return v +} + +// NewCounterIncrementCounterInfoAccepted builds a "api" service +// "CounterIncrement" endpoint result from a HTTP "Accepted" response. +func NewCounterIncrementCounterInfoAccepted(body *CounterIncrementResponseBody) *apiviews.CounterInfoView { + v := &apiviews.CounterInfoView{ + Count: body.Count, + LastIncrementBy: body.LastIncrementBy, + LastIncrementAt: body.LastIncrementAt, + NextFinalizeAt: body.NextFinalizeAt, + } + + return v +} + +// NewCounterIncrementExistingIncrementRequest builds a api service +// CounterIncrement endpoint existing_increment_request error. +func NewCounterIncrementExistingIncrementRequest(body *CounterIncrementExistingIncrementRequestResponseBody) *goa.ServiceError { + v := &goa.ServiceError{ + Name: *body.Name, + ID: *body.ID, + Message: *body.Message, + Temporary: *body.Temporary, + Timeout: *body.Timeout, + Fault: *body.Fault, + } + + return v +} + +// NewCounterIncrementUnauthorized builds a api service CounterIncrement +// endpoint unauthorized error. +func NewCounterIncrementUnauthorized(body *CounterIncrementUnauthorizedResponseBody) *goa.ServiceError { + v := &goa.ServiceError{ + Name: *body.Name, + ID: *body.ID, + Message: *body.Message, + Temporary: *body.Temporary, + Timeout: *body.Timeout, + Fault: *body.Fault, + } + + return v +} + +// NewCounterIncrementForbidden builds a api service CounterIncrement endpoint +// forbidden error. +func NewCounterIncrementForbidden(body *CounterIncrementForbiddenResponseBody) *goa.ServiceError { + v := &goa.ServiceError{ + Name: *body.Name, + ID: *body.ID, + Message: *body.Message, + Temporary: *body.Temporary, + Timeout: *body.Timeout, + Fault: *body.Fault, + } + + return v +} + +// ValidateAuthTokenResponseBody runs the validations defined on +// AuthTokenResponseBody +func ValidateAuthTokenResponseBody(body *AuthTokenResponseBody) (err error) { + if body.Token == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("token", "body")) + } + return +} + +// ValidateAuthTokenIncompleteAuthInfoResponseBody runs the validations defined +// on AuthToken_incomplete_auth_info_Response_Body +func ValidateAuthTokenIncompleteAuthInfoResponseBody(body *AuthTokenIncompleteAuthInfoResponseBody) (err error) { + if body.Name == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("name", "body")) + } + if body.ID == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("id", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + if body.Temporary == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("temporary", "body")) + } + if body.Timeout == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("timeout", "body")) + } + if body.Fault == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("fault", "body")) + } + return +} + +// ValidateAuthTokenUnauthorizedResponseBody runs the validations defined on +// AuthToken_unauthorized_Response_Body +func ValidateAuthTokenUnauthorizedResponseBody(body *AuthTokenUnauthorizedResponseBody) (err error) { + if body.Name == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("name", "body")) + } + if body.ID == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("id", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + if body.Temporary == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("temporary", "body")) + } + if body.Timeout == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("timeout", "body")) + } + if body.Fault == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("fault", "body")) + } + return +} + +// ValidateAuthTokenForbiddenResponseBody runs the validations defined on +// AuthToken_forbidden_Response_Body +func ValidateAuthTokenForbiddenResponseBody(body *AuthTokenForbiddenResponseBody) (err error) { + if body.Name == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("name", "body")) + } + if body.ID == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("id", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + if body.Temporary == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("temporary", "body")) + } + if body.Timeout == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("timeout", "body")) + } + if body.Fault == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("fault", "body")) + } + return +} + +// ValidateCounterGetUnauthorizedResponseBody runs the validations defined on +// CounterGet_unauthorized_Response_Body +func ValidateCounterGetUnauthorizedResponseBody(body *CounterGetUnauthorizedResponseBody) (err error) { + if body.Name == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("name", "body")) + } + if body.ID == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("id", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + if body.Temporary == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("temporary", "body")) + } + if body.Timeout == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("timeout", "body")) + } + if body.Fault == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("fault", "body")) + } + return +} + +// ValidateCounterGetForbiddenResponseBody runs the validations defined on +// CounterGet_forbidden_Response_Body +func ValidateCounterGetForbiddenResponseBody(body *CounterGetForbiddenResponseBody) (err error) { + if body.Name == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("name", "body")) + } + if body.ID == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("id", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + if body.Temporary == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("temporary", "body")) + } + if body.Timeout == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("timeout", "body")) + } + if body.Fault == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("fault", "body")) + } + return +} + +// ValidateCounterIncrementExistingIncrementRequestResponseBody runs the +// validations defined on +// CounterIncrement_existing_increment_request_Response_Body +func ValidateCounterIncrementExistingIncrementRequestResponseBody(body *CounterIncrementExistingIncrementRequestResponseBody) (err error) { + if body.Name == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("name", "body")) + } + if body.ID == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("id", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + if body.Temporary == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("temporary", "body")) + } + if body.Timeout == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("timeout", "body")) + } + if body.Fault == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("fault", "body")) + } + return +} + +// ValidateCounterIncrementUnauthorizedResponseBody runs the validations +// defined on CounterIncrement_unauthorized_Response_Body +func ValidateCounterIncrementUnauthorizedResponseBody(body *CounterIncrementUnauthorizedResponseBody) (err error) { + if body.Name == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("name", "body")) + } + if body.ID == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("id", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + if body.Temporary == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("temporary", "body")) + } + if body.Timeout == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("timeout", "body")) + } + if body.Fault == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("fault", "body")) + } + return +} + +// ValidateCounterIncrementForbiddenResponseBody runs the validations defined +// on CounterIncrement_forbidden_Response_Body +func ValidateCounterIncrementForbiddenResponseBody(body *CounterIncrementForbiddenResponseBody) (err error) { + if body.Name == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("name", "body")) + } + if body.ID == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("id", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + if body.Temporary == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("temporary", "body")) + } + if body.Timeout == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("timeout", "body")) + } + if body.Fault == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("fault", "body")) + } + return +} diff --git a/app/api/v1/gen/http/api/server/encode_decode.go b/app/api/v1/gen/http/api/server/encode_decode.go new file mode 100644 index 0000000..20fe360 --- /dev/null +++ b/app/api/v1/gen/http/api/server/encode_decode.go @@ -0,0 +1,262 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// api HTTP server encoders and decoders +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package server + +import ( + "context" + "errors" + "io" + "net/http" + "strings" + + api "github.com/jace-ys/countup/api/v1/gen/api" + apiviews "github.com/jace-ys/countup/api/v1/gen/api/views" + goahttp "goa.design/goa/v3/http" + goa "goa.design/goa/v3/pkg" +) + +// EncodeAuthTokenResponse returns an encoder for responses returned by the api +// AuthToken endpoint. +func EncodeAuthTokenResponse(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder) func(context.Context, http.ResponseWriter, any) error { + return func(ctx context.Context, w http.ResponseWriter, v any) error { + res, _ := v.(*api.AuthTokenResult) + enc := encoder(ctx, w) + body := NewAuthTokenResponseBody(res) + w.WriteHeader(http.StatusOK) + return enc.Encode(body) + } +} + +// DecodeAuthTokenRequest returns a decoder for requests sent to the api +// AuthToken endpoint. +func DecodeAuthTokenRequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp.Decoder) func(*http.Request) (any, error) { + return func(r *http.Request) (any, error) { + var ( + body AuthTokenRequestBody + err error + ) + err = decoder(r).Decode(&body) + if err != nil { + if err == io.EOF { + return nil, goa.MissingPayloadError() + } + var gerr *goa.ServiceError + if errors.As(err, &gerr) { + return nil, gerr + } + return nil, goa.DecodePayloadError(err.Error()) + } + err = ValidateAuthTokenRequestBody(&body) + if err != nil { + return nil, err + } + payload := NewAuthTokenPayload(&body) + + return payload, nil + } +} + +// EncodeAuthTokenError returns an encoder for errors returned by the AuthToken +// api endpoint. +func EncodeAuthTokenError(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, formatter func(ctx context.Context, err error) goahttp.Statuser) func(context.Context, http.ResponseWriter, error) error { + encodeError := goahttp.ErrorEncoder(encoder, formatter) + return func(ctx context.Context, w http.ResponseWriter, v error) error { + var en goa.GoaErrorNamer + if !errors.As(v, &en) { + return encodeError(ctx, w, v) + } + switch en.GoaErrorName() { + case "incomplete_auth_info": + var res *goa.ServiceError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewAuthTokenIncompleteAuthInfoResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusUnauthorized) + return enc.Encode(body) + case "unauthorized": + var res *goa.ServiceError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewAuthTokenUnauthorizedResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusUnauthorized) + return enc.Encode(body) + case "forbidden": + var res *goa.ServiceError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewAuthTokenForbiddenResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusForbidden) + return enc.Encode(body) + default: + return encodeError(ctx, w, v) + } + } +} + +// EncodeCounterGetResponse returns an encoder for responses returned by the +// api CounterGet endpoint. +func EncodeCounterGetResponse(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder) func(context.Context, http.ResponseWriter, any) error { + return func(ctx context.Context, w http.ResponseWriter, v any) error { + res := v.(*apiviews.CounterInfo) + enc := encoder(ctx, w) + body := NewCounterGetResponseBody(res.Projected) + w.WriteHeader(http.StatusOK) + return enc.Encode(body) + } +} + +// EncodeCounterGetError returns an encoder for errors returned by the +// CounterGet api endpoint. +func EncodeCounterGetError(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, formatter func(ctx context.Context, err error) goahttp.Statuser) func(context.Context, http.ResponseWriter, error) error { + encodeError := goahttp.ErrorEncoder(encoder, formatter) + return func(ctx context.Context, w http.ResponseWriter, v error) error { + var en goa.GoaErrorNamer + if !errors.As(v, &en) { + return encodeError(ctx, w, v) + } + switch en.GoaErrorName() { + case "unauthorized": + var res *goa.ServiceError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewCounterGetUnauthorizedResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusUnauthorized) + return enc.Encode(body) + case "forbidden": + var res *goa.ServiceError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewCounterGetForbiddenResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusForbidden) + return enc.Encode(body) + default: + return encodeError(ctx, w, v) + } + } +} + +// EncodeCounterIncrementResponse returns an encoder for responses returned by +// the api CounterIncrement endpoint. +func EncodeCounterIncrementResponse(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder) func(context.Context, http.ResponseWriter, any) error { + return func(ctx context.Context, w http.ResponseWriter, v any) error { + res := v.(*apiviews.CounterInfo) + enc := encoder(ctx, w) + body := NewCounterIncrementResponseBody(res.Projected) + w.WriteHeader(http.StatusAccepted) + return enc.Encode(body) + } +} + +// DecodeCounterIncrementRequest returns a decoder for requests sent to the api +// CounterIncrement endpoint. +func DecodeCounterIncrementRequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp.Decoder) func(*http.Request) (any, error) { + return func(r *http.Request) (any, error) { + var ( + token *string + ) + tokenRaw := r.Header.Get("Authorization") + if tokenRaw != "" { + token = &tokenRaw + } + payload := NewCounterIncrementPayload(token) + if payload.Token != nil { + if strings.Contains(*payload.Token, " ") { + // Remove authorization scheme prefix (e.g. "Bearer") + cred := strings.SplitN(*payload.Token, " ", 2)[1] + payload.Token = &cred + } + } + + return payload, nil + } +} + +// EncodeCounterIncrementError returns an encoder for errors returned by the +// CounterIncrement api endpoint. +func EncodeCounterIncrementError(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, formatter func(ctx context.Context, err error) goahttp.Statuser) func(context.Context, http.ResponseWriter, error) error { + encodeError := goahttp.ErrorEncoder(encoder, formatter) + return func(ctx context.Context, w http.ResponseWriter, v error) error { + var en goa.GoaErrorNamer + if !errors.As(v, &en) { + return encodeError(ctx, w, v) + } + switch en.GoaErrorName() { + case "existing_increment_request": + var res *goa.ServiceError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewCounterIncrementExistingIncrementRequestResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusTooManyRequests) + return enc.Encode(body) + case "unauthorized": + var res *goa.ServiceError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewCounterIncrementUnauthorizedResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusUnauthorized) + return enc.Encode(body) + case "forbidden": + var res *goa.ServiceError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewCounterIncrementForbiddenResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusForbidden) + return enc.Encode(body) + default: + return encodeError(ctx, w, v) + } + } +} diff --git a/app/api/v1/gen/http/api/server/paths.go b/app/api/v1/gen/http/api/server/paths.go new file mode 100644 index 0000000..9bf4fd6 --- /dev/null +++ b/app/api/v1/gen/http/api/server/paths.go @@ -0,0 +1,23 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// HTTP request path constructors for the api service. +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package server + +// AuthTokenAPIPath returns the URL path to the api service AuthToken HTTP endpoint. +func AuthTokenAPIPath() string { + return "/api/v1/auth/token" +} + +// CounterGetAPIPath returns the URL path to the api service CounterGet HTTP endpoint. +func CounterGetAPIPath() string { + return "/api/v1/counter" +} + +// CounterIncrementAPIPath returns the URL path to the api service CounterIncrement HTTP endpoint. +func CounterIncrementAPIPath() string { + return "/api/v1/counter" +} diff --git a/app/api/v1/gen/http/api/server/server.go b/app/api/v1/gen/http/api/server/server.go new file mode 100644 index 0000000..9a4a1e1 --- /dev/null +++ b/app/api/v1/gen/http/api/server/server.go @@ -0,0 +1,272 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// api HTTP server +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package server + +import ( + "context" + "net/http" + "path" + + api "github.com/jace-ys/countup/api/v1/gen/api" + goahttp "goa.design/goa/v3/http" + goa "goa.design/goa/v3/pkg" +) + +// Server lists the api service endpoint HTTP handlers. +type Server struct { + Mounts []*MountPoint + AuthToken http.Handler + CounterGet http.Handler + CounterIncrement http.Handler + GenHTTPOpenapi3JSON http.Handler +} + +// MountPoint holds information about the mounted endpoints. +type MountPoint struct { + // Method is the name of the service method served by the mounted HTTP handler. + Method string + // Verb is the HTTP method used to match requests to the mounted handler. + Verb string + // Pattern is the HTTP request path pattern used to match requests to the + // mounted handler. + Pattern string +} + +// New instantiates HTTP handlers for all the api service endpoints using the +// provided encoder and decoder. The handlers are mounted on the given mux +// using the HTTP verb and path defined in the design. errhandler is called +// whenever a response fails to be encoded. formatter is used to format errors +// returned by the service methods prior to encoding. Both errhandler and +// formatter are optional and can be nil. +func New( + e *api.Endpoints, + mux goahttp.Muxer, + decoder func(*http.Request) goahttp.Decoder, + encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, + errhandler func(context.Context, http.ResponseWriter, error), + formatter func(ctx context.Context, err error) goahttp.Statuser, + fileSystemGenHTTPOpenapi3JSON http.FileSystem, +) *Server { + if fileSystemGenHTTPOpenapi3JSON == nil { + fileSystemGenHTTPOpenapi3JSON = http.Dir(".") + } + fileSystemGenHTTPOpenapi3JSON = appendPrefix(fileSystemGenHTTPOpenapi3JSON, "/gen/http") + return &Server{ + Mounts: []*MountPoint{ + {"AuthToken", "POST", "/api/v1/auth/token"}, + {"CounterGet", "GET", "/api/v1/counter"}, + {"CounterIncrement", "POST", "/api/v1/counter"}, + {"Serve gen/http/openapi3.json", "GET", "/api/v1/openapi.json"}, + }, + AuthToken: NewAuthTokenHandler(e.AuthToken, mux, decoder, encoder, errhandler, formatter), + CounterGet: NewCounterGetHandler(e.CounterGet, mux, decoder, encoder, errhandler, formatter), + CounterIncrement: NewCounterIncrementHandler(e.CounterIncrement, mux, decoder, encoder, errhandler, formatter), + GenHTTPOpenapi3JSON: http.FileServer(fileSystemGenHTTPOpenapi3JSON), + } +} + +// Service returns the name of the service served. +func (s *Server) Service() string { return "api" } + +// Use wraps the server handlers with the given middleware. +func (s *Server) Use(m func(http.Handler) http.Handler) { + s.AuthToken = m(s.AuthToken) + s.CounterGet = m(s.CounterGet) + s.CounterIncrement = m(s.CounterIncrement) +} + +// MethodNames returns the methods served. +func (s *Server) MethodNames() []string { return api.MethodNames[:] } + +// Mount configures the mux to serve the api endpoints. +func Mount(mux goahttp.Muxer, h *Server) { + MountAuthTokenHandler(mux, h.AuthToken) + MountCounterGetHandler(mux, h.CounterGet) + MountCounterIncrementHandler(mux, h.CounterIncrement) + MountGenHTTPOpenapi3JSON(mux, http.StripPrefix("/api/v1", h.GenHTTPOpenapi3JSON)) +} + +// Mount configures the mux to serve the api endpoints. +func (s *Server) Mount(mux goahttp.Muxer) { + Mount(mux, s) +} + +// MountAuthTokenHandler configures the mux to serve the "api" service +// "AuthToken" endpoint. +func MountAuthTokenHandler(mux goahttp.Muxer, h http.Handler) { + f, ok := h.(http.HandlerFunc) + if !ok { + f = func(w http.ResponseWriter, r *http.Request) { + h.ServeHTTP(w, r) + } + } + mux.Handle("POST", "/api/v1/auth/token", f) +} + +// NewAuthTokenHandler creates a HTTP handler which loads the HTTP request and +// calls the "api" service "AuthToken" endpoint. +func NewAuthTokenHandler( + endpoint goa.Endpoint, + mux goahttp.Muxer, + decoder func(*http.Request) goahttp.Decoder, + encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, + errhandler func(context.Context, http.ResponseWriter, error), + formatter func(ctx context.Context, err error) goahttp.Statuser, +) http.Handler { + var ( + decodeRequest = DecodeAuthTokenRequest(mux, decoder) + encodeResponse = EncodeAuthTokenResponse(encoder) + encodeError = EncodeAuthTokenError(encoder, formatter) + ) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), goahttp.AcceptTypeKey, r.Header.Get("Accept")) + ctx = context.WithValue(ctx, goa.MethodKey, "AuthToken") + ctx = context.WithValue(ctx, goa.ServiceKey, "api") + payload, err := decodeRequest(r) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + res, err := endpoint(ctx, payload) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + if err := encodeResponse(ctx, w, res); err != nil { + errhandler(ctx, w, err) + } + }) +} + +// MountCounterGetHandler configures the mux to serve the "api" service +// "CounterGet" endpoint. +func MountCounterGetHandler(mux goahttp.Muxer, h http.Handler) { + f, ok := h.(http.HandlerFunc) + if !ok { + f = func(w http.ResponseWriter, r *http.Request) { + h.ServeHTTP(w, r) + } + } + mux.Handle("GET", "/api/v1/counter", f) +} + +// NewCounterGetHandler creates a HTTP handler which loads the HTTP request and +// calls the "api" service "CounterGet" endpoint. +func NewCounterGetHandler( + endpoint goa.Endpoint, + mux goahttp.Muxer, + decoder func(*http.Request) goahttp.Decoder, + encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, + errhandler func(context.Context, http.ResponseWriter, error), + formatter func(ctx context.Context, err error) goahttp.Statuser, +) http.Handler { + var ( + encodeResponse = EncodeCounterGetResponse(encoder) + encodeError = EncodeCounterGetError(encoder, formatter) + ) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), goahttp.AcceptTypeKey, r.Header.Get("Accept")) + ctx = context.WithValue(ctx, goa.MethodKey, "CounterGet") + ctx = context.WithValue(ctx, goa.ServiceKey, "api") + var err error + res, err := endpoint(ctx, nil) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + if err := encodeResponse(ctx, w, res); err != nil { + errhandler(ctx, w, err) + } + }) +} + +// MountCounterIncrementHandler configures the mux to serve the "api" service +// "CounterIncrement" endpoint. +func MountCounterIncrementHandler(mux goahttp.Muxer, h http.Handler) { + f, ok := h.(http.HandlerFunc) + if !ok { + f = func(w http.ResponseWriter, r *http.Request) { + h.ServeHTTP(w, r) + } + } + mux.Handle("POST", "/api/v1/counter", f) +} + +// NewCounterIncrementHandler creates a HTTP handler which loads the HTTP +// request and calls the "api" service "CounterIncrement" endpoint. +func NewCounterIncrementHandler( + endpoint goa.Endpoint, + mux goahttp.Muxer, + decoder func(*http.Request) goahttp.Decoder, + encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, + errhandler func(context.Context, http.ResponseWriter, error), + formatter func(ctx context.Context, err error) goahttp.Statuser, +) http.Handler { + var ( + decodeRequest = DecodeCounterIncrementRequest(mux, decoder) + encodeResponse = EncodeCounterIncrementResponse(encoder) + encodeError = EncodeCounterIncrementError(encoder, formatter) + ) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), goahttp.AcceptTypeKey, r.Header.Get("Accept")) + ctx = context.WithValue(ctx, goa.MethodKey, "CounterIncrement") + ctx = context.WithValue(ctx, goa.ServiceKey, "api") + payload, err := decodeRequest(r) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + res, err := endpoint(ctx, payload) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + if err := encodeResponse(ctx, w, res); err != nil { + errhandler(ctx, w, err) + } + }) +} + +// appendFS is a custom implementation of fs.FS that appends a specified prefix +// to the file paths before delegating the Open call to the underlying fs.FS. +type appendFS struct { + prefix string + fs http.FileSystem +} + +// Open opens the named file, appending the prefix to the file path before +// passing it to the underlying fs.FS. +func (s appendFS) Open(name string) (http.File, error) { + switch name { + case "/openapi.json": + name = "/openapi3.json" + } + return s.fs.Open(path.Join(s.prefix, name)) +} + +// appendPrefix returns a new fs.FS that appends the specified prefix to file paths +// before delegating to the provided embed.FS. +func appendPrefix(fsys http.FileSystem, prefix string) http.FileSystem { + return appendFS{prefix: prefix, fs: fsys} +} + +// MountGenHTTPOpenapi3JSON configures the mux to serve GET request made to +// "/api/v1/openapi.json". +func MountGenHTTPOpenapi3JSON(mux goahttp.Muxer, h http.Handler) { + mux.Handle("GET", "/api/v1/openapi.json", h.ServeHTTP) +} diff --git a/app/api/v1/gen/http/api/server/types.go b/app/api/v1/gen/http/api/server/types.go new file mode 100644 index 0000000..0948f9e --- /dev/null +++ b/app/api/v1/gen/http/api/server/types.go @@ -0,0 +1,372 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// api HTTP server types +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package server + +import ( + api "github.com/jace-ys/countup/api/v1/gen/api" + apiviews "github.com/jace-ys/countup/api/v1/gen/api/views" + goa "goa.design/goa/v3/pkg" +) + +// AuthTokenRequestBody is the type of the "api" service "AuthToken" endpoint +// HTTP request body. +type AuthTokenRequestBody struct { + Provider *string `form:"provider,omitempty" json:"provider,omitempty" xml:"provider,omitempty"` + AccessToken *string `form:"access_token,omitempty" json:"access_token,omitempty" xml:"access_token,omitempty"` +} + +// AuthTokenResponseBody is the type of the "api" service "AuthToken" endpoint +// HTTP response body. +type AuthTokenResponseBody struct { + Token string `form:"token" json:"token" xml:"token"` +} + +// CounterGetResponseBody is the type of the "api" service "CounterGet" +// endpoint HTTP response body. +type CounterGetResponseBody struct { + Count int32 `form:"count" json:"count" xml:"count"` + LastIncrementBy string `form:"last_increment_by" json:"last_increment_by" xml:"last_increment_by"` + LastIncrementAt string `form:"last_increment_at" json:"last_increment_at" xml:"last_increment_at"` + NextFinalizeAt string `form:"next_finalize_at" json:"next_finalize_at" xml:"next_finalize_at"` +} + +// CounterIncrementResponseBody is the type of the "api" service +// "CounterIncrement" endpoint HTTP response body. +type CounterIncrementResponseBody struct { + Count int32 `form:"count" json:"count" xml:"count"` + LastIncrementBy string `form:"last_increment_by" json:"last_increment_by" xml:"last_increment_by"` + LastIncrementAt string `form:"last_increment_at" json:"last_increment_at" xml:"last_increment_at"` + NextFinalizeAt string `form:"next_finalize_at" json:"next_finalize_at" xml:"next_finalize_at"` +} + +// AuthTokenIncompleteAuthInfoResponseBody is the type of the "api" service +// "AuthToken" endpoint HTTP response body for the "incomplete_auth_info" error. +type AuthTokenIncompleteAuthInfoResponseBody struct { + // Name is the name of this class of errors. + Name string `form:"name" json:"name" xml:"name"` + // ID is a unique identifier for this particular occurrence of the problem. + ID string `form:"id" json:"id" xml:"id"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message string `form:"message" json:"message" xml:"message"` + // Is the error temporary? + Temporary bool `form:"temporary" json:"temporary" xml:"temporary"` + // Is the error a timeout? + Timeout bool `form:"timeout" json:"timeout" xml:"timeout"` + // Is the error a server-side fault? + Fault bool `form:"fault" json:"fault" xml:"fault"` +} + +// AuthTokenUnauthorizedResponseBody is the type of the "api" service +// "AuthToken" endpoint HTTP response body for the "unauthorized" error. +type AuthTokenUnauthorizedResponseBody struct { + // Name is the name of this class of errors. + Name string `form:"name" json:"name" xml:"name"` + // ID is a unique identifier for this particular occurrence of the problem. + ID string `form:"id" json:"id" xml:"id"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message string `form:"message" json:"message" xml:"message"` + // Is the error temporary? + Temporary bool `form:"temporary" json:"temporary" xml:"temporary"` + // Is the error a timeout? + Timeout bool `form:"timeout" json:"timeout" xml:"timeout"` + // Is the error a server-side fault? + Fault bool `form:"fault" json:"fault" xml:"fault"` +} + +// AuthTokenForbiddenResponseBody is the type of the "api" service "AuthToken" +// endpoint HTTP response body for the "forbidden" error. +type AuthTokenForbiddenResponseBody struct { + // Name is the name of this class of errors. + Name string `form:"name" json:"name" xml:"name"` + // ID is a unique identifier for this particular occurrence of the problem. + ID string `form:"id" json:"id" xml:"id"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message string `form:"message" json:"message" xml:"message"` + // Is the error temporary? + Temporary bool `form:"temporary" json:"temporary" xml:"temporary"` + // Is the error a timeout? + Timeout bool `form:"timeout" json:"timeout" xml:"timeout"` + // Is the error a server-side fault? + Fault bool `form:"fault" json:"fault" xml:"fault"` +} + +// CounterGetUnauthorizedResponseBody is the type of the "api" service +// "CounterGet" endpoint HTTP response body for the "unauthorized" error. +type CounterGetUnauthorizedResponseBody struct { + // Name is the name of this class of errors. + Name string `form:"name" json:"name" xml:"name"` + // ID is a unique identifier for this particular occurrence of the problem. + ID string `form:"id" json:"id" xml:"id"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message string `form:"message" json:"message" xml:"message"` + // Is the error temporary? + Temporary bool `form:"temporary" json:"temporary" xml:"temporary"` + // Is the error a timeout? + Timeout bool `form:"timeout" json:"timeout" xml:"timeout"` + // Is the error a server-side fault? + Fault bool `form:"fault" json:"fault" xml:"fault"` +} + +// CounterGetForbiddenResponseBody is the type of the "api" service +// "CounterGet" endpoint HTTP response body for the "forbidden" error. +type CounterGetForbiddenResponseBody struct { + // Name is the name of this class of errors. + Name string `form:"name" json:"name" xml:"name"` + // ID is a unique identifier for this particular occurrence of the problem. + ID string `form:"id" json:"id" xml:"id"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message string `form:"message" json:"message" xml:"message"` + // Is the error temporary? + Temporary bool `form:"temporary" json:"temporary" xml:"temporary"` + // Is the error a timeout? + Timeout bool `form:"timeout" json:"timeout" xml:"timeout"` + // Is the error a server-side fault? + Fault bool `form:"fault" json:"fault" xml:"fault"` +} + +// CounterIncrementExistingIncrementRequestResponseBody is the type of the +// "api" service "CounterIncrement" endpoint HTTP response body for the +// "existing_increment_request" error. +type CounterIncrementExistingIncrementRequestResponseBody struct { + // Name is the name of this class of errors. + Name string `form:"name" json:"name" xml:"name"` + // ID is a unique identifier for this particular occurrence of the problem. + ID string `form:"id" json:"id" xml:"id"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message string `form:"message" json:"message" xml:"message"` + // Is the error temporary? + Temporary bool `form:"temporary" json:"temporary" xml:"temporary"` + // Is the error a timeout? + Timeout bool `form:"timeout" json:"timeout" xml:"timeout"` + // Is the error a server-side fault? + Fault bool `form:"fault" json:"fault" xml:"fault"` +} + +// CounterIncrementUnauthorizedResponseBody is the type of the "api" service +// "CounterIncrement" endpoint HTTP response body for the "unauthorized" error. +type CounterIncrementUnauthorizedResponseBody struct { + // Name is the name of this class of errors. + Name string `form:"name" json:"name" xml:"name"` + // ID is a unique identifier for this particular occurrence of the problem. + ID string `form:"id" json:"id" xml:"id"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message string `form:"message" json:"message" xml:"message"` + // Is the error temporary? + Temporary bool `form:"temporary" json:"temporary" xml:"temporary"` + // Is the error a timeout? + Timeout bool `form:"timeout" json:"timeout" xml:"timeout"` + // Is the error a server-side fault? + Fault bool `form:"fault" json:"fault" xml:"fault"` +} + +// CounterIncrementForbiddenResponseBody is the type of the "api" service +// "CounterIncrement" endpoint HTTP response body for the "forbidden" error. +type CounterIncrementForbiddenResponseBody struct { + // Name is the name of this class of errors. + Name string `form:"name" json:"name" xml:"name"` + // ID is a unique identifier for this particular occurrence of the problem. + ID string `form:"id" json:"id" xml:"id"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message string `form:"message" json:"message" xml:"message"` + // Is the error temporary? + Temporary bool `form:"temporary" json:"temporary" xml:"temporary"` + // Is the error a timeout? + Timeout bool `form:"timeout" json:"timeout" xml:"timeout"` + // Is the error a server-side fault? + Fault bool `form:"fault" json:"fault" xml:"fault"` +} + +// NewAuthTokenResponseBody builds the HTTP response body from the result of +// the "AuthToken" endpoint of the "api" service. +func NewAuthTokenResponseBody(res *api.AuthTokenResult) *AuthTokenResponseBody { + body := &AuthTokenResponseBody{ + Token: res.Token, + } + return body +} + +// NewCounterGetResponseBody builds the HTTP response body from the result of +// the "CounterGet" endpoint of the "api" service. +func NewCounterGetResponseBody(res *apiviews.CounterInfoView) *CounterGetResponseBody { + body := &CounterGetResponseBody{ + Count: *res.Count, + LastIncrementBy: *res.LastIncrementBy, + LastIncrementAt: *res.LastIncrementAt, + NextFinalizeAt: *res.NextFinalizeAt, + } + return body +} + +// NewCounterIncrementResponseBody builds the HTTP response body from the +// result of the "CounterIncrement" endpoint of the "api" service. +func NewCounterIncrementResponseBody(res *apiviews.CounterInfoView) *CounterIncrementResponseBody { + body := &CounterIncrementResponseBody{ + Count: *res.Count, + LastIncrementBy: *res.LastIncrementBy, + LastIncrementAt: *res.LastIncrementAt, + NextFinalizeAt: *res.NextFinalizeAt, + } + return body +} + +// NewAuthTokenIncompleteAuthInfoResponseBody builds the HTTP response body +// from the result of the "AuthToken" endpoint of the "api" service. +func NewAuthTokenIncompleteAuthInfoResponseBody(res *goa.ServiceError) *AuthTokenIncompleteAuthInfoResponseBody { + body := &AuthTokenIncompleteAuthInfoResponseBody{ + Name: res.Name, + ID: res.ID, + Message: res.Message, + Temporary: res.Temporary, + Timeout: res.Timeout, + Fault: res.Fault, + } + return body +} + +// NewAuthTokenUnauthorizedResponseBody builds the HTTP response body from the +// result of the "AuthToken" endpoint of the "api" service. +func NewAuthTokenUnauthorizedResponseBody(res *goa.ServiceError) *AuthTokenUnauthorizedResponseBody { + body := &AuthTokenUnauthorizedResponseBody{ + Name: res.Name, + ID: res.ID, + Message: res.Message, + Temporary: res.Temporary, + Timeout: res.Timeout, + Fault: res.Fault, + } + return body +} + +// NewAuthTokenForbiddenResponseBody builds the HTTP response body from the +// result of the "AuthToken" endpoint of the "api" service. +func NewAuthTokenForbiddenResponseBody(res *goa.ServiceError) *AuthTokenForbiddenResponseBody { + body := &AuthTokenForbiddenResponseBody{ + Name: res.Name, + ID: res.ID, + Message: res.Message, + Temporary: res.Temporary, + Timeout: res.Timeout, + Fault: res.Fault, + } + return body +} + +// NewCounterGetUnauthorizedResponseBody builds the HTTP response body from the +// result of the "CounterGet" endpoint of the "api" service. +func NewCounterGetUnauthorizedResponseBody(res *goa.ServiceError) *CounterGetUnauthorizedResponseBody { + body := &CounterGetUnauthorizedResponseBody{ + Name: res.Name, + ID: res.ID, + Message: res.Message, + Temporary: res.Temporary, + Timeout: res.Timeout, + Fault: res.Fault, + } + return body +} + +// NewCounterGetForbiddenResponseBody builds the HTTP response body from the +// result of the "CounterGet" endpoint of the "api" service. +func NewCounterGetForbiddenResponseBody(res *goa.ServiceError) *CounterGetForbiddenResponseBody { + body := &CounterGetForbiddenResponseBody{ + Name: res.Name, + ID: res.ID, + Message: res.Message, + Temporary: res.Temporary, + Timeout: res.Timeout, + Fault: res.Fault, + } + return body +} + +// NewCounterIncrementExistingIncrementRequestResponseBody builds the HTTP +// response body from the result of the "CounterIncrement" endpoint of the +// "api" service. +func NewCounterIncrementExistingIncrementRequestResponseBody(res *goa.ServiceError) *CounterIncrementExistingIncrementRequestResponseBody { + body := &CounterIncrementExistingIncrementRequestResponseBody{ + Name: res.Name, + ID: res.ID, + Message: res.Message, + Temporary: res.Temporary, + Timeout: res.Timeout, + Fault: res.Fault, + } + return body +} + +// NewCounterIncrementUnauthorizedResponseBody builds the HTTP response body +// from the result of the "CounterIncrement" endpoint of the "api" service. +func NewCounterIncrementUnauthorizedResponseBody(res *goa.ServiceError) *CounterIncrementUnauthorizedResponseBody { + body := &CounterIncrementUnauthorizedResponseBody{ + Name: res.Name, + ID: res.ID, + Message: res.Message, + Temporary: res.Temporary, + Timeout: res.Timeout, + Fault: res.Fault, + } + return body +} + +// NewCounterIncrementForbiddenResponseBody builds the HTTP response body from +// the result of the "CounterIncrement" endpoint of the "api" service. +func NewCounterIncrementForbiddenResponseBody(res *goa.ServiceError) *CounterIncrementForbiddenResponseBody { + body := &CounterIncrementForbiddenResponseBody{ + Name: res.Name, + ID: res.ID, + Message: res.Message, + Temporary: res.Temporary, + Timeout: res.Timeout, + Fault: res.Fault, + } + return body +} + +// NewAuthTokenPayload builds a api service AuthToken endpoint payload. +func NewAuthTokenPayload(body *AuthTokenRequestBody) *api.AuthTokenPayload { + v := &api.AuthTokenPayload{ + Provider: *body.Provider, + AccessToken: *body.AccessToken, + } + + return v +} + +// NewCounterIncrementPayload builds a api service CounterIncrement endpoint +// payload. +func NewCounterIncrementPayload(token *string) *api.CounterIncrementPayload { + v := &api.CounterIncrementPayload{} + v.Token = token + + return v +} + +// ValidateAuthTokenRequestBody runs the validations defined on +// AuthTokenRequestBody +func ValidateAuthTokenRequestBody(body *AuthTokenRequestBody) (err error) { + if body.Provider == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("provider", "body")) + } + if body.AccessToken == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("access_token", "body")) + } + if body.Provider != nil { + if !(*body.Provider == "google") { + err = goa.MergeErrors(err, goa.InvalidEnumValueError("body.provider", *body.Provider, []any{"google"})) + } + } + return +} diff --git a/app/api/v1/gen/http/cli/countup/cli.go b/app/api/v1/gen/http/cli/countup/cli.go new file mode 100644 index 0000000..1e49cff --- /dev/null +++ b/app/api/v1/gen/http/cli/countup/cli.go @@ -0,0 +1,408 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// countup HTTP client CLI support package +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package cli + +import ( + "flag" + "fmt" + "net/http" + "os" + + apic "github.com/jace-ys/countup/api/v1/gen/http/api/client" + teapotc "github.com/jace-ys/countup/api/v1/gen/http/teapot/client" + webc "github.com/jace-ys/countup/api/v1/gen/http/web/client" + goahttp "goa.design/goa/v3/http" + goa "goa.design/goa/v3/pkg" +) + +// UsageCommands returns the set of commands and sub-commands using the format +// +// command (subcommand1|subcommand2|...) +func UsageCommands() string { + return `api (auth-token|counter-get|counter-increment) +web (index|another|login-google|login-google-callback|logout|session-token) +teapot echo +` +} + +// UsageExamples produces an example of a valid invocation of the CLI tool. +func UsageExamples() string { + return os.Args[0] + ` api auth-token --body '{ + "access_token": "Itaque unde qui ut molestiae et omnis.", + "provider": "google" + }'` + "\n" + + os.Args[0] + ` web index` + "\n" + + os.Args[0] + ` teapot echo --body '{ + "text": "Eos itaque." + }'` + "\n" + + "" +} + +// ParseEndpoint returns the endpoint and payload as specified on the command +// line. +func ParseEndpoint( + scheme, host string, + doer goahttp.Doer, + enc func(*http.Request) goahttp.Encoder, + dec func(*http.Response) goahttp.Decoder, + restore bool, +) (goa.Endpoint, any, error) { + var ( + apiFlags = flag.NewFlagSet("api", flag.ContinueOnError) + + apiAuthTokenFlags = flag.NewFlagSet("auth-token", flag.ExitOnError) + apiAuthTokenBodyFlag = apiAuthTokenFlags.String("body", "REQUIRED", "") + + apiCounterGetFlags = flag.NewFlagSet("counter-get", flag.ExitOnError) + + apiCounterIncrementFlags = flag.NewFlagSet("counter-increment", flag.ExitOnError) + apiCounterIncrementTokenFlag = apiCounterIncrementFlags.String("token", "", "") + + webFlags = flag.NewFlagSet("web", flag.ContinueOnError) + + webIndexFlags = flag.NewFlagSet("index", flag.ExitOnError) + + webAnotherFlags = flag.NewFlagSet("another", flag.ExitOnError) + + webLoginGoogleFlags = flag.NewFlagSet("login-google", flag.ExitOnError) + + webLoginGoogleCallbackFlags = flag.NewFlagSet("login-google-callback", flag.ExitOnError) + webLoginGoogleCallbackCodeFlag = webLoginGoogleCallbackFlags.String("code", "REQUIRED", "") + webLoginGoogleCallbackStateFlag = webLoginGoogleCallbackFlags.String("state", "REQUIRED", "") + webLoginGoogleCallbackSessionCookieFlag = webLoginGoogleCallbackFlags.String("session-cookie", "REQUIRED", "") + + webLogoutFlags = flag.NewFlagSet("logout", flag.ExitOnError) + webLogoutSessionCookieFlag = webLogoutFlags.String("session-cookie", "REQUIRED", "") + + webSessionTokenFlags = flag.NewFlagSet("session-token", flag.ExitOnError) + webSessionTokenSessionCookieFlag = webSessionTokenFlags.String("session-cookie", "REQUIRED", "") + + teapotFlags = flag.NewFlagSet("teapot", flag.ContinueOnError) + + teapotEchoFlags = flag.NewFlagSet("echo", flag.ExitOnError) + teapotEchoBodyFlag = teapotEchoFlags.String("body", "REQUIRED", "") + ) + apiFlags.Usage = apiUsage + apiAuthTokenFlags.Usage = apiAuthTokenUsage + apiCounterGetFlags.Usage = apiCounterGetUsage + apiCounterIncrementFlags.Usage = apiCounterIncrementUsage + + webFlags.Usage = webUsage + webIndexFlags.Usage = webIndexUsage + webAnotherFlags.Usage = webAnotherUsage + webLoginGoogleFlags.Usage = webLoginGoogleUsage + webLoginGoogleCallbackFlags.Usage = webLoginGoogleCallbackUsage + webLogoutFlags.Usage = webLogoutUsage + webSessionTokenFlags.Usage = webSessionTokenUsage + + teapotFlags.Usage = teapotUsage + teapotEchoFlags.Usage = teapotEchoUsage + + if err := flag.CommandLine.Parse(os.Args[1:]); err != nil { + return nil, nil, err + } + + if flag.NArg() < 2 { // two non flag args are required: SERVICE and ENDPOINT (aka COMMAND) + return nil, nil, fmt.Errorf("not enough arguments") + } + + var ( + svcn string + svcf *flag.FlagSet + ) + { + svcn = flag.Arg(0) + switch svcn { + case "api": + svcf = apiFlags + case "web": + svcf = webFlags + case "teapot": + svcf = teapotFlags + default: + return nil, nil, fmt.Errorf("unknown service %q", svcn) + } + } + if err := svcf.Parse(flag.Args()[1:]); err != nil { + return nil, nil, err + } + + var ( + epn string + epf *flag.FlagSet + ) + { + epn = svcf.Arg(0) + switch svcn { + case "api": + switch epn { + case "auth-token": + epf = apiAuthTokenFlags + + case "counter-get": + epf = apiCounterGetFlags + + case "counter-increment": + epf = apiCounterIncrementFlags + + } + + case "web": + switch epn { + case "index": + epf = webIndexFlags + + case "another": + epf = webAnotherFlags + + case "login-google": + epf = webLoginGoogleFlags + + case "login-google-callback": + epf = webLoginGoogleCallbackFlags + + case "logout": + epf = webLogoutFlags + + case "session-token": + epf = webSessionTokenFlags + + } + + case "teapot": + switch epn { + case "echo": + epf = teapotEchoFlags + + } + + } + } + if epf == nil { + return nil, nil, fmt.Errorf("unknown %q endpoint %q", svcn, epn) + } + + // Parse endpoint flags if any + if svcf.NArg() > 1 { + if err := epf.Parse(svcf.Args()[1:]); err != nil { + return nil, nil, err + } + } + + var ( + data any + endpoint goa.Endpoint + err error + ) + { + switch svcn { + case "api": + c := apic.NewClient(scheme, host, doer, enc, dec, restore) + switch epn { + case "auth-token": + endpoint = c.AuthToken() + data, err = apic.BuildAuthTokenPayload(*apiAuthTokenBodyFlag) + case "counter-get": + endpoint = c.CounterGet() + case "counter-increment": + endpoint = c.CounterIncrement() + data, err = apic.BuildCounterIncrementPayload(*apiCounterIncrementTokenFlag) + } + case "web": + c := webc.NewClient(scheme, host, doer, enc, dec, restore) + switch epn { + case "index": + endpoint = c.Index() + case "another": + endpoint = c.Another() + case "login-google": + endpoint = c.LoginGoogle() + case "login-google-callback": + endpoint = c.LoginGoogleCallback() + data, err = webc.BuildLoginGoogleCallbackPayload(*webLoginGoogleCallbackCodeFlag, *webLoginGoogleCallbackStateFlag, *webLoginGoogleCallbackSessionCookieFlag) + case "logout": + endpoint = c.Logout() + data, err = webc.BuildLogoutPayload(*webLogoutSessionCookieFlag) + case "session-token": + endpoint = c.SessionToken() + data, err = webc.BuildSessionTokenPayload(*webSessionTokenSessionCookieFlag) + } + case "teapot": + c := teapotc.NewClient(scheme, host, doer, enc, dec, restore) + switch epn { + case "echo": + endpoint = c.Echo() + data, err = teapotc.BuildEchoPayload(*teapotEchoBodyFlag) + } + } + } + if err != nil { + return nil, nil, err + } + + return endpoint, data, nil +} + +// apiUsage displays the usage of the api command and its subcommands. +func apiUsage() { + fmt.Fprintf(os.Stderr, `Service is the api service interface. +Usage: + %[1]s [globalflags] api COMMAND [flags] + +COMMAND: + auth-token: AuthToken implements AuthToken. + counter-get: CounterGet implements CounterGet. + counter-increment: CounterIncrement implements CounterIncrement. + +Additional help: + %[1]s api COMMAND --help +`, os.Args[0]) +} +func apiAuthTokenUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] api auth-token -body JSON + +AuthToken implements AuthToken. + -body JSON: + +Example: + %[1]s api auth-token --body '{ + "access_token": "Itaque unde qui ut molestiae et omnis.", + "provider": "google" + }' +`, os.Args[0]) +} + +func apiCounterGetUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] api counter-get + +CounterGet implements CounterGet. + +Example: + %[1]s api counter-get +`, os.Args[0]) +} + +func apiCounterIncrementUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] api counter-increment -token STRING + +CounterIncrement implements CounterIncrement. + -token STRING: + +Example: + %[1]s api counter-increment --token "Sapiente beatae ullam itaque fugit qui." +`, os.Args[0]) +} + +// webUsage displays the usage of the web command and its subcommands. +func webUsage() { + fmt.Fprintf(os.Stderr, `Service is the web service interface. +Usage: + %[1]s [globalflags] web COMMAND [flags] + +COMMAND: + index: Index implements Index. + another: Another implements Another. + login-google: LoginGoogle implements LoginGoogle. + login-google-callback: LoginGoogleCallback implements LoginGoogleCallback. + logout: Logout implements Logout. + session-token: SessionToken implements SessionToken. + +Additional help: + %[1]s web COMMAND --help +`, os.Args[0]) +} +func webIndexUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] web index + +Index implements Index. + +Example: + %[1]s web index +`, os.Args[0]) +} + +func webAnotherUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] web another + +Another implements Another. + +Example: + %[1]s web another +`, os.Args[0]) +} + +func webLoginGoogleUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] web login-google + +LoginGoogle implements LoginGoogle. + +Example: + %[1]s web login-google +`, os.Args[0]) +} + +func webLoginGoogleCallbackUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] web login-google-callback -code STRING -state STRING -session-cookie STRING + +LoginGoogleCallback implements LoginGoogleCallback. + -code STRING: + -state STRING: + -session-cookie STRING: + +Example: + %[1]s web login-google-callback --code "Blanditiis non consectetur." --state "Laboriosam quasi." --session-cookie "Natus minima non perspiciatis eum dicta." +`, os.Args[0]) +} + +func webLogoutUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] web logout -session-cookie STRING + +Logout implements Logout. + -session-cookie STRING: + +Example: + %[1]s web logout --session-cookie "Eum dolor alias sequi dolore." +`, os.Args[0]) +} + +func webSessionTokenUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] web session-token -session-cookie STRING + +SessionToken implements SessionToken. + -session-cookie STRING: + +Example: + %[1]s web session-token --session-cookie "Aliquam quia nemo voluptas." +`, os.Args[0]) +} + +// teapotUsage displays the usage of the teapot command and its subcommands. +func teapotUsage() { + fmt.Fprintf(os.Stderr, `Service is the teapot service interface. +Usage: + %[1]s [globalflags] teapot COMMAND [flags] + +COMMAND: + echo: Echo implements Echo. + +Additional help: + %[1]s teapot COMMAND --help +`, os.Args[0]) +} +func teapotEchoUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] teapot echo -body JSON + +Echo implements Echo. + -body JSON: + +Example: + %[1]s teapot echo --body '{ + "text": "Eos itaque." + }' +`, os.Args[0]) +} diff --git a/app/api/v1/gen/http/openapi.json b/app/api/v1/gen/http/openapi.json new file mode 100644 index 0000000..5f7df50 --- /dev/null +++ b/app/api/v1/gen/http/openapi.json @@ -0,0 +1 @@ +{"swagger":"2.0","info":{"title":"Count Up","description":"A production-ready Go service deployed on Kubernetes","version":"1.0.0"},"host":"localhost:8080","consumes":["application/json","application/xml","application/gob"],"produces":["application/json","application/xml","application/gob"],"paths":{"/":{"get":{"tags":["web"],"summary":"Index web","operationId":"web#Index","produces":["text/html"],"responses":{"200":{"description":"OK response.","schema":{"type":"string","format":"byte"}},"401":{"description":"Unauthorized response.","schema":{"$ref":"#/definitions/WebIndexUnauthorizedResponseBody"}}},"schemes":["http"]}},"/another":{"get":{"tags":["web"],"summary":"Another web","operationId":"web#Another","produces":["text/html"],"responses":{"200":{"description":"OK response.","schema":{"type":"string","format":"byte"}},"401":{"description":"Unauthorized response.","schema":{"$ref":"#/definitions/WebAnotherUnauthorizedResponseBody"}}},"schemes":["http"]}},"/api/v1/auth/token":{"post":{"tags":["api"],"summary":"AuthToken api","operationId":"api#AuthToken","parameters":[{"name":"AuthTokenRequestBody","in":"body","required":true,"schema":{"$ref":"#/definitions/APIAuthTokenRequestBody","required":["provider","access_token"]}}],"responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/APIAuthTokenResponseBody","required":["token"]}},"401":{"description":"Unauthorized response.","schema":{"$ref":"#/definitions/APIAuthTokenUnauthorizedResponseBody"}},"403":{"description":"Forbidden response.","schema":{"$ref":"#/definitions/APIAuthTokenForbiddenResponseBody"}}},"schemes":["http"]}},"/api/v1/counter":{"get":{"tags":["api"],"summary":"CounterGet api","operationId":"api#CounterGet","responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/CounterInfo"}},"401":{"description":"Unauthorized response.","schema":{"$ref":"#/definitions/APICounterGetUnauthorizedResponseBody"}},"403":{"description":"Forbidden response.","schema":{"$ref":"#/definitions/APICounterGetForbiddenResponseBody"}}},"schemes":["http"]},"post":{"tags":["api"],"summary":"CounterIncrement api","description":"\n**Required security scopes for jwt**:\n * `api.user`","operationId":"api#CounterIncrement","parameters":[{"name":"Authorization","in":"header","required":false,"type":"string"}],"responses":{"202":{"description":"Accepted response.","schema":{"$ref":"#/definitions/CounterInfo"}},"401":{"description":"Unauthorized response.","schema":{"$ref":"#/definitions/APICounterIncrementUnauthorizedResponseBody"}},"403":{"description":"Forbidden response.","schema":{"$ref":"#/definitions/APICounterIncrementForbiddenResponseBody"}},"429":{"description":"Too Many Requests response.","schema":{"$ref":"#/definitions/APICounterIncrementExistingIncrementRequestResponseBody"}}},"schemes":["http"],"security":[{"jwt_header_Authorization":[]}]}},"/api/v1/openapi.json":{"get":{"tags":["api"],"summary":"Download gen/http/openapi3.json","operationId":"api#/api/v1/openapi.json","responses":{"200":{"description":"File downloaded","schema":{"type":"file"}}},"schemes":["http"]}},"/echo":{"post":{"tags":["teapot"],"summary":"Echo teapot","operationId":"teapot#Echo","parameters":[{"name":"EchoRequestBody","in":"body","required":true,"schema":{"$ref":"#/definitions/TeapotEchoRequestBody","required":["text"]}}],"responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/TeapotEchoResponseBody","required":["text"]}}},"schemes":["http"]}},"/login/google":{"get":{"tags":["web"],"summary":"LoginGoogle web","operationId":"web#LoginGoogle","responses":{"302":{"description":"Found response.","headers":{"Location":{"type":"string"}}},"401":{"description":"Unauthorized response.","schema":{"$ref":"#/definitions/WebLoginGoogleUnauthorizedResponseBody"}}},"schemes":["http"]}},"/login/google/callback":{"get":{"tags":["web"],"summary":"LoginGoogleCallback web","operationId":"web#LoginGoogleCallback","parameters":[{"name":"code","in":"query","required":true,"type":"string"},{"name":"state","in":"query","required":true,"type":"string"}],"responses":{"302":{"description":"Found response.","headers":{"Location":{"type":"string"}}},"401":{"description":"Unauthorized response.","schema":{"$ref":"#/definitions/WebLoginGoogleCallbackUnauthorizedResponseBody"}}},"schemes":["http"]}},"/logout":{"get":{"tags":["web"],"summary":"Logout web","operationId":"web#Logout","responses":{"302":{"description":"Found response.","headers":{"Location":{"type":"string"}}},"401":{"description":"Unauthorized response.","schema":{"$ref":"#/definitions/WebLogoutUnauthorizedResponseBody"}}},"schemes":["http"]}},"/openapi.json":{"get":{"tags":["teapot"],"summary":"Download gen/http/openapi3.json","operationId":"teapot#/openapi.json","responses":{"200":{"description":"File downloaded","schema":{"type":"file"}}},"schemes":["http"]}},"/session/token":{"get":{"tags":["web"],"summary":"SessionToken web","operationId":"web#SessionToken","produces":["application/json"],"responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/WebSessionTokenResponseBody","required":["token"]}},"401":{"description":"Unauthorized response.","schema":{"$ref":"#/definitions/WebSessionTokenUnauthorizedResponseBody"}}},"schemes":["http"]}},"/static/*":{"get":{"tags":["web"],"summary":"Download static/","operationId":"web#/static/*","responses":{"200":{"description":"File downloaded","schema":{"type":"file"}}},"schemes":["http"]}}},"definitions":{"APIAuthTokenForbiddenResponseBody":{"title":"Mediatype identifier: application/vnd.goa.error; view=default","type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":true},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":true},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":true}},"description":"AuthToken_forbidden_Response_Body result type (default view)","example":{"fault":true,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":false,"timeout":true},"required":["name","id","message","temporary","timeout","fault"]},"APIAuthTokenIncompleteAuthInfoResponseBody":{"title":"Mediatype identifier: application/vnd.goa.error; view=default","type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":false},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":false},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":false}},"description":"AuthToken_incomplete_auth_info_Response_Body result type (default view)","example":{"fault":false,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":true,"timeout":true},"required":["name","id","message","temporary","timeout","fault"]},"APIAuthTokenRequestBody":{"title":"APIAuthTokenRequestBody","type":"object","properties":{"access_token":{"type":"string","example":"Aut culpa quo sit dolor aperiam consequatur."},"provider":{"type":"string","example":"google","enum":["google"]}},"example":{"access_token":"Harum iusto quas.","provider":"google"},"required":["provider","access_token"]},"APIAuthTokenResponseBody":{"title":"APIAuthTokenResponseBody","type":"object","properties":{"token":{"type":"string","example":"Error animi atque nobis sit dolor ut."}},"example":{"token":"Illo vel qui ipsa adipisci."},"required":["token"]},"APIAuthTokenUnauthorizedResponseBody":{"title":"Mediatype identifier: application/vnd.goa.error; view=default","type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":true},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":true},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":true}},"description":"AuthToken_unauthorized_Response_Body result type (default view)","example":{"fault":true,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":true,"timeout":false},"required":["name","id","message","temporary","timeout","fault"]},"APICounterGetForbiddenResponseBody":{"title":"Mediatype identifier: application/vnd.goa.error; view=default","type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":false},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":false},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":true}},"description":"CounterGet_forbidden_Response_Body result type (default view)","example":{"fault":false,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":false,"timeout":true},"required":["name","id","message","temporary","timeout","fault"]},"APICounterGetUnauthorizedResponseBody":{"title":"Mediatype identifier: application/vnd.goa.error; view=default","type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":true},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":true},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":true}},"description":"CounterGet_unauthorized_Response_Body result type (default view)","example":{"fault":true,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":true,"timeout":true},"required":["name","id","message","temporary","timeout","fault"]},"APICounterIncrementExistingIncrementRequestResponseBody":{"title":"Mediatype identifier: application/vnd.goa.error; view=default","type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":true},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":false},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":true}},"description":"CounterIncrement_existing_increment_request_Response_Body result type (default view)","example":{"fault":true,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":true,"timeout":false},"required":["name","id","message","temporary","timeout","fault"]},"APICounterIncrementForbiddenResponseBody":{"title":"Mediatype identifier: application/vnd.goa.error; view=default","type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":false},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":true},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":false}},"description":"CounterIncrement_forbidden_Response_Body result type (default view)","example":{"fault":false,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":true,"timeout":true},"required":["name","id","message","temporary","timeout","fault"]},"APICounterIncrementUnauthorizedResponseBody":{"title":"Mediatype identifier: application/vnd.goa.error; view=default","type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":true},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":false},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":true}},"description":"CounterIncrement_unauthorized_Response_Body result type (default view)","example":{"fault":false,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":false,"timeout":true},"required":["name","id","message","temporary","timeout","fault"]},"CounterInfo":{"title":"Mediatype identifier: application/vnd.countup.counter-info`; view=default","type":"object","properties":{"count":{"type":"integer","example":718974130,"format":"int32"},"last_increment_at":{"type":"string","example":"Consequuntur cupiditate pariatur aut placeat."},"last_increment_by":{"type":"string","example":"Ipsam nesciunt minima cupiditate."},"next_finalize_at":{"type":"string","example":"Amet voluptas ex nostrum aperiam explicabo sed."}},"description":"CounterGetResponseBody result type (default view)","example":{"count":837784502,"last_increment_at":"Eius rerum.","last_increment_by":"Sint asperiores sed voluptas voluptatem.","next_finalize_at":"Accusamus maiores ut voluptas."},"required":["count","last_increment_by","last_increment_at","next_finalize_at"]},"TeapotEchoRequestBody":{"title":"TeapotEchoRequestBody","type":"object","properties":{"text":{"type":"string","example":"Sit non qui quaerat nobis incidunt porro."}},"example":{"text":"Quam ut nobis reiciendis."},"required":["text"]},"TeapotEchoResponseBody":{"title":"TeapotEchoResponseBody","type":"object","properties":{"text":{"type":"string","example":"Aliquid dignissimos non at doloremque."}},"example":{"text":"Et optio hic."},"required":["text"]},"WebAnotherUnauthorizedResponseBody":{"title":"Mediatype identifier: application/vnd.goa.error; view=default","type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":true},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":false},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":true}},"description":"Another_unauthorized_Response_Body result type (default view)","example":{"fault":true,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":true,"timeout":true},"required":["name","id","message","temporary","timeout","fault"]},"WebIndexUnauthorizedResponseBody":{"title":"Mediatype identifier: application/vnd.goa.error; view=default","type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":false},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":false},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":false}},"description":"Index_unauthorized_Response_Body result type (default view)","example":{"fault":true,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":false,"timeout":true},"required":["name","id","message","temporary","timeout","fault"]},"WebLoginGoogleCallbackUnauthorizedResponseBody":{"title":"Mediatype identifier: application/vnd.goa.error; view=default","type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":false},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":true},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":false}},"description":"LoginGoogleCallback_unauthorized_Response_Body result type (default view)","example":{"fault":false,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":false,"timeout":false},"required":["name","id","message","temporary","timeout","fault"]},"WebLoginGoogleUnauthorizedResponseBody":{"title":"Mediatype identifier: application/vnd.goa.error; view=default","type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":false},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":true},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":true}},"description":"LoginGoogle_unauthorized_Response_Body result type (default view)","example":{"fault":false,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":false,"timeout":true},"required":["name","id","message","temporary","timeout","fault"]},"WebLogoutUnauthorizedResponseBody":{"title":"Mediatype identifier: application/vnd.goa.error; view=default","type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":false},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":true},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":false}},"description":"Logout_unauthorized_Response_Body result type (default view)","example":{"fault":true,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":false,"timeout":true},"required":["name","id","message","temporary","timeout","fault"]},"WebSessionTokenResponseBody":{"title":"WebSessionTokenResponseBody","type":"object","properties":{"token":{"type":"string","example":"Ut aut possimus et."}},"example":{"token":"Magni harum atque dolor quod at consectetur."},"required":["token"]},"WebSessionTokenUnauthorizedResponseBody":{"title":"Mediatype identifier: application/vnd.goa.error; view=default","type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":false},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":true},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":false}},"description":"SessionToken_unauthorized_Response_Body result type (default view)","example":{"fault":false,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":true,"timeout":false},"required":["name","id","message","temporary","timeout","fault"]}},"securityDefinitions":{"jwt_header_Authorization":{"type":"apiKey","description":"\n**Security Scopes**:\n * `api.user`: no description","name":"Authorization","in":"header"}}} \ No newline at end of file diff --git a/app/api/v1/gen/http/openapi.yaml b/app/api/v1/gen/http/openapi.yaml new file mode 100644 index 0000000..f5bc45f --- /dev/null +++ b/app/api/v1/gen/http/openapi.yaml @@ -0,0 +1,989 @@ +swagger: "2.0" +info: + title: Count Up + description: A production-ready Go service deployed on Kubernetes + version: 1.0.0 +host: localhost:8080 +consumes: + - application/json + - application/xml + - application/gob +produces: + - application/json + - application/xml + - application/gob +paths: + /: + get: + tags: + - web + summary: Index web + operationId: web#Index + produces: + - text/html + responses: + "200": + description: OK response. + schema: + type: string + format: byte + "401": + description: Unauthorized response. + schema: + $ref: '#/definitions/WebIndexUnauthorizedResponseBody' + schemes: + - http + /another: + get: + tags: + - web + summary: Another web + operationId: web#Another + produces: + - text/html + responses: + "200": + description: OK response. + schema: + type: string + format: byte + "401": + description: Unauthorized response. + schema: + $ref: '#/definitions/WebAnotherUnauthorizedResponseBody' + schemes: + - http + /api/v1/auth/token: + post: + tags: + - api + summary: AuthToken api + operationId: api#AuthToken + parameters: + - name: AuthTokenRequestBody + in: body + required: true + schema: + $ref: '#/definitions/APIAuthTokenRequestBody' + required: + - provider + - access_token + responses: + "200": + description: OK response. + schema: + $ref: '#/definitions/APIAuthTokenResponseBody' + required: + - token + "401": + description: Unauthorized response. + schema: + $ref: '#/definitions/APIAuthTokenUnauthorizedResponseBody' + "403": + description: Forbidden response. + schema: + $ref: '#/definitions/APIAuthTokenForbiddenResponseBody' + schemes: + - http + /api/v1/counter: + get: + tags: + - api + summary: CounterGet api + operationId: api#CounterGet + responses: + "200": + description: OK response. + schema: + $ref: '#/definitions/CounterInfo' + "401": + description: Unauthorized response. + schema: + $ref: '#/definitions/APICounterGetUnauthorizedResponseBody' + "403": + description: Forbidden response. + schema: + $ref: '#/definitions/APICounterGetForbiddenResponseBody' + schemes: + - http + post: + tags: + - api + summary: CounterIncrement api + description: |4- + **Required security scopes for jwt**: + * `api.user` + operationId: api#CounterIncrement + parameters: + - name: Authorization + in: header + required: false + type: string + responses: + "202": + description: Accepted response. + schema: + $ref: '#/definitions/CounterInfo' + "401": + description: Unauthorized response. + schema: + $ref: '#/definitions/APICounterIncrementUnauthorizedResponseBody' + "403": + description: Forbidden response. + schema: + $ref: '#/definitions/APICounterIncrementForbiddenResponseBody' + "429": + description: Too Many Requests response. + schema: + $ref: '#/definitions/APICounterIncrementExistingIncrementRequestResponseBody' + schemes: + - http + security: + - jwt_header_Authorization: [] + /api/v1/openapi.json: + get: + tags: + - api + summary: Download gen/http/openapi3.json + operationId: api#/api/v1/openapi.json + responses: + "200": + description: File downloaded + schema: + type: file + schemes: + - http + /echo: + post: + tags: + - teapot + summary: Echo teapot + operationId: teapot#Echo + parameters: + - name: EchoRequestBody + in: body + required: true + schema: + $ref: '#/definitions/TeapotEchoRequestBody' + required: + - text + responses: + "200": + description: OK response. + schema: + $ref: '#/definitions/TeapotEchoResponseBody' + required: + - text + schemes: + - http + /login/google: + get: + tags: + - web + summary: LoginGoogle web + operationId: web#LoginGoogle + responses: + "302": + description: Found response. + headers: + Location: + type: string + "401": + description: Unauthorized response. + schema: + $ref: '#/definitions/WebLoginGoogleUnauthorizedResponseBody' + schemes: + - http + /login/google/callback: + get: + tags: + - web + summary: LoginGoogleCallback web + operationId: web#LoginGoogleCallback + parameters: + - name: code + in: query + required: true + type: string + - name: state + in: query + required: true + type: string + responses: + "302": + description: Found response. + headers: + Location: + type: string + "401": + description: Unauthorized response. + schema: + $ref: '#/definitions/WebLoginGoogleCallbackUnauthorizedResponseBody' + schemes: + - http + /logout: + get: + tags: + - web + summary: Logout web + operationId: web#Logout + responses: + "302": + description: Found response. + headers: + Location: + type: string + "401": + description: Unauthorized response. + schema: + $ref: '#/definitions/WebLogoutUnauthorizedResponseBody' + schemes: + - http + /openapi.json: + get: + tags: + - teapot + summary: Download gen/http/openapi3.json + operationId: teapot#/openapi.json + responses: + "200": + description: File downloaded + schema: + type: file + schemes: + - http + /session/token: + get: + tags: + - web + summary: SessionToken web + operationId: web#SessionToken + produces: + - application/json + responses: + "200": + description: OK response. + schema: + $ref: '#/definitions/WebSessionTokenResponseBody' + required: + - token + "401": + description: Unauthorized response. + schema: + $ref: '#/definitions/WebSessionTokenUnauthorizedResponseBody' + schemes: + - http + /static/*: + get: + tags: + - web + summary: Download static/ + operationId: web#/static/* + responses: + "200": + description: File downloaded + schema: + type: file + schemes: + - http +definitions: + APIAuthTokenForbiddenResponseBody: + title: 'Mediatype identifier: application/vnd.goa.error; view=default' + type: object + properties: + fault: + type: boolean + description: Is the error a server-side fault? + example: true + id: + type: string + description: ID is a unique identifier for this particular occurrence of the problem. + example: 123abc + message: + type: string + description: Message is a human-readable explanation specific to this occurrence of the problem. + example: parameter 'p' must be an integer + name: + type: string + description: Name is the name of this class of errors. + example: bad_request + temporary: + type: boolean + description: Is the error temporary? + example: true + timeout: + type: boolean + description: Is the error a timeout? + example: true + description: AuthToken_forbidden_Response_Body result type (default view) + example: + fault: true + id: 123abc + message: parameter 'p' must be an integer + name: bad_request + temporary: false + timeout: true + required: + - name + - id + - message + - temporary + - timeout + - fault + APIAuthTokenIncompleteAuthInfoResponseBody: + title: 'Mediatype identifier: application/vnd.goa.error; view=default' + type: object + properties: + fault: + type: boolean + description: Is the error a server-side fault? + example: false + id: + type: string + description: ID is a unique identifier for this particular occurrence of the problem. + example: 123abc + message: + type: string + description: Message is a human-readable explanation specific to this occurrence of the problem. + example: parameter 'p' must be an integer + name: + type: string + description: Name is the name of this class of errors. + example: bad_request + temporary: + type: boolean + description: Is the error temporary? + example: false + timeout: + type: boolean + description: Is the error a timeout? + example: false + description: AuthToken_incomplete_auth_info_Response_Body result type (default view) + example: + fault: false + id: 123abc + message: parameter 'p' must be an integer + name: bad_request + temporary: true + timeout: true + required: + - name + - id + - message + - temporary + - timeout + - fault + APIAuthTokenRequestBody: + title: APIAuthTokenRequestBody + type: object + properties: + access_token: + type: string + example: Aut culpa quo sit dolor aperiam consequatur. + provider: + type: string + example: google + enum: + - google + example: + access_token: Harum iusto quas. + provider: google + required: + - provider + - access_token + APIAuthTokenResponseBody: + title: APIAuthTokenResponseBody + type: object + properties: + token: + type: string + example: Error animi atque nobis sit dolor ut. + example: + token: Illo vel qui ipsa adipisci. + required: + - token + APIAuthTokenUnauthorizedResponseBody: + title: 'Mediatype identifier: application/vnd.goa.error; view=default' + type: object + properties: + fault: + type: boolean + description: Is the error a server-side fault? + example: true + id: + type: string + description: ID is a unique identifier for this particular occurrence of the problem. + example: 123abc + message: + type: string + description: Message is a human-readable explanation specific to this occurrence of the problem. + example: parameter 'p' must be an integer + name: + type: string + description: Name is the name of this class of errors. + example: bad_request + temporary: + type: boolean + description: Is the error temporary? + example: true + timeout: + type: boolean + description: Is the error a timeout? + example: true + description: AuthToken_unauthorized_Response_Body result type (default view) + example: + fault: true + id: 123abc + message: parameter 'p' must be an integer + name: bad_request + temporary: true + timeout: false + required: + - name + - id + - message + - temporary + - timeout + - fault + APICounterGetForbiddenResponseBody: + title: 'Mediatype identifier: application/vnd.goa.error; view=default' + type: object + properties: + fault: + type: boolean + description: Is the error a server-side fault? + example: false + id: + type: string + description: ID is a unique identifier for this particular occurrence of the problem. + example: 123abc + message: + type: string + description: Message is a human-readable explanation specific to this occurrence of the problem. + example: parameter 'p' must be an integer + name: + type: string + description: Name is the name of this class of errors. + example: bad_request + temporary: + type: boolean + description: Is the error temporary? + example: false + timeout: + type: boolean + description: Is the error a timeout? + example: true + description: CounterGet_forbidden_Response_Body result type (default view) + example: + fault: false + id: 123abc + message: parameter 'p' must be an integer + name: bad_request + temporary: false + timeout: true + required: + - name + - id + - message + - temporary + - timeout + - fault + APICounterGetUnauthorizedResponseBody: + title: 'Mediatype identifier: application/vnd.goa.error; view=default' + type: object + properties: + fault: + type: boolean + description: Is the error a server-side fault? + example: true + id: + type: string + description: ID is a unique identifier for this particular occurrence of the problem. + example: 123abc + message: + type: string + description: Message is a human-readable explanation specific to this occurrence of the problem. + example: parameter 'p' must be an integer + name: + type: string + description: Name is the name of this class of errors. + example: bad_request + temporary: + type: boolean + description: Is the error temporary? + example: true + timeout: + type: boolean + description: Is the error a timeout? + example: true + description: CounterGet_unauthorized_Response_Body result type (default view) + example: + fault: true + id: 123abc + message: parameter 'p' must be an integer + name: bad_request + temporary: true + timeout: true + required: + - name + - id + - message + - temporary + - timeout + - fault + APICounterIncrementExistingIncrementRequestResponseBody: + title: 'Mediatype identifier: application/vnd.goa.error; view=default' + type: object + properties: + fault: + type: boolean + description: Is the error a server-side fault? + example: true + id: + type: string + description: ID is a unique identifier for this particular occurrence of the problem. + example: 123abc + message: + type: string + description: Message is a human-readable explanation specific to this occurrence of the problem. + example: parameter 'p' must be an integer + name: + type: string + description: Name is the name of this class of errors. + example: bad_request + temporary: + type: boolean + description: Is the error temporary? + example: false + timeout: + type: boolean + description: Is the error a timeout? + example: true + description: CounterIncrement_existing_increment_request_Response_Body result type (default view) + example: + fault: true + id: 123abc + message: parameter 'p' must be an integer + name: bad_request + temporary: true + timeout: false + required: + - name + - id + - message + - temporary + - timeout + - fault + APICounterIncrementForbiddenResponseBody: + title: 'Mediatype identifier: application/vnd.goa.error; view=default' + type: object + properties: + fault: + type: boolean + description: Is the error a server-side fault? + example: false + id: + type: string + description: ID is a unique identifier for this particular occurrence of the problem. + example: 123abc + message: + type: string + description: Message is a human-readable explanation specific to this occurrence of the problem. + example: parameter 'p' must be an integer + name: + type: string + description: Name is the name of this class of errors. + example: bad_request + temporary: + type: boolean + description: Is the error temporary? + example: true + timeout: + type: boolean + description: Is the error a timeout? + example: false + description: CounterIncrement_forbidden_Response_Body result type (default view) + example: + fault: false + id: 123abc + message: parameter 'p' must be an integer + name: bad_request + temporary: true + timeout: true + required: + - name + - id + - message + - temporary + - timeout + - fault + APICounterIncrementUnauthorizedResponseBody: + title: 'Mediatype identifier: application/vnd.goa.error; view=default' + type: object + properties: + fault: + type: boolean + description: Is the error a server-side fault? + example: true + id: + type: string + description: ID is a unique identifier for this particular occurrence of the problem. + example: 123abc + message: + type: string + description: Message is a human-readable explanation specific to this occurrence of the problem. + example: parameter 'p' must be an integer + name: + type: string + description: Name is the name of this class of errors. + example: bad_request + temporary: + type: boolean + description: Is the error temporary? + example: false + timeout: + type: boolean + description: Is the error a timeout? + example: true + description: CounterIncrement_unauthorized_Response_Body result type (default view) + example: + fault: false + id: 123abc + message: parameter 'p' must be an integer + name: bad_request + temporary: false + timeout: true + required: + - name + - id + - message + - temporary + - timeout + - fault + CounterInfo: + title: 'Mediatype identifier: application/vnd.countup.counter-info`; view=default' + type: object + properties: + count: + type: integer + example: 718974130 + format: int32 + last_increment_at: + type: string + example: Consequuntur cupiditate pariatur aut placeat. + last_increment_by: + type: string + example: Ipsam nesciunt minima cupiditate. + next_finalize_at: + type: string + example: Amet voluptas ex nostrum aperiam explicabo sed. + description: CounterGetResponseBody result type (default view) + example: + count: 837784502 + last_increment_at: Eius rerum. + last_increment_by: Sint asperiores sed voluptas voluptatem. + next_finalize_at: Accusamus maiores ut voluptas. + required: + - count + - last_increment_by + - last_increment_at + - next_finalize_at + TeapotEchoRequestBody: + title: TeapotEchoRequestBody + type: object + properties: + text: + type: string + example: Sit non qui quaerat nobis incidunt porro. + example: + text: Quam ut nobis reiciendis. + required: + - text + TeapotEchoResponseBody: + title: TeapotEchoResponseBody + type: object + properties: + text: + type: string + example: Aliquid dignissimos non at doloremque. + example: + text: Et optio hic. + required: + - text + WebAnotherUnauthorizedResponseBody: + title: 'Mediatype identifier: application/vnd.goa.error; view=default' + type: object + properties: + fault: + type: boolean + description: Is the error a server-side fault? + example: true + id: + type: string + description: ID is a unique identifier for this particular occurrence of the problem. + example: 123abc + message: + type: string + description: Message is a human-readable explanation specific to this occurrence of the problem. + example: parameter 'p' must be an integer + name: + type: string + description: Name is the name of this class of errors. + example: bad_request + temporary: + type: boolean + description: Is the error temporary? + example: false + timeout: + type: boolean + description: Is the error a timeout? + example: true + description: Another_unauthorized_Response_Body result type (default view) + example: + fault: true + id: 123abc + message: parameter 'p' must be an integer + name: bad_request + temporary: true + timeout: true + required: + - name + - id + - message + - temporary + - timeout + - fault + WebIndexUnauthorizedResponseBody: + title: 'Mediatype identifier: application/vnd.goa.error; view=default' + type: object + properties: + fault: + type: boolean + description: Is the error a server-side fault? + example: false + id: + type: string + description: ID is a unique identifier for this particular occurrence of the problem. + example: 123abc + message: + type: string + description: Message is a human-readable explanation specific to this occurrence of the problem. + example: parameter 'p' must be an integer + name: + type: string + description: Name is the name of this class of errors. + example: bad_request + temporary: + type: boolean + description: Is the error temporary? + example: false + timeout: + type: boolean + description: Is the error a timeout? + example: false + description: Index_unauthorized_Response_Body result type (default view) + example: + fault: true + id: 123abc + message: parameter 'p' must be an integer + name: bad_request + temporary: false + timeout: true + required: + - name + - id + - message + - temporary + - timeout + - fault + WebLoginGoogleCallbackUnauthorizedResponseBody: + title: 'Mediatype identifier: application/vnd.goa.error; view=default' + type: object + properties: + fault: + type: boolean + description: Is the error a server-side fault? + example: false + id: + type: string + description: ID is a unique identifier for this particular occurrence of the problem. + example: 123abc + message: + type: string + description: Message is a human-readable explanation specific to this occurrence of the problem. + example: parameter 'p' must be an integer + name: + type: string + description: Name is the name of this class of errors. + example: bad_request + temporary: + type: boolean + description: Is the error temporary? + example: true + timeout: + type: boolean + description: Is the error a timeout? + example: false + description: LoginGoogleCallback_unauthorized_Response_Body result type (default view) + example: + fault: false + id: 123abc + message: parameter 'p' must be an integer + name: bad_request + temporary: false + timeout: false + required: + - name + - id + - message + - temporary + - timeout + - fault + WebLoginGoogleUnauthorizedResponseBody: + title: 'Mediatype identifier: application/vnd.goa.error; view=default' + type: object + properties: + fault: + type: boolean + description: Is the error a server-side fault? + example: false + id: + type: string + description: ID is a unique identifier for this particular occurrence of the problem. + example: 123abc + message: + type: string + description: Message is a human-readable explanation specific to this occurrence of the problem. + example: parameter 'p' must be an integer + name: + type: string + description: Name is the name of this class of errors. + example: bad_request + temporary: + type: boolean + description: Is the error temporary? + example: true + timeout: + type: boolean + description: Is the error a timeout? + example: true + description: LoginGoogle_unauthorized_Response_Body result type (default view) + example: + fault: false + id: 123abc + message: parameter 'p' must be an integer + name: bad_request + temporary: false + timeout: true + required: + - name + - id + - message + - temporary + - timeout + - fault + WebLogoutUnauthorizedResponseBody: + title: 'Mediatype identifier: application/vnd.goa.error; view=default' + type: object + properties: + fault: + type: boolean + description: Is the error a server-side fault? + example: false + id: + type: string + description: ID is a unique identifier for this particular occurrence of the problem. + example: 123abc + message: + type: string + description: Message is a human-readable explanation specific to this occurrence of the problem. + example: parameter 'p' must be an integer + name: + type: string + description: Name is the name of this class of errors. + example: bad_request + temporary: + type: boolean + description: Is the error temporary? + example: true + timeout: + type: boolean + description: Is the error a timeout? + example: false + description: Logout_unauthorized_Response_Body result type (default view) + example: + fault: true + id: 123abc + message: parameter 'p' must be an integer + name: bad_request + temporary: false + timeout: true + required: + - name + - id + - message + - temporary + - timeout + - fault + WebSessionTokenResponseBody: + title: WebSessionTokenResponseBody + type: object + properties: + token: + type: string + example: Ut aut possimus et. + example: + token: Magni harum atque dolor quod at consectetur. + required: + - token + WebSessionTokenUnauthorizedResponseBody: + title: 'Mediatype identifier: application/vnd.goa.error; view=default' + type: object + properties: + fault: + type: boolean + description: Is the error a server-side fault? + example: false + id: + type: string + description: ID is a unique identifier for this particular occurrence of the problem. + example: 123abc + message: + type: string + description: Message is a human-readable explanation specific to this occurrence of the problem. + example: parameter 'p' must be an integer + name: + type: string + description: Name is the name of this class of errors. + example: bad_request + temporary: + type: boolean + description: Is the error temporary? + example: true + timeout: + type: boolean + description: Is the error a timeout? + example: false + description: SessionToken_unauthorized_Response_Body result type (default view) + example: + fault: false + id: 123abc + message: parameter 'p' must be an integer + name: bad_request + temporary: true + timeout: false + required: + - name + - id + - message + - temporary + - timeout + - fault +securityDefinitions: + jwt_header_Authorization: + type: apiKey + description: |4- + **Security Scopes**: + * `api.user`: no description + name: Authorization + in: header diff --git a/app/api/v1/gen/http/openapi3.json b/app/api/v1/gen/http/openapi3.json new file mode 100644 index 0000000..dedd996 --- /dev/null +++ b/app/api/v1/gen/http/openapi3.json @@ -0,0 +1 @@ +{"openapi":"3.0.3","info":{"title":"Count Up","description":"A production-ready Go service deployed on Kubernetes","version":"1.0.0"},"servers":[{"url":"http://localhost:8080"},{"url":"http://localhost:80"}],"paths":{"/":{"get":{"tags":["web"],"summary":"Index web","operationId":"web#Index","responses":{"200":{"description":"OK response.","content":{"text/html":{"schema":{"type":"string","example":"SWxsbyBjdW0u","format":"binary"},"example":"RG9sb3JpYnVzIHZvbHVwdGF0ZW0gaGljIHRvdGFtIHJlcHVkaWFuZGFlLg=="}}},"401":{"description":"unauthorized: Unauthorized response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/another":{"get":{"tags":["web"],"summary":"Another web","operationId":"web#Another","responses":{"200":{"description":"OK response.","content":{"text/html":{"schema":{"type":"string","example":"RXN0IGFzc3VtZW5kYSBlbmltIGVhIGFzcGVybmF0dXIgdXQu","format":"binary"},"example":"RW5pbSBhbGlhcyBzdW50Lg=="}}},"401":{"description":"unauthorized: Unauthorized response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/auth/token":{"post":{"tags":["api"],"summary":"AuthToken api","operationId":"api#AuthToken","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthTokenRequestBody"},"example":{"access_token":"Itaque unde qui ut molestiae et omnis.","provider":"google"}}}},"responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthTokenResponseBody"},"example":{"token":"Est asperiores."}}}},"401":{"description":"unauthorized: Unauthorized response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"forbidden: Forbidden response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/counter":{"get":{"tags":["api"],"summary":"CounterGet api","operationId":"api#CounterGet","responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CounterInfo"},"example":{"count":855809606,"last_increment_at":"Non accusantium eos culpa autem illum architecto.","last_increment_by":"Eum inventore.","next_finalize_at":"Vel enim ut autem quo."}}}},"401":{"description":"unauthorized: Unauthorized response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"forbidden: Forbidden response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["api"],"summary":"CounterIncrement api","operationId":"api#CounterIncrement","responses":{"202":{"description":"Accepted response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CounterInfo"},"example":{"count":1227174102,"last_increment_at":"Harum error iste ipsam.","last_increment_by":"Quae cupiditate.","next_finalize_at":"Nobis non est."}}}},"401":{"description":"unauthorized: Unauthorized response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"forbidden: Forbidden response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"existing_increment_request: Too Many Requests response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"jwt_header_Authorization":["api.user"]}]}},"/api/v1/openapi.json":{"get":{"tags":["api"],"summary":"Download gen/http/openapi3.json","operationId":"api#/api/v1/openapi.json","responses":{"200":{"description":"File downloaded"}}}},"/echo":{"post":{"tags":["teapot"],"summary":"Echo teapot","operationId":"teapot#Echo","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EchoRequestBody"},"example":{"text":"Eos itaque."}}}},"responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EchoRequestBody"},"example":{"text":"Et veniam et illum quaerat et et."}}}}}}},"/login/google":{"get":{"tags":["web"],"summary":"LoginGoogle web","operationId":"web#LoginGoogle","responses":{"302":{"description":"Found response.","headers":{"Location":{"schema":{"type":"string","example":"Dolores velit."},"example":"Repellendus quo error."},"Set-Cookie":{"schema":{"type":"string","example":"Optio et."},"example":"Mollitia similique dignissimos."}}},"401":{"description":"unauthorized: Unauthorized response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/login/google/callback":{"get":{"tags":["web"],"summary":"LoginGoogleCallback web","operationId":"web#LoginGoogleCallback","parameters":[{"name":"code","in":"query","allowEmptyValue":true,"required":true,"schema":{"type":"string","example":"Libero soluta."},"example":"Exercitationem omnis perferendis ipsa dolor eum."},{"name":"state","in":"query","allowEmptyValue":true,"required":true,"schema":{"type":"string","example":"Consequuntur quo excepturi eos dolor voluptatem."},"example":"Quia et nulla."},{"name":"countup.session","in":"cookie","allowEmptyValue":true,"required":true,"schema":{"type":"string","example":"Explicabo veritatis labore quidem deserunt enim qui."},"example":"Aut ab."}],"responses":{"302":{"description":"Found response.","headers":{"Location":{"schema":{"type":"string","example":"Officiis consequuntur mollitia provident recusandae."},"example":"Expedita sint inventore possimus cumque magni."},"Set-Cookie":{"schema":{"type":"string","example":"Deleniti facere eius eos."},"example":"Omnis distinctio adipisci."}}},"401":{"description":"unauthorized: Unauthorized response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/logout":{"get":{"tags":["web"],"summary":"Logout web","operationId":"web#Logout","parameters":[{"name":"countup.session","in":"cookie","allowEmptyValue":true,"required":true,"schema":{"type":"string","example":"Aut magni consequatur omnis."},"example":"Soluta necessitatibus ad modi esse."}],"responses":{"302":{"description":"Found response.","headers":{"Location":{"schema":{"type":"string","example":"Iusto architecto corporis commodi aut minus."},"example":"Quo aut."},"Set-Cookie":{"schema":{"type":"string","example":"Illo qui sit tenetur accusamus tempore laboriosam."},"example":"Ea voluptas sit consectetur alias libero saepe."}}},"401":{"description":"unauthorized: Unauthorized response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/openapi.json":{"get":{"tags":["teapot"],"summary":"Download gen/http/openapi3.json","operationId":"teapot#/openapi.json","responses":{"200":{"description":"File downloaded"}}}},"/session/token":{"get":{"tags":["web"],"summary":"SessionToken web","operationId":"web#SessionToken","parameters":[{"name":"countup.session","in":"cookie","allowEmptyValue":true,"required":true,"schema":{"type":"string","example":"Ipsam autem."},"example":"Error earum ex."}],"responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthTokenResponseBody"},"example":{"token":"Et consectetur."}}}},"401":{"description":"unauthorized: Unauthorized response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/static/*":{"get":{"tags":["web"],"summary":"Download static/","operationId":"web#/static/*","responses":{"200":{"description":"File downloaded"}}}}},"components":{"schemas":{"AuthTokenRequestBody":{"type":"object","properties":{"access_token":{"type":"string","example":"Velit ullam itaque temporibus eius voluptatem aliquam."},"provider":{"type":"string","example":"google","enum":["google"]}},"example":{"access_token":"Pariatur dolores unde.","provider":"google"},"required":["provider","access_token"]},"AuthTokenResponseBody":{"type":"object","properties":{"token":{"type":"string","example":"Et sint velit aut blanditiis eius."}},"example":{"token":"Fugiat quia facilis sint eligendi commodi sit."},"required":["token"]},"CounterInfo":{"type":"object","properties":{"count":{"type":"integer","example":2045787089,"format":"int32"},"last_increment_at":{"type":"string","example":"Optio aut est."},"last_increment_by":{"type":"string","example":"Ex eos tempora eum."},"next_finalize_at":{"type":"string","example":"Animi rerum aut adipisci."}},"example":{"count":116253852,"last_increment_at":"Sunt ex distinctio et rerum pariatur.","last_increment_by":"Iusto perspiciatis laborum distinctio rem ipsam rem.","next_finalize_at":"Nihil totam ipsum."},"required":["count","last_increment_by","last_increment_at","next_finalize_at"]},"EchoRequestBody":{"type":"object","properties":{"text":{"type":"string","example":"Illo sequi."}},"example":{"text":"Aut dolorem."},"required":["text"]},"Error":{"type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":true},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":true},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":true}},"example":{"fault":true,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":true,"timeout":false},"required":["name","id","message","temporary","timeout","fault"]}},"securitySchemes":{"jwt_header_Authorization":{"type":"http","scheme":"bearer"}}},"tags":[{"name":"api"},{"name":"web"},{"name":"teapot"}]} \ No newline at end of file diff --git a/app/api/v1/gen/http/openapi3.yaml b/app/api/v1/gen/http/openapi3.yaml new file mode 100644 index 0000000..68065ae --- /dev/null +++ b/app/api/v1/gen/http/openapi3.yaml @@ -0,0 +1,567 @@ +openapi: 3.0.3 +info: + title: Count Up + description: A production-ready Go service deployed on Kubernetes + version: 1.0.0 +servers: + - url: http://localhost:8080 + - url: http://localhost:80 +paths: + /: + get: + tags: + - web + summary: Index web + operationId: web#Index + responses: + "200": + description: OK response. + content: + text/html: + schema: + type: string + example: + - 73 + - 108 + - 108 + - 111 + - 32 + - 99 + - 117 + - 109 + - 46 + format: binary + example: + - 68 + - 111 + - 108 + - 111 + - 114 + - 105 + - 98 + - 117 + - 115 + - 32 + - 118 + - 111 + - 108 + - 117 + - 112 + - 116 + - 97 + - 116 + - 101 + - 109 + - 32 + - 104 + - 105 + - 99 + - 32 + - 116 + - 111 + - 116 + - 97 + - 109 + - 32 + - 114 + - 101 + - 112 + - 117 + - 100 + - 105 + - 97 + - 110 + - 100 + - 97 + - 101 + - 46 + "401": + description: 'unauthorized: Unauthorized response.' + content: + application/vnd.goa.error: + schema: + $ref: '#/components/schemas/Error' + /another: + get: + tags: + - web + summary: Another web + operationId: web#Another + responses: + "200": + description: OK response. + content: + text/html: + schema: + type: string + example: + - 69 + - 115 + - 116 + - 32 + - 97 + - 115 + - 115 + - 117 + - 109 + - 101 + - 110 + - 100 + - 97 + - 32 + - 101 + - 110 + - 105 + - 109 + - 32 + - 101 + - 97 + - 32 + - 97 + - 115 + - 112 + - 101 + - 114 + - 110 + - 97 + - 116 + - 117 + - 114 + - 32 + - 117 + - 116 + - 46 + format: binary + example: + - 69 + - 110 + - 105 + - 109 + - 32 + - 97 + - 108 + - 105 + - 97 + - 115 + - 32 + - 115 + - 117 + - 110 + - 116 + - 46 + "401": + description: 'unauthorized: Unauthorized response.' + content: + application/vnd.goa.error: + schema: + $ref: '#/components/schemas/Error' + /api/v1/auth/token: + post: + tags: + - api + summary: AuthToken api + operationId: api#AuthToken + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AuthTokenRequestBody' + example: + access_token: Itaque unde qui ut molestiae et omnis. + provider: google + responses: + "200": + description: OK response. + content: + application/json: + schema: + $ref: '#/components/schemas/AuthTokenResponseBody' + example: + token: Est asperiores. + "401": + description: 'unauthorized: Unauthorized response.' + content: + application/vnd.goa.error: + schema: + $ref: '#/components/schemas/Error' + "403": + description: 'forbidden: Forbidden response.' + content: + application/vnd.goa.error: + schema: + $ref: '#/components/schemas/Error' + /api/v1/counter: + get: + tags: + - api + summary: CounterGet api + operationId: api#CounterGet + responses: + "200": + description: OK response. + content: + application/json: + schema: + $ref: '#/components/schemas/CounterInfo' + example: + count: 855809606 + last_increment_at: Non accusantium eos culpa autem illum architecto. + last_increment_by: Eum inventore. + next_finalize_at: Vel enim ut autem quo. + "401": + description: 'unauthorized: Unauthorized response.' + content: + application/vnd.goa.error: + schema: + $ref: '#/components/schemas/Error' + "403": + description: 'forbidden: Forbidden response.' + content: + application/vnd.goa.error: + schema: + $ref: '#/components/schemas/Error' + post: + tags: + - api + summary: CounterIncrement api + operationId: api#CounterIncrement + responses: + "202": + description: Accepted response. + content: + application/json: + schema: + $ref: '#/components/schemas/CounterInfo' + example: + count: 1227174102 + last_increment_at: Harum error iste ipsam. + last_increment_by: Quae cupiditate. + next_finalize_at: Nobis non est. + "401": + description: 'unauthorized: Unauthorized response.' + content: + application/vnd.goa.error: + schema: + $ref: '#/components/schemas/Error' + "403": + description: 'forbidden: Forbidden response.' + content: + application/vnd.goa.error: + schema: + $ref: '#/components/schemas/Error' + "429": + description: 'existing_increment_request: Too Many Requests response.' + content: + application/vnd.goa.error: + schema: + $ref: '#/components/schemas/Error' + security: + - jwt_header_Authorization: + - api.user + /api/v1/openapi.json: + get: + tags: + - api + summary: Download gen/http/openapi3.json + operationId: api#/api/v1/openapi.json + responses: + "200": + description: File downloaded + /echo: + post: + tags: + - teapot + summary: Echo teapot + operationId: teapot#Echo + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EchoRequestBody' + example: + text: Eos itaque. + responses: + "200": + description: OK response. + content: + application/json: + schema: + $ref: '#/components/schemas/EchoRequestBody' + example: + text: Et veniam et illum quaerat et et. + /login/google: + get: + tags: + - web + summary: LoginGoogle web + operationId: web#LoginGoogle + responses: + "302": + description: Found response. + headers: + Location: + schema: + type: string + example: Dolores velit. + example: Repellendus quo error. + Set-Cookie: + schema: + type: string + example: Optio et. + example: Mollitia similique dignissimos. + "401": + description: 'unauthorized: Unauthorized response.' + content: + application/vnd.goa.error: + schema: + $ref: '#/components/schemas/Error' + /login/google/callback: + get: + tags: + - web + summary: LoginGoogleCallback web + operationId: web#LoginGoogleCallback + parameters: + - name: code + in: query + allowEmptyValue: true + required: true + schema: + type: string + example: Libero soluta. + example: Exercitationem omnis perferendis ipsa dolor eum. + - name: state + in: query + allowEmptyValue: true + required: true + schema: + type: string + example: Consequuntur quo excepturi eos dolor voluptatem. + example: Quia et nulla. + - name: countup.session + in: cookie + allowEmptyValue: true + required: true + schema: + type: string + example: Explicabo veritatis labore quidem deserunt enim qui. + example: Aut ab. + responses: + "302": + description: Found response. + headers: + Location: + schema: + type: string + example: Officiis consequuntur mollitia provident recusandae. + example: Expedita sint inventore possimus cumque magni. + Set-Cookie: + schema: + type: string + example: Deleniti facere eius eos. + example: Omnis distinctio adipisci. + "401": + description: 'unauthorized: Unauthorized response.' + content: + application/vnd.goa.error: + schema: + $ref: '#/components/schemas/Error' + /logout: + get: + tags: + - web + summary: Logout web + operationId: web#Logout + parameters: + - name: countup.session + in: cookie + allowEmptyValue: true + required: true + schema: + type: string + example: Aut magni consequatur omnis. + example: Soluta necessitatibus ad modi esse. + responses: + "302": + description: Found response. + headers: + Location: + schema: + type: string + example: Iusto architecto corporis commodi aut minus. + example: Quo aut. + Set-Cookie: + schema: + type: string + example: Illo qui sit tenetur accusamus tempore laboriosam. + example: Ea voluptas sit consectetur alias libero saepe. + "401": + description: 'unauthorized: Unauthorized response.' + content: + application/vnd.goa.error: + schema: + $ref: '#/components/schemas/Error' + /openapi.json: + get: + tags: + - teapot + summary: Download gen/http/openapi3.json + operationId: teapot#/openapi.json + responses: + "200": + description: File downloaded + /session/token: + get: + tags: + - web + summary: SessionToken web + operationId: web#SessionToken + parameters: + - name: countup.session + in: cookie + allowEmptyValue: true + required: true + schema: + type: string + example: Ipsam autem. + example: Error earum ex. + responses: + "200": + description: OK response. + content: + application/json: + schema: + $ref: '#/components/schemas/AuthTokenResponseBody' + example: + token: Et consectetur. + "401": + description: 'unauthorized: Unauthorized response.' + content: + application/vnd.goa.error: + schema: + $ref: '#/components/schemas/Error' + /static/*: + get: + tags: + - web + summary: Download static/ + operationId: web#/static/* + responses: + "200": + description: File downloaded +components: + schemas: + AuthTokenRequestBody: + type: object + properties: + access_token: + type: string + example: Velit ullam itaque temporibus eius voluptatem aliquam. + provider: + type: string + example: google + enum: + - google + example: + access_token: Pariatur dolores unde. + provider: google + required: + - provider + - access_token + AuthTokenResponseBody: + type: object + properties: + token: + type: string + example: Et sint velit aut blanditiis eius. + example: + token: Fugiat quia facilis sint eligendi commodi sit. + required: + - token + CounterInfo: + type: object + properties: + count: + type: integer + example: 2045787089 + format: int32 + last_increment_at: + type: string + example: Optio aut est. + last_increment_by: + type: string + example: Ex eos tempora eum. + next_finalize_at: + type: string + example: Animi rerum aut adipisci. + example: + count: 116253852 + last_increment_at: Sunt ex distinctio et rerum pariatur. + last_increment_by: Iusto perspiciatis laborum distinctio rem ipsam rem. + next_finalize_at: Nihil totam ipsum. + required: + - count + - last_increment_by + - last_increment_at + - next_finalize_at + EchoRequestBody: + type: object + properties: + text: + type: string + example: Illo sequi. + example: + text: Aut dolorem. + required: + - text + Error: + type: object + properties: + fault: + type: boolean + description: Is the error a server-side fault? + example: true + id: + type: string + description: ID is a unique identifier for this particular occurrence of the problem. + example: 123abc + message: + type: string + description: Message is a human-readable explanation specific to this occurrence of the problem. + example: parameter 'p' must be an integer + name: + type: string + description: Name is the name of this class of errors. + example: bad_request + temporary: + type: boolean + description: Is the error temporary? + example: true + timeout: + type: boolean + description: Is the error a timeout? + example: true + example: + fault: true + id: 123abc + message: parameter 'p' must be an integer + name: bad_request + temporary: true + timeout: false + required: + - name + - id + - message + - temporary + - timeout + - fault + securitySchemes: + jwt_header_Authorization: + type: http + scheme: bearer +tags: + - name: api + - name: web + - name: teapot diff --git a/app/api/v1/gen/http/teapot/client/cli.go b/app/api/v1/gen/http/teapot/client/cli.go new file mode 100644 index 0000000..62d8dfa --- /dev/null +++ b/app/api/v1/gen/http/teapot/client/cli.go @@ -0,0 +1,33 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// teapot HTTP client CLI support package +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package client + +import ( + "encoding/json" + "fmt" + + teapot "github.com/jace-ys/countup/api/v1/gen/teapot" +) + +// BuildEchoPayload builds the payload for the teapot Echo endpoint from CLI +// flags. +func BuildEchoPayload(teapotEchoBody string) (*teapot.EchoPayload, error) { + var err error + var body EchoRequestBody + { + err = json.Unmarshal([]byte(teapotEchoBody), &body) + if err != nil { + return nil, fmt.Errorf("invalid JSON for body, \nerror: %s, \nexample of valid JSON:\n%s", err, "'{\n \"text\": \"Eos itaque.\"\n }'") + } + } + v := &teapot.EchoPayload{ + Text: body.Text, + } + + return v, nil +} diff --git a/app/api/v1/gen/http/teapot/client/client.go b/app/api/v1/gen/http/teapot/client/client.go new file mode 100644 index 0000000..3e791e2 --- /dev/null +++ b/app/api/v1/gen/http/teapot/client/client.go @@ -0,0 +1,74 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// teapot client HTTP transport +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package client + +import ( + "context" + "net/http" + + goahttp "goa.design/goa/v3/http" + goa "goa.design/goa/v3/pkg" +) + +// Client lists the teapot service endpoint HTTP clients. +type Client struct { + // Echo Doer is the HTTP client used to make requests to the Echo endpoint. + EchoDoer goahttp.Doer + + // RestoreResponseBody controls whether the response bodies are reset after + // decoding so they can be read again. + RestoreResponseBody bool + + scheme string + host string + encoder func(*http.Request) goahttp.Encoder + decoder func(*http.Response) goahttp.Decoder +} + +// NewClient instantiates HTTP clients for all the teapot service servers. +func NewClient( + scheme string, + host string, + doer goahttp.Doer, + enc func(*http.Request) goahttp.Encoder, + dec func(*http.Response) goahttp.Decoder, + restoreBody bool, +) *Client { + return &Client{ + EchoDoer: doer, + RestoreResponseBody: restoreBody, + scheme: scheme, + host: host, + decoder: dec, + encoder: enc, + } +} + +// Echo returns an endpoint that makes HTTP requests to the teapot service Echo +// server. +func (c *Client) Echo() goa.Endpoint { + var ( + encodeRequest = EncodeEchoRequest(c.encoder) + decodeResponse = DecodeEchoResponse(c.decoder, c.RestoreResponseBody) + ) + return func(ctx context.Context, v any) (any, error) { + req, err := c.BuildEchoRequest(ctx, v) + if err != nil { + return nil, err + } + err = encodeRequest(req, v) + if err != nil { + return nil, err + } + resp, err := c.EchoDoer.Do(req) + if err != nil { + return nil, goahttp.ErrRequestError("teapot", "Echo", err) + } + return decodeResponse(resp) + } +} diff --git a/app/api/v1/gen/http/teapot/client/encode_decode.go b/app/api/v1/gen/http/teapot/client/encode_decode.go new file mode 100644 index 0000000..e7adb3f --- /dev/null +++ b/app/api/v1/gen/http/teapot/client/encode_decode.go @@ -0,0 +1,90 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// teapot HTTP client encoders and decoders +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package client + +import ( + "bytes" + "context" + "io" + "net/http" + "net/url" + + teapot "github.com/jace-ys/countup/api/v1/gen/teapot" + goahttp "goa.design/goa/v3/http" +) + +// BuildEchoRequest instantiates a HTTP request object with method and path set +// to call the "teapot" service "Echo" endpoint +func (c *Client) BuildEchoRequest(ctx context.Context, v any) (*http.Request, error) { + u := &url.URL{Scheme: c.scheme, Host: c.host, Path: EchoTeapotPath()} + req, err := http.NewRequest("POST", u.String(), nil) + if err != nil { + return nil, goahttp.ErrInvalidURL("teapot", "Echo", u.String(), err) + } + if ctx != nil { + req = req.WithContext(ctx) + } + + return req, nil +} + +// EncodeEchoRequest returns an encoder for requests sent to the teapot Echo +// server. +func EncodeEchoRequest(encoder func(*http.Request) goahttp.Encoder) func(*http.Request, any) error { + return func(req *http.Request, v any) error { + p, ok := v.(*teapot.EchoPayload) + if !ok { + return goahttp.ErrInvalidType("teapot", "Echo", "*teapot.EchoPayload", v) + } + body := NewEchoRequestBody(p) + if err := encoder(req).Encode(&body); err != nil { + return goahttp.ErrEncodingError("teapot", "Echo", err) + } + return nil + } +} + +// DecodeEchoResponse returns a decoder for responses returned by the teapot +// Echo endpoint. restoreBody controls whether the response body should be +// restored after having been read. +func DecodeEchoResponse(decoder func(*http.Response) goahttp.Decoder, restoreBody bool) func(*http.Response) (any, error) { + return func(resp *http.Response) (any, error) { + if restoreBody { + b, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + defer func() { + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + }() + } else { + defer resp.Body.Close() + } + switch resp.StatusCode { + case http.StatusOK: + var ( + body EchoResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("teapot", "Echo", err) + } + err = ValidateEchoResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("teapot", "Echo", err) + } + res := NewEchoResultOK(&body) + return res, nil + default: + body, _ := io.ReadAll(resp.Body) + return nil, goahttp.ErrInvalidResponse("teapot", "Echo", resp.StatusCode, string(body)) + } + } +} diff --git a/app/api/v1/gen/http/teapot/client/paths.go b/app/api/v1/gen/http/teapot/client/paths.go new file mode 100644 index 0000000..1efe5e6 --- /dev/null +++ b/app/api/v1/gen/http/teapot/client/paths.go @@ -0,0 +1,13 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// HTTP request path constructors for the teapot service. +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package client + +// EchoTeapotPath returns the URL path to the teapot service Echo HTTP endpoint. +func EchoTeapotPath() string { + return "/echo" +} diff --git a/app/api/v1/gen/http/teapot/client/types.go b/app/api/v1/gen/http/teapot/client/types.go new file mode 100644 index 0000000..fb91ed1 --- /dev/null +++ b/app/api/v1/gen/http/teapot/client/types.go @@ -0,0 +1,52 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// teapot HTTP client types +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package client + +import ( + teapot "github.com/jace-ys/countup/api/v1/gen/teapot" + goa "goa.design/goa/v3/pkg" +) + +// EchoRequestBody is the type of the "teapot" service "Echo" endpoint HTTP +// request body. +type EchoRequestBody struct { + Text string `form:"text" json:"text" xml:"text"` +} + +// EchoResponseBody is the type of the "teapot" service "Echo" endpoint HTTP +// response body. +type EchoResponseBody struct { + Text *string `form:"text,omitempty" json:"text,omitempty" xml:"text,omitempty"` +} + +// NewEchoRequestBody builds the HTTP request body from the payload of the +// "Echo" endpoint of the "teapot" service. +func NewEchoRequestBody(p *teapot.EchoPayload) *EchoRequestBody { + body := &EchoRequestBody{ + Text: p.Text, + } + return body +} + +// NewEchoResultOK builds a "teapot" service "Echo" endpoint result from a HTTP +// "OK" response. +func NewEchoResultOK(body *EchoResponseBody) *teapot.EchoResult { + v := &teapot.EchoResult{ + Text: *body.Text, + } + + return v +} + +// ValidateEchoResponseBody runs the validations defined on EchoResponseBody +func ValidateEchoResponseBody(body *EchoResponseBody) (err error) { + if body.Text == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("text", "body")) + } + return +} diff --git a/app/api/v1/gen/http/teapot/server/encode_decode.go b/app/api/v1/gen/http/teapot/server/encode_decode.go new file mode 100644 index 0000000..1017d40 --- /dev/null +++ b/app/api/v1/gen/http/teapot/server/encode_decode.go @@ -0,0 +1,60 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// teapot HTTP server encoders and decoders +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package server + +import ( + "context" + "errors" + "io" + "net/http" + + teapot "github.com/jace-ys/countup/api/v1/gen/teapot" + goahttp "goa.design/goa/v3/http" + goa "goa.design/goa/v3/pkg" +) + +// EncodeEchoResponse returns an encoder for responses returned by the teapot +// Echo endpoint. +func EncodeEchoResponse(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder) func(context.Context, http.ResponseWriter, any) error { + return func(ctx context.Context, w http.ResponseWriter, v any) error { + res, _ := v.(*teapot.EchoResult) + enc := encoder(ctx, w) + body := NewEchoResponseBody(res) + w.WriteHeader(http.StatusOK) + return enc.Encode(body) + } +} + +// DecodeEchoRequest returns a decoder for requests sent to the teapot Echo +// endpoint. +func DecodeEchoRequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp.Decoder) func(*http.Request) (any, error) { + return func(r *http.Request) (any, error) { + var ( + body EchoRequestBody + err error + ) + err = decoder(r).Decode(&body) + if err != nil { + if err == io.EOF { + return nil, goa.MissingPayloadError() + } + var gerr *goa.ServiceError + if errors.As(err, &gerr) { + return nil, gerr + } + return nil, goa.DecodePayloadError(err.Error()) + } + err = ValidateEchoRequestBody(&body) + if err != nil { + return nil, err + } + payload := NewEchoPayload(&body) + + return payload, nil + } +} diff --git a/app/api/v1/gen/http/teapot/server/paths.go b/app/api/v1/gen/http/teapot/server/paths.go new file mode 100644 index 0000000..264b16d --- /dev/null +++ b/app/api/v1/gen/http/teapot/server/paths.go @@ -0,0 +1,13 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// HTTP request path constructors for the teapot service. +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package server + +// EchoTeapotPath returns the URL path to the teapot service Echo HTTP endpoint. +func EchoTeapotPath() string { + return "/echo" +} diff --git a/app/api/v1/gen/http/teapot/server/server.go b/app/api/v1/gen/http/teapot/server/server.go new file mode 100644 index 0000000..08546c6 --- /dev/null +++ b/app/api/v1/gen/http/teapot/server/server.go @@ -0,0 +1,167 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// teapot HTTP server +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package server + +import ( + "context" + "net/http" + "path" + + teapot "github.com/jace-ys/countup/api/v1/gen/teapot" + goahttp "goa.design/goa/v3/http" + goa "goa.design/goa/v3/pkg" +) + +// Server lists the teapot service endpoint HTTP handlers. +type Server struct { + Mounts []*MountPoint + Echo http.Handler + GenHTTPOpenapi3JSON http.Handler +} + +// MountPoint holds information about the mounted endpoints. +type MountPoint struct { + // Method is the name of the service method served by the mounted HTTP handler. + Method string + // Verb is the HTTP method used to match requests to the mounted handler. + Verb string + // Pattern is the HTTP request path pattern used to match requests to the + // mounted handler. + Pattern string +} + +// New instantiates HTTP handlers for all the teapot service endpoints using +// the provided encoder and decoder. The handlers are mounted on the given mux +// using the HTTP verb and path defined in the design. errhandler is called +// whenever a response fails to be encoded. formatter is used to format errors +// returned by the service methods prior to encoding. Both errhandler and +// formatter are optional and can be nil. +func New( + e *teapot.Endpoints, + mux goahttp.Muxer, + decoder func(*http.Request) goahttp.Decoder, + encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, + errhandler func(context.Context, http.ResponseWriter, error), + formatter func(ctx context.Context, err error) goahttp.Statuser, + fileSystemGenHTTPOpenapi3JSON http.FileSystem, +) *Server { + if fileSystemGenHTTPOpenapi3JSON == nil { + fileSystemGenHTTPOpenapi3JSON = http.Dir(".") + } + fileSystemGenHTTPOpenapi3JSON = appendPrefix(fileSystemGenHTTPOpenapi3JSON, "/gen/http") + return &Server{ + Mounts: []*MountPoint{ + {"Echo", "POST", "/echo"}, + {"Serve gen/http/openapi3.json", "GET", "/openapi.json"}, + }, + Echo: NewEchoHandler(e.Echo, mux, decoder, encoder, errhandler, formatter), + GenHTTPOpenapi3JSON: http.FileServer(fileSystemGenHTTPOpenapi3JSON), + } +} + +// Service returns the name of the service served. +func (s *Server) Service() string { return "teapot" } + +// Use wraps the server handlers with the given middleware. +func (s *Server) Use(m func(http.Handler) http.Handler) { + s.Echo = m(s.Echo) +} + +// MethodNames returns the methods served. +func (s *Server) MethodNames() []string { return teapot.MethodNames[:] } + +// Mount configures the mux to serve the teapot endpoints. +func Mount(mux goahttp.Muxer, h *Server) { + MountEchoHandler(mux, h.Echo) + MountGenHTTPOpenapi3JSON(mux, h.GenHTTPOpenapi3JSON) +} + +// Mount configures the mux to serve the teapot endpoints. +func (s *Server) Mount(mux goahttp.Muxer) { + Mount(mux, s) +} + +// MountEchoHandler configures the mux to serve the "teapot" service "Echo" +// endpoint. +func MountEchoHandler(mux goahttp.Muxer, h http.Handler) { + f, ok := h.(http.HandlerFunc) + if !ok { + f = func(w http.ResponseWriter, r *http.Request) { + h.ServeHTTP(w, r) + } + } + mux.Handle("POST", "/echo", f) +} + +// NewEchoHandler creates a HTTP handler which loads the HTTP request and calls +// the "teapot" service "Echo" endpoint. +func NewEchoHandler( + endpoint goa.Endpoint, + mux goahttp.Muxer, + decoder func(*http.Request) goahttp.Decoder, + encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, + errhandler func(context.Context, http.ResponseWriter, error), + formatter func(ctx context.Context, err error) goahttp.Statuser, +) http.Handler { + var ( + decodeRequest = DecodeEchoRequest(mux, decoder) + encodeResponse = EncodeEchoResponse(encoder) + encodeError = goahttp.ErrorEncoder(encoder, formatter) + ) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), goahttp.AcceptTypeKey, r.Header.Get("Accept")) + ctx = context.WithValue(ctx, goa.MethodKey, "Echo") + ctx = context.WithValue(ctx, goa.ServiceKey, "teapot") + payload, err := decodeRequest(r) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + res, err := endpoint(ctx, payload) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + if err := encodeResponse(ctx, w, res); err != nil { + errhandler(ctx, w, err) + } + }) +} + +// appendFS is a custom implementation of fs.FS that appends a specified prefix +// to the file paths before delegating the Open call to the underlying fs.FS. +type appendFS struct { + prefix string + fs http.FileSystem +} + +// Open opens the named file, appending the prefix to the file path before +// passing it to the underlying fs.FS. +func (s appendFS) Open(name string) (http.File, error) { + switch name { + case "/openapi.json": + name = "/openapi3.json" + } + return s.fs.Open(path.Join(s.prefix, name)) +} + +// appendPrefix returns a new fs.FS that appends the specified prefix to file paths +// before delegating to the provided embed.FS. +func appendPrefix(fsys http.FileSystem, prefix string) http.FileSystem { + return appendFS{prefix: prefix, fs: fsys} +} + +// MountGenHTTPOpenapi3JSON configures the mux to serve GET request made to +// "/openapi.json". +func MountGenHTTPOpenapi3JSON(mux goahttp.Muxer, h http.Handler) { + mux.Handle("GET", "/openapi.json", h.ServeHTTP) +} diff --git a/app/api/v1/gen/http/teapot/server/types.go b/app/api/v1/gen/http/teapot/server/types.go new file mode 100644 index 0000000..114465f --- /dev/null +++ b/app/api/v1/gen/http/teapot/server/types.go @@ -0,0 +1,51 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// teapot HTTP server types +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package server + +import ( + teapot "github.com/jace-ys/countup/api/v1/gen/teapot" + goa "goa.design/goa/v3/pkg" +) + +// EchoRequestBody is the type of the "teapot" service "Echo" endpoint HTTP +// request body. +type EchoRequestBody struct { + Text *string `form:"text,omitempty" json:"text,omitempty" xml:"text,omitempty"` +} + +// EchoResponseBody is the type of the "teapot" service "Echo" endpoint HTTP +// response body. +type EchoResponseBody struct { + Text string `form:"text" json:"text" xml:"text"` +} + +// NewEchoResponseBody builds the HTTP response body from the result of the +// "Echo" endpoint of the "teapot" service. +func NewEchoResponseBody(res *teapot.EchoResult) *EchoResponseBody { + body := &EchoResponseBody{ + Text: res.Text, + } + return body +} + +// NewEchoPayload builds a teapot service Echo endpoint payload. +func NewEchoPayload(body *EchoRequestBody) *teapot.EchoPayload { + v := &teapot.EchoPayload{ + Text: *body.Text, + } + + return v +} + +// ValidateEchoRequestBody runs the validations defined on EchoRequestBody +func ValidateEchoRequestBody(body *EchoRequestBody) (err error) { + if body.Text == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("text", "body")) + } + return +} diff --git a/app/api/v1/gen/http/web/client/cli.go b/app/api/v1/gen/http/web/client/cli.go new file mode 100644 index 0000000..5d68586 --- /dev/null +++ b/app/api/v1/gen/http/web/client/cli.go @@ -0,0 +1,61 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// web HTTP client CLI support package +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package client + +import ( + web "github.com/jace-ys/countup/api/v1/gen/web" +) + +// BuildLoginGoogleCallbackPayload builds the payload for the web +// LoginGoogleCallback endpoint from CLI flags. +func BuildLoginGoogleCallbackPayload(webLoginGoogleCallbackCode string, webLoginGoogleCallbackState string, webLoginGoogleCallbackSessionCookie string) (*web.LoginGoogleCallbackPayload, error) { + var code string + { + code = webLoginGoogleCallbackCode + } + var state string + { + state = webLoginGoogleCallbackState + } + var sessionCookie string + { + sessionCookie = webLoginGoogleCallbackSessionCookie + } + v := &web.LoginGoogleCallbackPayload{} + v.Code = code + v.State = state + v.SessionCookie = sessionCookie + + return v, nil +} + +// BuildLogoutPayload builds the payload for the web Logout endpoint from CLI +// flags. +func BuildLogoutPayload(webLogoutSessionCookie string) (*web.LogoutPayload, error) { + var sessionCookie string + { + sessionCookie = webLogoutSessionCookie + } + v := &web.LogoutPayload{} + v.SessionCookie = sessionCookie + + return v, nil +} + +// BuildSessionTokenPayload builds the payload for the web SessionToken +// endpoint from CLI flags. +func BuildSessionTokenPayload(webSessionTokenSessionCookie string) (*web.SessionTokenPayload, error) { + var sessionCookie string + { + sessionCookie = webSessionTokenSessionCookie + } + v := &web.SessionTokenPayload{} + v.SessionCookie = sessionCookie + + return v, nil +} diff --git a/app/api/v1/gen/http/web/client/client.go b/app/api/v1/gen/http/web/client/client.go new file mode 100644 index 0000000..5f40d5c --- /dev/null +++ b/app/api/v1/gen/http/web/client/client.go @@ -0,0 +1,203 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// web client HTTP transport +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package client + +import ( + "context" + "net/http" + + goahttp "goa.design/goa/v3/http" + goa "goa.design/goa/v3/pkg" +) + +// Client lists the web service endpoint HTTP clients. +type Client struct { + // Index Doer is the HTTP client used to make requests to the Index endpoint. + IndexDoer goahttp.Doer + + // Another Doer is the HTTP client used to make requests to the Another + // endpoint. + AnotherDoer goahttp.Doer + + // LoginGoogle Doer is the HTTP client used to make requests to the LoginGoogle + // endpoint. + LoginGoogleDoer goahttp.Doer + + // LoginGoogleCallback Doer is the HTTP client used to make requests to the + // LoginGoogleCallback endpoint. + LoginGoogleCallbackDoer goahttp.Doer + + // Logout Doer is the HTTP client used to make requests to the Logout endpoint. + LogoutDoer goahttp.Doer + + // SessionToken Doer is the HTTP client used to make requests to the + // SessionToken endpoint. + SessionTokenDoer goahttp.Doer + + // RestoreResponseBody controls whether the response bodies are reset after + // decoding so they can be read again. + RestoreResponseBody bool + + scheme string + host string + encoder func(*http.Request) goahttp.Encoder + decoder func(*http.Response) goahttp.Decoder +} + +// NewClient instantiates HTTP clients for all the web service servers. +func NewClient( + scheme string, + host string, + doer goahttp.Doer, + enc func(*http.Request) goahttp.Encoder, + dec func(*http.Response) goahttp.Decoder, + restoreBody bool, +) *Client { + return &Client{ + IndexDoer: doer, + AnotherDoer: doer, + LoginGoogleDoer: doer, + LoginGoogleCallbackDoer: doer, + LogoutDoer: doer, + SessionTokenDoer: doer, + RestoreResponseBody: restoreBody, + scheme: scheme, + host: host, + decoder: dec, + encoder: enc, + } +} + +// Index returns an endpoint that makes HTTP requests to the web service Index +// server. +func (c *Client) Index() goa.Endpoint { + var ( + decodeResponse = DecodeIndexResponse(c.decoder, c.RestoreResponseBody) + ) + return func(ctx context.Context, v any) (any, error) { + req, err := c.BuildIndexRequest(ctx, v) + if err != nil { + return nil, err + } + resp, err := c.IndexDoer.Do(req) + if err != nil { + return nil, goahttp.ErrRequestError("web", "Index", err) + } + return decodeResponse(resp) + } +} + +// Another returns an endpoint that makes HTTP requests to the web service +// Another server. +func (c *Client) Another() goa.Endpoint { + var ( + decodeResponse = DecodeAnotherResponse(c.decoder, c.RestoreResponseBody) + ) + return func(ctx context.Context, v any) (any, error) { + req, err := c.BuildAnotherRequest(ctx, v) + if err != nil { + return nil, err + } + resp, err := c.AnotherDoer.Do(req) + if err != nil { + return nil, goahttp.ErrRequestError("web", "Another", err) + } + return decodeResponse(resp) + } +} + +// LoginGoogle returns an endpoint that makes HTTP requests to the web service +// LoginGoogle server. +func (c *Client) LoginGoogle() goa.Endpoint { + var ( + decodeResponse = DecodeLoginGoogleResponse(c.decoder, c.RestoreResponseBody) + ) + return func(ctx context.Context, v any) (any, error) { + req, err := c.BuildLoginGoogleRequest(ctx, v) + if err != nil { + return nil, err + } + resp, err := c.LoginGoogleDoer.Do(req) + if err != nil { + return nil, goahttp.ErrRequestError("web", "LoginGoogle", err) + } + return decodeResponse(resp) + } +} + +// LoginGoogleCallback returns an endpoint that makes HTTP requests to the web +// service LoginGoogleCallback server. +func (c *Client) LoginGoogleCallback() goa.Endpoint { + var ( + encodeRequest = EncodeLoginGoogleCallbackRequest(c.encoder) + decodeResponse = DecodeLoginGoogleCallbackResponse(c.decoder, c.RestoreResponseBody) + ) + return func(ctx context.Context, v any) (any, error) { + req, err := c.BuildLoginGoogleCallbackRequest(ctx, v) + if err != nil { + return nil, err + } + err = encodeRequest(req, v) + if err != nil { + return nil, err + } + resp, err := c.LoginGoogleCallbackDoer.Do(req) + if err != nil { + return nil, goahttp.ErrRequestError("web", "LoginGoogleCallback", err) + } + return decodeResponse(resp) + } +} + +// Logout returns an endpoint that makes HTTP requests to the web service +// Logout server. +func (c *Client) Logout() goa.Endpoint { + var ( + encodeRequest = EncodeLogoutRequest(c.encoder) + decodeResponse = DecodeLogoutResponse(c.decoder, c.RestoreResponseBody) + ) + return func(ctx context.Context, v any) (any, error) { + req, err := c.BuildLogoutRequest(ctx, v) + if err != nil { + return nil, err + } + err = encodeRequest(req, v) + if err != nil { + return nil, err + } + resp, err := c.LogoutDoer.Do(req) + if err != nil { + return nil, goahttp.ErrRequestError("web", "Logout", err) + } + return decodeResponse(resp) + } +} + +// SessionToken returns an endpoint that makes HTTP requests to the web service +// SessionToken server. +func (c *Client) SessionToken() goa.Endpoint { + var ( + encodeRequest = EncodeSessionTokenRequest(c.encoder) + decodeResponse = DecodeSessionTokenResponse(c.decoder, c.RestoreResponseBody) + ) + return func(ctx context.Context, v any) (any, error) { + req, err := c.BuildSessionTokenRequest(ctx, v) + if err != nil { + return nil, err + } + err = encodeRequest(req, v) + if err != nil { + return nil, err + } + resp, err := c.SessionTokenDoer.Do(req) + if err != nil { + return nil, goahttp.ErrRequestError("web", "SessionToken", err) + } + return decodeResponse(resp) + } +} diff --git a/app/api/v1/gen/http/web/client/encode_decode.go b/app/api/v1/gen/http/web/client/encode_decode.go new file mode 100644 index 0000000..2df36b0 --- /dev/null +++ b/app/api/v1/gen/http/web/client/encode_decode.go @@ -0,0 +1,551 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// web HTTP client encoders and decoders +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package client + +import ( + "bytes" + "context" + "io" + "net/http" + "net/url" + + web "github.com/jace-ys/countup/api/v1/gen/web" + goahttp "goa.design/goa/v3/http" + goa "goa.design/goa/v3/pkg" +) + +// BuildIndexRequest instantiates a HTTP request object with method and path +// set to call the "web" service "Index" endpoint +func (c *Client) BuildIndexRequest(ctx context.Context, v any) (*http.Request, error) { + u := &url.URL{Scheme: c.scheme, Host: c.host, Path: IndexWebPath()} + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return nil, goahttp.ErrInvalidURL("web", "Index", u.String(), err) + } + if ctx != nil { + req = req.WithContext(ctx) + } + + return req, nil +} + +// DecodeIndexResponse returns a decoder for responses returned by the web +// Index endpoint. restoreBody controls whether the response body should be +// restored after having been read. +// DecodeIndexResponse may return the following errors: +// - "unauthorized" (type *goa.ServiceError): http.StatusUnauthorized +// - error: internal error +func DecodeIndexResponse(decoder func(*http.Response) goahttp.Decoder, restoreBody bool) func(*http.Response) (any, error) { + return func(resp *http.Response) (any, error) { + if restoreBody { + b, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + defer func() { + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + }() + } else { + defer resp.Body.Close() + } + switch resp.StatusCode { + case http.StatusOK: + var ( + body []byte + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("web", "Index", err) + } + return body, nil + case http.StatusUnauthorized: + var ( + body IndexUnauthorizedResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("web", "Index", err) + } + err = ValidateIndexUnauthorizedResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("web", "Index", err) + } + return nil, NewIndexUnauthorized(&body) + default: + body, _ := io.ReadAll(resp.Body) + return nil, goahttp.ErrInvalidResponse("web", "Index", resp.StatusCode, string(body)) + } + } +} + +// BuildAnotherRequest instantiates a HTTP request object with method and path +// set to call the "web" service "Another" endpoint +func (c *Client) BuildAnotherRequest(ctx context.Context, v any) (*http.Request, error) { + u := &url.URL{Scheme: c.scheme, Host: c.host, Path: AnotherWebPath()} + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return nil, goahttp.ErrInvalidURL("web", "Another", u.String(), err) + } + if ctx != nil { + req = req.WithContext(ctx) + } + + return req, nil +} + +// DecodeAnotherResponse returns a decoder for responses returned by the web +// Another endpoint. restoreBody controls whether the response body should be +// restored after having been read. +// DecodeAnotherResponse may return the following errors: +// - "unauthorized" (type *goa.ServiceError): http.StatusUnauthorized +// - error: internal error +func DecodeAnotherResponse(decoder func(*http.Response) goahttp.Decoder, restoreBody bool) func(*http.Response) (any, error) { + return func(resp *http.Response) (any, error) { + if restoreBody { + b, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + defer func() { + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + }() + } else { + defer resp.Body.Close() + } + switch resp.StatusCode { + case http.StatusOK: + var ( + body []byte + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("web", "Another", err) + } + return body, nil + case http.StatusUnauthorized: + var ( + body AnotherUnauthorizedResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("web", "Another", err) + } + err = ValidateAnotherUnauthorizedResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("web", "Another", err) + } + return nil, NewAnotherUnauthorized(&body) + default: + body, _ := io.ReadAll(resp.Body) + return nil, goahttp.ErrInvalidResponse("web", "Another", resp.StatusCode, string(body)) + } + } +} + +// BuildLoginGoogleRequest instantiates a HTTP request object with method and +// path set to call the "web" service "LoginGoogle" endpoint +func (c *Client) BuildLoginGoogleRequest(ctx context.Context, v any) (*http.Request, error) { + u := &url.URL{Scheme: c.scheme, Host: c.host, Path: LoginGoogleWebPath()} + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return nil, goahttp.ErrInvalidURL("web", "LoginGoogle", u.String(), err) + } + if ctx != nil { + req = req.WithContext(ctx) + } + + return req, nil +} + +// DecodeLoginGoogleResponse returns a decoder for responses returned by the +// web LoginGoogle endpoint. restoreBody controls whether the response body +// should be restored after having been read. +// DecodeLoginGoogleResponse may return the following errors: +// - "unauthorized" (type *goa.ServiceError): http.StatusUnauthorized +// - error: internal error +func DecodeLoginGoogleResponse(decoder func(*http.Response) goahttp.Decoder, restoreBody bool) func(*http.Response) (any, error) { + return func(resp *http.Response) (any, error) { + if restoreBody { + b, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + defer func() { + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + }() + } else { + defer resp.Body.Close() + } + switch resp.StatusCode { + case http.StatusFound: + var ( + redirectURL string + err error + ) + redirectURLRaw := resp.Header.Get("Location") + if redirectURLRaw == "" { + err = goa.MergeErrors(err, goa.MissingFieldError("redirect_url", "header")) + } + redirectURL = redirectURLRaw + var ( + sessionCookie string + sessionCookieRaw string + + cookies = resp.Cookies() + ) + for _, c := range cookies { + switch c.Name { + case "countup.session": + sessionCookieRaw = c.Value + } + } + if sessionCookieRaw == "" { + err = goa.MergeErrors(err, goa.MissingFieldError("session_cookie", "cookie")) + } + sessionCookie = sessionCookieRaw + if err != nil { + return nil, goahttp.ErrValidationError("web", "LoginGoogle", err) + } + res := NewLoginGoogleResultFound(redirectURL, sessionCookie) + return res, nil + case http.StatusUnauthorized: + var ( + body LoginGoogleUnauthorizedResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("web", "LoginGoogle", err) + } + err = ValidateLoginGoogleUnauthorizedResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("web", "LoginGoogle", err) + } + return nil, NewLoginGoogleUnauthorized(&body) + default: + body, _ := io.ReadAll(resp.Body) + return nil, goahttp.ErrInvalidResponse("web", "LoginGoogle", resp.StatusCode, string(body)) + } + } +} + +// BuildLoginGoogleCallbackRequest instantiates a HTTP request object with +// method and path set to call the "web" service "LoginGoogleCallback" endpoint +func (c *Client) BuildLoginGoogleCallbackRequest(ctx context.Context, v any) (*http.Request, error) { + u := &url.URL{Scheme: c.scheme, Host: c.host, Path: LoginGoogleCallbackWebPath()} + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return nil, goahttp.ErrInvalidURL("web", "LoginGoogleCallback", u.String(), err) + } + if ctx != nil { + req = req.WithContext(ctx) + } + + return req, nil +} + +// EncodeLoginGoogleCallbackRequest returns an encoder for requests sent to the +// web LoginGoogleCallback server. +func EncodeLoginGoogleCallbackRequest(encoder func(*http.Request) goahttp.Encoder) func(*http.Request, any) error { + return func(req *http.Request, v any) error { + p, ok := v.(*web.LoginGoogleCallbackPayload) + if !ok { + return goahttp.ErrInvalidType("web", "LoginGoogleCallback", "*web.LoginGoogleCallbackPayload", v) + } + { + v := p.SessionCookie + req.AddCookie(&http.Cookie{ + Name: "countup.session", + Value: v, + }) + } + values := req.URL.Query() + values.Add("code", p.Code) + values.Add("state", p.State) + req.URL.RawQuery = values.Encode() + return nil + } +} + +// DecodeLoginGoogleCallbackResponse returns a decoder for responses returned +// by the web LoginGoogleCallback endpoint. restoreBody controls whether the +// response body should be restored after having been read. +// DecodeLoginGoogleCallbackResponse may return the following errors: +// - "unauthorized" (type *goa.ServiceError): http.StatusUnauthorized +// - error: internal error +func DecodeLoginGoogleCallbackResponse(decoder func(*http.Response) goahttp.Decoder, restoreBody bool) func(*http.Response) (any, error) { + return func(resp *http.Response) (any, error) { + if restoreBody { + b, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + defer func() { + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + }() + } else { + defer resp.Body.Close() + } + switch resp.StatusCode { + case http.StatusFound: + var ( + redirectURL string + err error + ) + redirectURLRaw := resp.Header.Get("Location") + if redirectURLRaw == "" { + err = goa.MergeErrors(err, goa.MissingFieldError("redirect_url", "header")) + } + redirectURL = redirectURLRaw + var ( + sessionCookie string + sessionCookieRaw string + + cookies = resp.Cookies() + ) + for _, c := range cookies { + switch c.Name { + case "countup.session": + sessionCookieRaw = c.Value + } + } + if sessionCookieRaw == "" { + err = goa.MergeErrors(err, goa.MissingFieldError("session_cookie", "cookie")) + } + sessionCookie = sessionCookieRaw + if err != nil { + return nil, goahttp.ErrValidationError("web", "LoginGoogleCallback", err) + } + res := NewLoginGoogleCallbackResultFound(redirectURL, sessionCookie) + return res, nil + case http.StatusUnauthorized: + var ( + body LoginGoogleCallbackUnauthorizedResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("web", "LoginGoogleCallback", err) + } + err = ValidateLoginGoogleCallbackUnauthorizedResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("web", "LoginGoogleCallback", err) + } + return nil, NewLoginGoogleCallbackUnauthorized(&body) + default: + body, _ := io.ReadAll(resp.Body) + return nil, goahttp.ErrInvalidResponse("web", "LoginGoogleCallback", resp.StatusCode, string(body)) + } + } +} + +// BuildLogoutRequest instantiates a HTTP request object with method and path +// set to call the "web" service "Logout" endpoint +func (c *Client) BuildLogoutRequest(ctx context.Context, v any) (*http.Request, error) { + u := &url.URL{Scheme: c.scheme, Host: c.host, Path: LogoutWebPath()} + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return nil, goahttp.ErrInvalidURL("web", "Logout", u.String(), err) + } + if ctx != nil { + req = req.WithContext(ctx) + } + + return req, nil +} + +// EncodeLogoutRequest returns an encoder for requests sent to the web Logout +// server. +func EncodeLogoutRequest(encoder func(*http.Request) goahttp.Encoder) func(*http.Request, any) error { + return func(req *http.Request, v any) error { + p, ok := v.(*web.LogoutPayload) + if !ok { + return goahttp.ErrInvalidType("web", "Logout", "*web.LogoutPayload", v) + } + { + v := p.SessionCookie + req.AddCookie(&http.Cookie{ + Name: "countup.session", + Value: v, + }) + } + return nil + } +} + +// DecodeLogoutResponse returns a decoder for responses returned by the web +// Logout endpoint. restoreBody controls whether the response body should be +// restored after having been read. +// DecodeLogoutResponse may return the following errors: +// - "unauthorized" (type *goa.ServiceError): http.StatusUnauthorized +// - error: internal error +func DecodeLogoutResponse(decoder func(*http.Response) goahttp.Decoder, restoreBody bool) func(*http.Response) (any, error) { + return func(resp *http.Response) (any, error) { + if restoreBody { + b, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + defer func() { + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + }() + } else { + defer resp.Body.Close() + } + switch resp.StatusCode { + case http.StatusFound: + var ( + redirectURL string + err error + ) + redirectURLRaw := resp.Header.Get("Location") + if redirectURLRaw == "" { + err = goa.MergeErrors(err, goa.MissingFieldError("redirect_url", "header")) + } + redirectURL = redirectURLRaw + var ( + sessionCookie string + sessionCookieRaw string + + cookies = resp.Cookies() + ) + for _, c := range cookies { + switch c.Name { + case "countup.session": + sessionCookieRaw = c.Value + } + } + if sessionCookieRaw == "" { + err = goa.MergeErrors(err, goa.MissingFieldError("session_cookie", "cookie")) + } + sessionCookie = sessionCookieRaw + if err != nil { + return nil, goahttp.ErrValidationError("web", "Logout", err) + } + res := NewLogoutResultFound(redirectURL, sessionCookie) + return res, nil + case http.StatusUnauthorized: + var ( + body LogoutUnauthorizedResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("web", "Logout", err) + } + err = ValidateLogoutUnauthorizedResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("web", "Logout", err) + } + return nil, NewLogoutUnauthorized(&body) + default: + body, _ := io.ReadAll(resp.Body) + return nil, goahttp.ErrInvalidResponse("web", "Logout", resp.StatusCode, string(body)) + } + } +} + +// BuildSessionTokenRequest instantiates a HTTP request object with method and +// path set to call the "web" service "SessionToken" endpoint +func (c *Client) BuildSessionTokenRequest(ctx context.Context, v any) (*http.Request, error) { + u := &url.URL{Scheme: c.scheme, Host: c.host, Path: SessionTokenWebPath()} + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return nil, goahttp.ErrInvalidURL("web", "SessionToken", u.String(), err) + } + if ctx != nil { + req = req.WithContext(ctx) + } + + return req, nil +} + +// EncodeSessionTokenRequest returns an encoder for requests sent to the web +// SessionToken server. +func EncodeSessionTokenRequest(encoder func(*http.Request) goahttp.Encoder) func(*http.Request, any) error { + return func(req *http.Request, v any) error { + p, ok := v.(*web.SessionTokenPayload) + if !ok { + return goahttp.ErrInvalidType("web", "SessionToken", "*web.SessionTokenPayload", v) + } + { + v := p.SessionCookie + req.AddCookie(&http.Cookie{ + Name: "countup.session", + Value: v, + }) + } + return nil + } +} + +// DecodeSessionTokenResponse returns a decoder for responses returned by the +// web SessionToken endpoint. restoreBody controls whether the response body +// should be restored after having been read. +// DecodeSessionTokenResponse may return the following errors: +// - "unauthorized" (type *goa.ServiceError): http.StatusUnauthorized +// - error: internal error +func DecodeSessionTokenResponse(decoder func(*http.Response) goahttp.Decoder, restoreBody bool) func(*http.Response) (any, error) { + return func(resp *http.Response) (any, error) { + if restoreBody { + b, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + defer func() { + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + }() + } else { + defer resp.Body.Close() + } + switch resp.StatusCode { + case http.StatusOK: + var ( + body SessionTokenResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("web", "SessionToken", err) + } + err = ValidateSessionTokenResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("web", "SessionToken", err) + } + res := NewSessionTokenResultOK(&body) + return res, nil + case http.StatusUnauthorized: + var ( + body SessionTokenUnauthorizedResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("web", "SessionToken", err) + } + err = ValidateSessionTokenUnauthorizedResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("web", "SessionToken", err) + } + return nil, NewSessionTokenUnauthorized(&body) + default: + body, _ := io.ReadAll(resp.Body) + return nil, goahttp.ErrInvalidResponse("web", "SessionToken", resp.StatusCode, string(body)) + } + } +} diff --git a/app/api/v1/gen/http/web/client/paths.go b/app/api/v1/gen/http/web/client/paths.go new file mode 100644 index 0000000..1a563cf --- /dev/null +++ b/app/api/v1/gen/http/web/client/paths.go @@ -0,0 +1,38 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// HTTP request path constructors for the web service. +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package client + +// IndexWebPath returns the URL path to the web service Index HTTP endpoint. +func IndexWebPath() string { + return "/" +} + +// AnotherWebPath returns the URL path to the web service Another HTTP endpoint. +func AnotherWebPath() string { + return "/another" +} + +// LoginGoogleWebPath returns the URL path to the web service LoginGoogle HTTP endpoint. +func LoginGoogleWebPath() string { + return "/login/google" +} + +// LoginGoogleCallbackWebPath returns the URL path to the web service LoginGoogleCallback HTTP endpoint. +func LoginGoogleCallbackWebPath() string { + return "/login/google/callback" +} + +// LogoutWebPath returns the URL path to the web service Logout HTTP endpoint. +func LogoutWebPath() string { + return "/logout" +} + +// SessionTokenWebPath returns the URL path to the web service SessionToken HTTP endpoint. +func SessionTokenWebPath() string { + return "/session/token" +} diff --git a/app/api/v1/gen/http/web/client/types.go b/app/api/v1/gen/http/web/client/types.go new file mode 100644 index 0000000..3fda6cc --- /dev/null +++ b/app/api/v1/gen/http/web/client/types.go @@ -0,0 +1,410 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// web HTTP client types +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package client + +import ( + web "github.com/jace-ys/countup/api/v1/gen/web" + goa "goa.design/goa/v3/pkg" +) + +// SessionTokenResponseBody is the type of the "web" service "SessionToken" +// endpoint HTTP response body. +type SessionTokenResponseBody struct { + Token *string `form:"token,omitempty" json:"token,omitempty" xml:"token,omitempty"` +} + +// IndexUnauthorizedResponseBody is the type of the "web" service "Index" +// endpoint HTTP response body for the "unauthorized" error. +type IndexUnauthorizedResponseBody struct { + // Name is the name of this class of errors. + Name *string `form:"name,omitempty" json:"name,omitempty" xml:"name,omitempty"` + // ID is a unique identifier for this particular occurrence of the problem. + ID *string `form:"id,omitempty" json:"id,omitempty" xml:"id,omitempty"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` + // Is the error temporary? + Temporary *bool `form:"temporary,omitempty" json:"temporary,omitempty" xml:"temporary,omitempty"` + // Is the error a timeout? + Timeout *bool `form:"timeout,omitempty" json:"timeout,omitempty" xml:"timeout,omitempty"` + // Is the error a server-side fault? + Fault *bool `form:"fault,omitempty" json:"fault,omitempty" xml:"fault,omitempty"` +} + +// AnotherUnauthorizedResponseBody is the type of the "web" service "Another" +// endpoint HTTP response body for the "unauthorized" error. +type AnotherUnauthorizedResponseBody struct { + // Name is the name of this class of errors. + Name *string `form:"name,omitempty" json:"name,omitempty" xml:"name,omitempty"` + // ID is a unique identifier for this particular occurrence of the problem. + ID *string `form:"id,omitempty" json:"id,omitempty" xml:"id,omitempty"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` + // Is the error temporary? + Temporary *bool `form:"temporary,omitempty" json:"temporary,omitempty" xml:"temporary,omitempty"` + // Is the error a timeout? + Timeout *bool `form:"timeout,omitempty" json:"timeout,omitempty" xml:"timeout,omitempty"` + // Is the error a server-side fault? + Fault *bool `form:"fault,omitempty" json:"fault,omitempty" xml:"fault,omitempty"` +} + +// LoginGoogleUnauthorizedResponseBody is the type of the "web" service +// "LoginGoogle" endpoint HTTP response body for the "unauthorized" error. +type LoginGoogleUnauthorizedResponseBody struct { + // Name is the name of this class of errors. + Name *string `form:"name,omitempty" json:"name,omitempty" xml:"name,omitempty"` + // ID is a unique identifier for this particular occurrence of the problem. + ID *string `form:"id,omitempty" json:"id,omitempty" xml:"id,omitempty"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` + // Is the error temporary? + Temporary *bool `form:"temporary,omitempty" json:"temporary,omitempty" xml:"temporary,omitempty"` + // Is the error a timeout? + Timeout *bool `form:"timeout,omitempty" json:"timeout,omitempty" xml:"timeout,omitempty"` + // Is the error a server-side fault? + Fault *bool `form:"fault,omitempty" json:"fault,omitempty" xml:"fault,omitempty"` +} + +// LoginGoogleCallbackUnauthorizedResponseBody is the type of the "web" service +// "LoginGoogleCallback" endpoint HTTP response body for the "unauthorized" +// error. +type LoginGoogleCallbackUnauthorizedResponseBody struct { + // Name is the name of this class of errors. + Name *string `form:"name,omitempty" json:"name,omitempty" xml:"name,omitempty"` + // ID is a unique identifier for this particular occurrence of the problem. + ID *string `form:"id,omitempty" json:"id,omitempty" xml:"id,omitempty"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` + // Is the error temporary? + Temporary *bool `form:"temporary,omitempty" json:"temporary,omitempty" xml:"temporary,omitempty"` + // Is the error a timeout? + Timeout *bool `form:"timeout,omitempty" json:"timeout,omitempty" xml:"timeout,omitempty"` + // Is the error a server-side fault? + Fault *bool `form:"fault,omitempty" json:"fault,omitempty" xml:"fault,omitempty"` +} + +// LogoutUnauthorizedResponseBody is the type of the "web" service "Logout" +// endpoint HTTP response body for the "unauthorized" error. +type LogoutUnauthorizedResponseBody struct { + // Name is the name of this class of errors. + Name *string `form:"name,omitempty" json:"name,omitempty" xml:"name,omitempty"` + // ID is a unique identifier for this particular occurrence of the problem. + ID *string `form:"id,omitempty" json:"id,omitempty" xml:"id,omitempty"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` + // Is the error temporary? + Temporary *bool `form:"temporary,omitempty" json:"temporary,omitempty" xml:"temporary,omitempty"` + // Is the error a timeout? + Timeout *bool `form:"timeout,omitempty" json:"timeout,omitempty" xml:"timeout,omitempty"` + // Is the error a server-side fault? + Fault *bool `form:"fault,omitempty" json:"fault,omitempty" xml:"fault,omitempty"` +} + +// SessionTokenUnauthorizedResponseBody is the type of the "web" service +// "SessionToken" endpoint HTTP response body for the "unauthorized" error. +type SessionTokenUnauthorizedResponseBody struct { + // Name is the name of this class of errors. + Name *string `form:"name,omitempty" json:"name,omitempty" xml:"name,omitempty"` + // ID is a unique identifier for this particular occurrence of the problem. + ID *string `form:"id,omitempty" json:"id,omitempty" xml:"id,omitempty"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` + // Is the error temporary? + Temporary *bool `form:"temporary,omitempty" json:"temporary,omitempty" xml:"temporary,omitempty"` + // Is the error a timeout? + Timeout *bool `form:"timeout,omitempty" json:"timeout,omitempty" xml:"timeout,omitempty"` + // Is the error a server-side fault? + Fault *bool `form:"fault,omitempty" json:"fault,omitempty" xml:"fault,omitempty"` +} + +// NewIndexUnauthorized builds a web service Index endpoint unauthorized error. +func NewIndexUnauthorized(body *IndexUnauthorizedResponseBody) *goa.ServiceError { + v := &goa.ServiceError{ + Name: *body.Name, + ID: *body.ID, + Message: *body.Message, + Temporary: *body.Temporary, + Timeout: *body.Timeout, + Fault: *body.Fault, + } + + return v +} + +// NewAnotherUnauthorized builds a web service Another endpoint unauthorized +// error. +func NewAnotherUnauthorized(body *AnotherUnauthorizedResponseBody) *goa.ServiceError { + v := &goa.ServiceError{ + Name: *body.Name, + ID: *body.ID, + Message: *body.Message, + Temporary: *body.Temporary, + Timeout: *body.Timeout, + Fault: *body.Fault, + } + + return v +} + +// NewLoginGoogleResultFound builds a "web" service "LoginGoogle" endpoint +// result from a HTTP "Found" response. +func NewLoginGoogleResultFound(redirectURL string, sessionCookie string) *web.LoginGoogleResult { + v := &web.LoginGoogleResult{} + v.RedirectURL = redirectURL + v.SessionCookie = sessionCookie + + return v +} + +// NewLoginGoogleUnauthorized builds a web service LoginGoogle endpoint +// unauthorized error. +func NewLoginGoogleUnauthorized(body *LoginGoogleUnauthorizedResponseBody) *goa.ServiceError { + v := &goa.ServiceError{ + Name: *body.Name, + ID: *body.ID, + Message: *body.Message, + Temporary: *body.Temporary, + Timeout: *body.Timeout, + Fault: *body.Fault, + } + + return v +} + +// NewLoginGoogleCallbackResultFound builds a "web" service +// "LoginGoogleCallback" endpoint result from a HTTP "Found" response. +func NewLoginGoogleCallbackResultFound(redirectURL string, sessionCookie string) *web.LoginGoogleCallbackResult { + v := &web.LoginGoogleCallbackResult{} + v.RedirectURL = redirectURL + v.SessionCookie = sessionCookie + + return v +} + +// NewLoginGoogleCallbackUnauthorized builds a web service LoginGoogleCallback +// endpoint unauthorized error. +func NewLoginGoogleCallbackUnauthorized(body *LoginGoogleCallbackUnauthorizedResponseBody) *goa.ServiceError { + v := &goa.ServiceError{ + Name: *body.Name, + ID: *body.ID, + Message: *body.Message, + Temporary: *body.Temporary, + Timeout: *body.Timeout, + Fault: *body.Fault, + } + + return v +} + +// NewLogoutResultFound builds a "web" service "Logout" endpoint result from a +// HTTP "Found" response. +func NewLogoutResultFound(redirectURL string, sessionCookie string) *web.LogoutResult { + v := &web.LogoutResult{} + v.RedirectURL = redirectURL + v.SessionCookie = sessionCookie + + return v +} + +// NewLogoutUnauthorized builds a web service Logout endpoint unauthorized +// error. +func NewLogoutUnauthorized(body *LogoutUnauthorizedResponseBody) *goa.ServiceError { + v := &goa.ServiceError{ + Name: *body.Name, + ID: *body.ID, + Message: *body.Message, + Temporary: *body.Temporary, + Timeout: *body.Timeout, + Fault: *body.Fault, + } + + return v +} + +// NewSessionTokenResultOK builds a "web" service "SessionToken" endpoint +// result from a HTTP "OK" response. +func NewSessionTokenResultOK(body *SessionTokenResponseBody) *web.SessionTokenResult { + v := &web.SessionTokenResult{ + Token: *body.Token, + } + + return v +} + +// NewSessionTokenUnauthorized builds a web service SessionToken endpoint +// unauthorized error. +func NewSessionTokenUnauthorized(body *SessionTokenUnauthorizedResponseBody) *goa.ServiceError { + v := &goa.ServiceError{ + Name: *body.Name, + ID: *body.ID, + Message: *body.Message, + Temporary: *body.Temporary, + Timeout: *body.Timeout, + Fault: *body.Fault, + } + + return v +} + +// ValidateSessionTokenResponseBody runs the validations defined on +// SessionTokenResponseBody +func ValidateSessionTokenResponseBody(body *SessionTokenResponseBody) (err error) { + if body.Token == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("token", "body")) + } + return +} + +// ValidateIndexUnauthorizedResponseBody runs the validations defined on +// Index_unauthorized_Response_Body +func ValidateIndexUnauthorizedResponseBody(body *IndexUnauthorizedResponseBody) (err error) { + if body.Name == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("name", "body")) + } + if body.ID == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("id", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + if body.Temporary == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("temporary", "body")) + } + if body.Timeout == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("timeout", "body")) + } + if body.Fault == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("fault", "body")) + } + return +} + +// ValidateAnotherUnauthorizedResponseBody runs the validations defined on +// Another_unauthorized_Response_Body +func ValidateAnotherUnauthorizedResponseBody(body *AnotherUnauthorizedResponseBody) (err error) { + if body.Name == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("name", "body")) + } + if body.ID == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("id", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + if body.Temporary == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("temporary", "body")) + } + if body.Timeout == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("timeout", "body")) + } + if body.Fault == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("fault", "body")) + } + return +} + +// ValidateLoginGoogleUnauthorizedResponseBody runs the validations defined on +// LoginGoogle_unauthorized_Response_Body +func ValidateLoginGoogleUnauthorizedResponseBody(body *LoginGoogleUnauthorizedResponseBody) (err error) { + if body.Name == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("name", "body")) + } + if body.ID == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("id", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + if body.Temporary == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("temporary", "body")) + } + if body.Timeout == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("timeout", "body")) + } + if body.Fault == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("fault", "body")) + } + return +} + +// ValidateLoginGoogleCallbackUnauthorizedResponseBody runs the validations +// defined on LoginGoogleCallback_unauthorized_Response_Body +func ValidateLoginGoogleCallbackUnauthorizedResponseBody(body *LoginGoogleCallbackUnauthorizedResponseBody) (err error) { + if body.Name == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("name", "body")) + } + if body.ID == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("id", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + if body.Temporary == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("temporary", "body")) + } + if body.Timeout == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("timeout", "body")) + } + if body.Fault == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("fault", "body")) + } + return +} + +// ValidateLogoutUnauthorizedResponseBody runs the validations defined on +// Logout_unauthorized_Response_Body +func ValidateLogoutUnauthorizedResponseBody(body *LogoutUnauthorizedResponseBody) (err error) { + if body.Name == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("name", "body")) + } + if body.ID == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("id", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + if body.Temporary == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("temporary", "body")) + } + if body.Timeout == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("timeout", "body")) + } + if body.Fault == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("fault", "body")) + } + return +} + +// ValidateSessionTokenUnauthorizedResponseBody runs the validations defined on +// SessionToken_unauthorized_Response_Body +func ValidateSessionTokenUnauthorizedResponseBody(body *SessionTokenUnauthorizedResponseBody) (err error) { + if body.Name == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("name", "body")) + } + if body.ID == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("id", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + if body.Temporary == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("temporary", "body")) + } + if body.Timeout == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("timeout", "body")) + } + if body.Fault == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("fault", "body")) + } + return +} diff --git a/app/api/v1/gen/http/web/server/encode_decode.go b/app/api/v1/gen/http/web/server/encode_decode.go new file mode 100644 index 0000000..8b2bcd3 --- /dev/null +++ b/app/api/v1/gen/http/web/server/encode_decode.go @@ -0,0 +1,377 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// web HTTP server encoders and decoders +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package server + +import ( + "context" + "errors" + "net/http" + + web "github.com/jace-ys/countup/api/v1/gen/web" + goahttp "goa.design/goa/v3/http" + goa "goa.design/goa/v3/pkg" +) + +// EncodeIndexResponse returns an encoder for responses returned by the web +// Index endpoint. +func EncodeIndexResponse(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder) func(context.Context, http.ResponseWriter, any) error { + return func(ctx context.Context, w http.ResponseWriter, v any) error { + res, _ := v.([]byte) + ctx = context.WithValue(ctx, goahttp.ContentTypeKey, "text/html") + enc := encoder(ctx, w) + body := res + w.WriteHeader(http.StatusOK) + return enc.Encode(body) + } +} + +// EncodeIndexError returns an encoder for errors returned by the Index web +// endpoint. +func EncodeIndexError(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, formatter func(ctx context.Context, err error) goahttp.Statuser) func(context.Context, http.ResponseWriter, error) error { + encodeError := goahttp.ErrorEncoder(encoder, formatter) + return func(ctx context.Context, w http.ResponseWriter, v error) error { + var en goa.GoaErrorNamer + if !errors.As(v, &en) { + return encodeError(ctx, w, v) + } + switch en.GoaErrorName() { + case "unauthorized": + var res *goa.ServiceError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewIndexUnauthorizedResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusUnauthorized) + return enc.Encode(body) + default: + return encodeError(ctx, w, v) + } + } +} + +// EncodeAnotherResponse returns an encoder for responses returned by the web +// Another endpoint. +func EncodeAnotherResponse(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder) func(context.Context, http.ResponseWriter, any) error { + return func(ctx context.Context, w http.ResponseWriter, v any) error { + res, _ := v.([]byte) + ctx = context.WithValue(ctx, goahttp.ContentTypeKey, "text/html") + enc := encoder(ctx, w) + body := res + w.WriteHeader(http.StatusOK) + return enc.Encode(body) + } +} + +// EncodeAnotherError returns an encoder for errors returned by the Another web +// endpoint. +func EncodeAnotherError(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, formatter func(ctx context.Context, err error) goahttp.Statuser) func(context.Context, http.ResponseWriter, error) error { + encodeError := goahttp.ErrorEncoder(encoder, formatter) + return func(ctx context.Context, w http.ResponseWriter, v error) error { + var en goa.GoaErrorNamer + if !errors.As(v, &en) { + return encodeError(ctx, w, v) + } + switch en.GoaErrorName() { + case "unauthorized": + var res *goa.ServiceError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewAnotherUnauthorizedResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusUnauthorized) + return enc.Encode(body) + default: + return encodeError(ctx, w, v) + } + } +} + +// EncodeLoginGoogleResponse returns an encoder for responses returned by the +// web LoginGoogle endpoint. +func EncodeLoginGoogleResponse(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder) func(context.Context, http.ResponseWriter, any) error { + return func(ctx context.Context, w http.ResponseWriter, v any) error { + res, _ := v.(*web.LoginGoogleResult) + w.Header().Set("Location", res.RedirectURL) + sessionCookie := res.SessionCookie + http.SetCookie(w, &http.Cookie{ + Name: "countup.session", + Value: sessionCookie, + MaxAge: 86400, + Path: "/", + Secure: true, + HttpOnly: true, + SameSite: http.SameSiteLaxMode, + }) + w.WriteHeader(http.StatusFound) + return nil + } +} + +// EncodeLoginGoogleError returns an encoder for errors returned by the +// LoginGoogle web endpoint. +func EncodeLoginGoogleError(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, formatter func(ctx context.Context, err error) goahttp.Statuser) func(context.Context, http.ResponseWriter, error) error { + encodeError := goahttp.ErrorEncoder(encoder, formatter) + return func(ctx context.Context, w http.ResponseWriter, v error) error { + var en goa.GoaErrorNamer + if !errors.As(v, &en) { + return encodeError(ctx, w, v) + } + switch en.GoaErrorName() { + case "unauthorized": + var res *goa.ServiceError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewLoginGoogleUnauthorizedResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusUnauthorized) + return enc.Encode(body) + default: + return encodeError(ctx, w, v) + } + } +} + +// EncodeLoginGoogleCallbackResponse returns an encoder for responses returned +// by the web LoginGoogleCallback endpoint. +func EncodeLoginGoogleCallbackResponse(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder) func(context.Context, http.ResponseWriter, any) error { + return func(ctx context.Context, w http.ResponseWriter, v any) error { + res, _ := v.(*web.LoginGoogleCallbackResult) + w.Header().Set("Location", res.RedirectURL) + sessionCookie := res.SessionCookie + http.SetCookie(w, &http.Cookie{ + Name: "countup.session", + Value: sessionCookie, + MaxAge: 86400, + Path: "/", + Secure: true, + HttpOnly: true, + SameSite: http.SameSiteLaxMode, + }) + w.WriteHeader(http.StatusFound) + return nil + } +} + +// DecodeLoginGoogleCallbackRequest returns a decoder for requests sent to the +// web LoginGoogleCallback endpoint. +func DecodeLoginGoogleCallbackRequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp.Decoder) func(*http.Request) (any, error) { + return func(r *http.Request) (any, error) { + var ( + code string + state string + sessionCookie string + err error + c *http.Cookie + ) + qp := r.URL.Query() + code = qp.Get("code") + if code == "" { + err = goa.MergeErrors(err, goa.MissingFieldError("code", "query string")) + } + state = qp.Get("state") + if state == "" { + err = goa.MergeErrors(err, goa.MissingFieldError("state", "query string")) + } + c, err = r.Cookie("countup.session") + if err == http.ErrNoCookie { + err = goa.MergeErrors(err, goa.MissingFieldError("session_cookie", "cookie")) + } else { + sessionCookie = c.Value + } + if err != nil { + return nil, err + } + payload := NewLoginGoogleCallbackPayload(code, state, sessionCookie) + + return payload, nil + } +} + +// EncodeLoginGoogleCallbackError returns an encoder for errors returned by the +// LoginGoogleCallback web endpoint. +func EncodeLoginGoogleCallbackError(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, formatter func(ctx context.Context, err error) goahttp.Statuser) func(context.Context, http.ResponseWriter, error) error { + encodeError := goahttp.ErrorEncoder(encoder, formatter) + return func(ctx context.Context, w http.ResponseWriter, v error) error { + var en goa.GoaErrorNamer + if !errors.As(v, &en) { + return encodeError(ctx, w, v) + } + switch en.GoaErrorName() { + case "unauthorized": + var res *goa.ServiceError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewLoginGoogleCallbackUnauthorizedResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusUnauthorized) + return enc.Encode(body) + default: + return encodeError(ctx, w, v) + } + } +} + +// EncodeLogoutResponse returns an encoder for responses returned by the web +// Logout endpoint. +func EncodeLogoutResponse(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder) func(context.Context, http.ResponseWriter, any) error { + return func(ctx context.Context, w http.ResponseWriter, v any) error { + res, _ := v.(*web.LogoutResult) + w.Header().Set("Location", res.RedirectURL) + sessionCookie := res.SessionCookie + http.SetCookie(w, &http.Cookie{ + Name: "countup.session", + Value: sessionCookie, + MaxAge: 86400, + Path: "/", + Secure: true, + HttpOnly: true, + SameSite: http.SameSiteLaxMode, + }) + w.WriteHeader(http.StatusFound) + return nil + } +} + +// DecodeLogoutRequest returns a decoder for requests sent to the web Logout +// endpoint. +func DecodeLogoutRequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp.Decoder) func(*http.Request) (any, error) { + return func(r *http.Request) (any, error) { + var ( + sessionCookie string + err error + c *http.Cookie + ) + c, err = r.Cookie("countup.session") + if err == http.ErrNoCookie { + err = goa.MergeErrors(err, goa.MissingFieldError("session_cookie", "cookie")) + } else { + sessionCookie = c.Value + } + if err != nil { + return nil, err + } + payload := NewLogoutPayload(sessionCookie) + + return payload, nil + } +} + +// EncodeLogoutError returns an encoder for errors returned by the Logout web +// endpoint. +func EncodeLogoutError(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, formatter func(ctx context.Context, err error) goahttp.Statuser) func(context.Context, http.ResponseWriter, error) error { + encodeError := goahttp.ErrorEncoder(encoder, formatter) + return func(ctx context.Context, w http.ResponseWriter, v error) error { + var en goa.GoaErrorNamer + if !errors.As(v, &en) { + return encodeError(ctx, w, v) + } + switch en.GoaErrorName() { + case "unauthorized": + var res *goa.ServiceError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewLogoutUnauthorizedResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusUnauthorized) + return enc.Encode(body) + default: + return encodeError(ctx, w, v) + } + } +} + +// EncodeSessionTokenResponse returns an encoder for responses returned by the +// web SessionToken endpoint. +func EncodeSessionTokenResponse(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder) func(context.Context, http.ResponseWriter, any) error { + return func(ctx context.Context, w http.ResponseWriter, v any) error { + res, _ := v.(*web.SessionTokenResult) + ctx = context.WithValue(ctx, goahttp.ContentTypeKey, "application/json") + enc := encoder(ctx, w) + body := NewSessionTokenResponseBody(res) + w.WriteHeader(http.StatusOK) + return enc.Encode(body) + } +} + +// DecodeSessionTokenRequest returns a decoder for requests sent to the web +// SessionToken endpoint. +func DecodeSessionTokenRequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp.Decoder) func(*http.Request) (any, error) { + return func(r *http.Request) (any, error) { + var ( + sessionCookie string + err error + c *http.Cookie + ) + c, err = r.Cookie("countup.session") + if err == http.ErrNoCookie { + err = goa.MergeErrors(err, goa.MissingFieldError("session_cookie", "cookie")) + } else { + sessionCookie = c.Value + } + if err != nil { + return nil, err + } + payload := NewSessionTokenPayload(sessionCookie) + + return payload, nil + } +} + +// EncodeSessionTokenError returns an encoder for errors returned by the +// SessionToken web endpoint. +func EncodeSessionTokenError(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, formatter func(ctx context.Context, err error) goahttp.Statuser) func(context.Context, http.ResponseWriter, error) error { + encodeError := goahttp.ErrorEncoder(encoder, formatter) + return func(ctx context.Context, w http.ResponseWriter, v error) error { + var en goa.GoaErrorNamer + if !errors.As(v, &en) { + return encodeError(ctx, w, v) + } + switch en.GoaErrorName() { + case "unauthorized": + var res *goa.ServiceError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewSessionTokenUnauthorizedResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusUnauthorized) + return enc.Encode(body) + default: + return encodeError(ctx, w, v) + } + } +} diff --git a/app/api/v1/gen/http/web/server/paths.go b/app/api/v1/gen/http/web/server/paths.go new file mode 100644 index 0000000..ee25941 --- /dev/null +++ b/app/api/v1/gen/http/web/server/paths.go @@ -0,0 +1,38 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// HTTP request path constructors for the web service. +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package server + +// IndexWebPath returns the URL path to the web service Index HTTP endpoint. +func IndexWebPath() string { + return "/" +} + +// AnotherWebPath returns the URL path to the web service Another HTTP endpoint. +func AnotherWebPath() string { + return "/another" +} + +// LoginGoogleWebPath returns the URL path to the web service LoginGoogle HTTP endpoint. +func LoginGoogleWebPath() string { + return "/login/google" +} + +// LoginGoogleCallbackWebPath returns the URL path to the web service LoginGoogleCallback HTTP endpoint. +func LoginGoogleCallbackWebPath() string { + return "/login/google/callback" +} + +// LogoutWebPath returns the URL path to the web service Logout HTTP endpoint. +func LogoutWebPath() string { + return "/logout" +} + +// SessionTokenWebPath returns the URL path to the web service SessionToken HTTP endpoint. +func SessionTokenWebPath() string { + return "/session/token" +} diff --git a/app/api/v1/gen/http/web/server/server.go b/app/api/v1/gen/http/web/server/server.go new file mode 100644 index 0000000..94cb90a --- /dev/null +++ b/app/api/v1/gen/http/web/server/server.go @@ -0,0 +1,425 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// web HTTP server +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package server + +import ( + "context" + "net/http" + "path" + + web "github.com/jace-ys/countup/api/v1/gen/web" + goahttp "goa.design/goa/v3/http" + goa "goa.design/goa/v3/pkg" +) + +// Server lists the web service endpoint HTTP handlers. +type Server struct { + Mounts []*MountPoint + Index http.Handler + Another http.Handler + LoginGoogle http.Handler + LoginGoogleCallback http.Handler + Logout http.Handler + SessionToken http.Handler + Static http.Handler +} + +// MountPoint holds information about the mounted endpoints. +type MountPoint struct { + // Method is the name of the service method served by the mounted HTTP handler. + Method string + // Verb is the HTTP method used to match requests to the mounted handler. + Verb string + // Pattern is the HTTP request path pattern used to match requests to the + // mounted handler. + Pattern string +} + +// New instantiates HTTP handlers for all the web service endpoints using the +// provided encoder and decoder. The handlers are mounted on the given mux +// using the HTTP verb and path defined in the design. errhandler is called +// whenever a response fails to be encoded. formatter is used to format errors +// returned by the service methods prior to encoding. Both errhandler and +// formatter are optional and can be nil. +func New( + e *web.Endpoints, + mux goahttp.Muxer, + decoder func(*http.Request) goahttp.Decoder, + encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, + errhandler func(context.Context, http.ResponseWriter, error), + formatter func(ctx context.Context, err error) goahttp.Statuser, + fileSystemStatic http.FileSystem, +) *Server { + if fileSystemStatic == nil { + fileSystemStatic = http.Dir(".") + } + fileSystemStatic = appendPrefix(fileSystemStatic, "/static") + return &Server{ + Mounts: []*MountPoint{ + {"Index", "GET", "/"}, + {"Another", "GET", "/another"}, + {"LoginGoogle", "GET", "/login/google"}, + {"LoginGoogleCallback", "GET", "/login/google/callback"}, + {"Logout", "GET", "/logout"}, + {"SessionToken", "GET", "/session/token"}, + {"Serve static/", "GET", "/static/*"}, + }, + Index: NewIndexHandler(e.Index, mux, decoder, encoder, errhandler, formatter), + Another: NewAnotherHandler(e.Another, mux, decoder, encoder, errhandler, formatter), + LoginGoogle: NewLoginGoogleHandler(e.LoginGoogle, mux, decoder, encoder, errhandler, formatter), + LoginGoogleCallback: NewLoginGoogleCallbackHandler(e.LoginGoogleCallback, mux, decoder, encoder, errhandler, formatter), + Logout: NewLogoutHandler(e.Logout, mux, decoder, encoder, errhandler, formatter), + SessionToken: NewSessionTokenHandler(e.SessionToken, mux, decoder, encoder, errhandler, formatter), + Static: http.FileServer(fileSystemStatic), + } +} + +// Service returns the name of the service served. +func (s *Server) Service() string { return "web" } + +// Use wraps the server handlers with the given middleware. +func (s *Server) Use(m func(http.Handler) http.Handler) { + s.Index = m(s.Index) + s.Another = m(s.Another) + s.LoginGoogle = m(s.LoginGoogle) + s.LoginGoogleCallback = m(s.LoginGoogleCallback) + s.Logout = m(s.Logout) + s.SessionToken = m(s.SessionToken) +} + +// MethodNames returns the methods served. +func (s *Server) MethodNames() []string { return web.MethodNames[:] } + +// Mount configures the mux to serve the web endpoints. +func Mount(mux goahttp.Muxer, h *Server) { + MountIndexHandler(mux, h.Index) + MountAnotherHandler(mux, h.Another) + MountLoginGoogleHandler(mux, h.LoginGoogle) + MountLoginGoogleCallbackHandler(mux, h.LoginGoogleCallback) + MountLogoutHandler(mux, h.Logout) + MountSessionTokenHandler(mux, h.SessionToken) + MountStatic(mux, http.StripPrefix("/static", h.Static)) +} + +// Mount configures the mux to serve the web endpoints. +func (s *Server) Mount(mux goahttp.Muxer) { + Mount(mux, s) +} + +// MountIndexHandler configures the mux to serve the "web" service "Index" +// endpoint. +func MountIndexHandler(mux goahttp.Muxer, h http.Handler) { + f, ok := h.(http.HandlerFunc) + if !ok { + f = func(w http.ResponseWriter, r *http.Request) { + h.ServeHTTP(w, r) + } + } + mux.Handle("GET", "/", f) +} + +// NewIndexHandler creates a HTTP handler which loads the HTTP request and +// calls the "web" service "Index" endpoint. +func NewIndexHandler( + endpoint goa.Endpoint, + mux goahttp.Muxer, + decoder func(*http.Request) goahttp.Decoder, + encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, + errhandler func(context.Context, http.ResponseWriter, error), + formatter func(ctx context.Context, err error) goahttp.Statuser, +) http.Handler { + var ( + encodeResponse = EncodeIndexResponse(encoder) + encodeError = EncodeIndexError(encoder, formatter) + ) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), goahttp.AcceptTypeKey, r.Header.Get("Accept")) + ctx = context.WithValue(ctx, goa.MethodKey, "Index") + ctx = context.WithValue(ctx, goa.ServiceKey, "web") + var err error + res, err := endpoint(ctx, nil) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + if err := encodeResponse(ctx, w, res); err != nil { + errhandler(ctx, w, err) + } + }) +} + +// MountAnotherHandler configures the mux to serve the "web" service "Another" +// endpoint. +func MountAnotherHandler(mux goahttp.Muxer, h http.Handler) { + f, ok := h.(http.HandlerFunc) + if !ok { + f = func(w http.ResponseWriter, r *http.Request) { + h.ServeHTTP(w, r) + } + } + mux.Handle("GET", "/another", f) +} + +// NewAnotherHandler creates a HTTP handler which loads the HTTP request and +// calls the "web" service "Another" endpoint. +func NewAnotherHandler( + endpoint goa.Endpoint, + mux goahttp.Muxer, + decoder func(*http.Request) goahttp.Decoder, + encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, + errhandler func(context.Context, http.ResponseWriter, error), + formatter func(ctx context.Context, err error) goahttp.Statuser, +) http.Handler { + var ( + encodeResponse = EncodeAnotherResponse(encoder) + encodeError = EncodeAnotherError(encoder, formatter) + ) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), goahttp.AcceptTypeKey, r.Header.Get("Accept")) + ctx = context.WithValue(ctx, goa.MethodKey, "Another") + ctx = context.WithValue(ctx, goa.ServiceKey, "web") + var err error + res, err := endpoint(ctx, nil) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + if err := encodeResponse(ctx, w, res); err != nil { + errhandler(ctx, w, err) + } + }) +} + +// MountLoginGoogleHandler configures the mux to serve the "web" service +// "LoginGoogle" endpoint. +func MountLoginGoogleHandler(mux goahttp.Muxer, h http.Handler) { + f, ok := h.(http.HandlerFunc) + if !ok { + f = func(w http.ResponseWriter, r *http.Request) { + h.ServeHTTP(w, r) + } + } + mux.Handle("GET", "/login/google", f) +} + +// NewLoginGoogleHandler creates a HTTP handler which loads the HTTP request +// and calls the "web" service "LoginGoogle" endpoint. +func NewLoginGoogleHandler( + endpoint goa.Endpoint, + mux goahttp.Muxer, + decoder func(*http.Request) goahttp.Decoder, + encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, + errhandler func(context.Context, http.ResponseWriter, error), + formatter func(ctx context.Context, err error) goahttp.Statuser, +) http.Handler { + var ( + encodeResponse = EncodeLoginGoogleResponse(encoder) + encodeError = EncodeLoginGoogleError(encoder, formatter) + ) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), goahttp.AcceptTypeKey, r.Header.Get("Accept")) + ctx = context.WithValue(ctx, goa.MethodKey, "LoginGoogle") + ctx = context.WithValue(ctx, goa.ServiceKey, "web") + var err error + res, err := endpoint(ctx, nil) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + if err := encodeResponse(ctx, w, res); err != nil { + errhandler(ctx, w, err) + } + }) +} + +// MountLoginGoogleCallbackHandler configures the mux to serve the "web" +// service "LoginGoogleCallback" endpoint. +func MountLoginGoogleCallbackHandler(mux goahttp.Muxer, h http.Handler) { + f, ok := h.(http.HandlerFunc) + if !ok { + f = func(w http.ResponseWriter, r *http.Request) { + h.ServeHTTP(w, r) + } + } + mux.Handle("GET", "/login/google/callback", f) +} + +// NewLoginGoogleCallbackHandler creates a HTTP handler which loads the HTTP +// request and calls the "web" service "LoginGoogleCallback" endpoint. +func NewLoginGoogleCallbackHandler( + endpoint goa.Endpoint, + mux goahttp.Muxer, + decoder func(*http.Request) goahttp.Decoder, + encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, + errhandler func(context.Context, http.ResponseWriter, error), + formatter func(ctx context.Context, err error) goahttp.Statuser, +) http.Handler { + var ( + decodeRequest = DecodeLoginGoogleCallbackRequest(mux, decoder) + encodeResponse = EncodeLoginGoogleCallbackResponse(encoder) + encodeError = EncodeLoginGoogleCallbackError(encoder, formatter) + ) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), goahttp.AcceptTypeKey, r.Header.Get("Accept")) + ctx = context.WithValue(ctx, goa.MethodKey, "LoginGoogleCallback") + ctx = context.WithValue(ctx, goa.ServiceKey, "web") + payload, err := decodeRequest(r) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + res, err := endpoint(ctx, payload) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + if err := encodeResponse(ctx, w, res); err != nil { + errhandler(ctx, w, err) + } + }) +} + +// MountLogoutHandler configures the mux to serve the "web" service "Logout" +// endpoint. +func MountLogoutHandler(mux goahttp.Muxer, h http.Handler) { + f, ok := h.(http.HandlerFunc) + if !ok { + f = func(w http.ResponseWriter, r *http.Request) { + h.ServeHTTP(w, r) + } + } + mux.Handle("GET", "/logout", f) +} + +// NewLogoutHandler creates a HTTP handler which loads the HTTP request and +// calls the "web" service "Logout" endpoint. +func NewLogoutHandler( + endpoint goa.Endpoint, + mux goahttp.Muxer, + decoder func(*http.Request) goahttp.Decoder, + encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, + errhandler func(context.Context, http.ResponseWriter, error), + formatter func(ctx context.Context, err error) goahttp.Statuser, +) http.Handler { + var ( + decodeRequest = DecodeLogoutRequest(mux, decoder) + encodeResponse = EncodeLogoutResponse(encoder) + encodeError = EncodeLogoutError(encoder, formatter) + ) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), goahttp.AcceptTypeKey, r.Header.Get("Accept")) + ctx = context.WithValue(ctx, goa.MethodKey, "Logout") + ctx = context.WithValue(ctx, goa.ServiceKey, "web") + payload, err := decodeRequest(r) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + res, err := endpoint(ctx, payload) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + if err := encodeResponse(ctx, w, res); err != nil { + errhandler(ctx, w, err) + } + }) +} + +// MountSessionTokenHandler configures the mux to serve the "web" service +// "SessionToken" endpoint. +func MountSessionTokenHandler(mux goahttp.Muxer, h http.Handler) { + f, ok := h.(http.HandlerFunc) + if !ok { + f = func(w http.ResponseWriter, r *http.Request) { + h.ServeHTTP(w, r) + } + } + mux.Handle("GET", "/session/token", f) +} + +// NewSessionTokenHandler creates a HTTP handler which loads the HTTP request +// and calls the "web" service "SessionToken" endpoint. +func NewSessionTokenHandler( + endpoint goa.Endpoint, + mux goahttp.Muxer, + decoder func(*http.Request) goahttp.Decoder, + encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, + errhandler func(context.Context, http.ResponseWriter, error), + formatter func(ctx context.Context, err error) goahttp.Statuser, +) http.Handler { + var ( + decodeRequest = DecodeSessionTokenRequest(mux, decoder) + encodeResponse = EncodeSessionTokenResponse(encoder) + encodeError = EncodeSessionTokenError(encoder, formatter) + ) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), goahttp.AcceptTypeKey, r.Header.Get("Accept")) + ctx = context.WithValue(ctx, goa.MethodKey, "SessionToken") + ctx = context.WithValue(ctx, goa.ServiceKey, "web") + payload, err := decodeRequest(r) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + res, err := endpoint(ctx, payload) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + if err := encodeResponse(ctx, w, res); err != nil { + errhandler(ctx, w, err) + } + }) +} + +// appendFS is a custom implementation of fs.FS that appends a specified prefix +// to the file paths before delegating the Open call to the underlying fs.FS. +type appendFS struct { + prefix string + fs http.FileSystem +} + +// Open opens the named file, appending the prefix to the file path before +// passing it to the underlying fs.FS. +func (s appendFS) Open(name string) (http.File, error) { + switch name { + case "/*": + name = "/static" + } + return s.fs.Open(path.Join(s.prefix, name)) +} + +// appendPrefix returns a new fs.FS that appends the specified prefix to file paths +// before delegating to the provided embed.FS. +func appendPrefix(fsys http.FileSystem, prefix string) http.FileSystem { + return appendFS{prefix: prefix, fs: fsys} +} + +// MountStatic configures the mux to serve GET request made to "/static/*". +func MountStatic(mux goahttp.Muxer, h http.Handler) { + mux.Handle("GET", "/static/*", h.ServeHTTP) +} diff --git a/app/api/v1/gen/http/web/server/types.go b/app/api/v1/gen/http/web/server/types.go new file mode 100644 index 0000000..339a653 --- /dev/null +++ b/app/api/v1/gen/http/web/server/types.go @@ -0,0 +1,248 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// web HTTP server types +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package server + +import ( + web "github.com/jace-ys/countup/api/v1/gen/web" + goa "goa.design/goa/v3/pkg" +) + +// SessionTokenResponseBody is the type of the "web" service "SessionToken" +// endpoint HTTP response body. +type SessionTokenResponseBody struct { + Token string `form:"token" json:"token" xml:"token"` +} + +// IndexUnauthorizedResponseBody is the type of the "web" service "Index" +// endpoint HTTP response body for the "unauthorized" error. +type IndexUnauthorizedResponseBody struct { + // Name is the name of this class of errors. + Name string `form:"name" json:"name" xml:"name"` + // ID is a unique identifier for this particular occurrence of the problem. + ID string `form:"id" json:"id" xml:"id"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message string `form:"message" json:"message" xml:"message"` + // Is the error temporary? + Temporary bool `form:"temporary" json:"temporary" xml:"temporary"` + // Is the error a timeout? + Timeout bool `form:"timeout" json:"timeout" xml:"timeout"` + // Is the error a server-side fault? + Fault bool `form:"fault" json:"fault" xml:"fault"` +} + +// AnotherUnauthorizedResponseBody is the type of the "web" service "Another" +// endpoint HTTP response body for the "unauthorized" error. +type AnotherUnauthorizedResponseBody struct { + // Name is the name of this class of errors. + Name string `form:"name" json:"name" xml:"name"` + // ID is a unique identifier for this particular occurrence of the problem. + ID string `form:"id" json:"id" xml:"id"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message string `form:"message" json:"message" xml:"message"` + // Is the error temporary? + Temporary bool `form:"temporary" json:"temporary" xml:"temporary"` + // Is the error a timeout? + Timeout bool `form:"timeout" json:"timeout" xml:"timeout"` + // Is the error a server-side fault? + Fault bool `form:"fault" json:"fault" xml:"fault"` +} + +// LoginGoogleUnauthorizedResponseBody is the type of the "web" service +// "LoginGoogle" endpoint HTTP response body for the "unauthorized" error. +type LoginGoogleUnauthorizedResponseBody struct { + // Name is the name of this class of errors. + Name string `form:"name" json:"name" xml:"name"` + // ID is a unique identifier for this particular occurrence of the problem. + ID string `form:"id" json:"id" xml:"id"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message string `form:"message" json:"message" xml:"message"` + // Is the error temporary? + Temporary bool `form:"temporary" json:"temporary" xml:"temporary"` + // Is the error a timeout? + Timeout bool `form:"timeout" json:"timeout" xml:"timeout"` + // Is the error a server-side fault? + Fault bool `form:"fault" json:"fault" xml:"fault"` +} + +// LoginGoogleCallbackUnauthorizedResponseBody is the type of the "web" service +// "LoginGoogleCallback" endpoint HTTP response body for the "unauthorized" +// error. +type LoginGoogleCallbackUnauthorizedResponseBody struct { + // Name is the name of this class of errors. + Name string `form:"name" json:"name" xml:"name"` + // ID is a unique identifier for this particular occurrence of the problem. + ID string `form:"id" json:"id" xml:"id"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message string `form:"message" json:"message" xml:"message"` + // Is the error temporary? + Temporary bool `form:"temporary" json:"temporary" xml:"temporary"` + // Is the error a timeout? + Timeout bool `form:"timeout" json:"timeout" xml:"timeout"` + // Is the error a server-side fault? + Fault bool `form:"fault" json:"fault" xml:"fault"` +} + +// LogoutUnauthorizedResponseBody is the type of the "web" service "Logout" +// endpoint HTTP response body for the "unauthorized" error. +type LogoutUnauthorizedResponseBody struct { + // Name is the name of this class of errors. + Name string `form:"name" json:"name" xml:"name"` + // ID is a unique identifier for this particular occurrence of the problem. + ID string `form:"id" json:"id" xml:"id"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message string `form:"message" json:"message" xml:"message"` + // Is the error temporary? + Temporary bool `form:"temporary" json:"temporary" xml:"temporary"` + // Is the error a timeout? + Timeout bool `form:"timeout" json:"timeout" xml:"timeout"` + // Is the error a server-side fault? + Fault bool `form:"fault" json:"fault" xml:"fault"` +} + +// SessionTokenUnauthorizedResponseBody is the type of the "web" service +// "SessionToken" endpoint HTTP response body for the "unauthorized" error. +type SessionTokenUnauthorizedResponseBody struct { + // Name is the name of this class of errors. + Name string `form:"name" json:"name" xml:"name"` + // ID is a unique identifier for this particular occurrence of the problem. + ID string `form:"id" json:"id" xml:"id"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message string `form:"message" json:"message" xml:"message"` + // Is the error temporary? + Temporary bool `form:"temporary" json:"temporary" xml:"temporary"` + // Is the error a timeout? + Timeout bool `form:"timeout" json:"timeout" xml:"timeout"` + // Is the error a server-side fault? + Fault bool `form:"fault" json:"fault" xml:"fault"` +} + +// NewSessionTokenResponseBody builds the HTTP response body from the result of +// the "SessionToken" endpoint of the "web" service. +func NewSessionTokenResponseBody(res *web.SessionTokenResult) *SessionTokenResponseBody { + body := &SessionTokenResponseBody{ + Token: res.Token, + } + return body +} + +// NewIndexUnauthorizedResponseBody builds the HTTP response body from the +// result of the "Index" endpoint of the "web" service. +func NewIndexUnauthorizedResponseBody(res *goa.ServiceError) *IndexUnauthorizedResponseBody { + body := &IndexUnauthorizedResponseBody{ + Name: res.Name, + ID: res.ID, + Message: res.Message, + Temporary: res.Temporary, + Timeout: res.Timeout, + Fault: res.Fault, + } + return body +} + +// NewAnotherUnauthorizedResponseBody builds the HTTP response body from the +// result of the "Another" endpoint of the "web" service. +func NewAnotherUnauthorizedResponseBody(res *goa.ServiceError) *AnotherUnauthorizedResponseBody { + body := &AnotherUnauthorizedResponseBody{ + Name: res.Name, + ID: res.ID, + Message: res.Message, + Temporary: res.Temporary, + Timeout: res.Timeout, + Fault: res.Fault, + } + return body +} + +// NewLoginGoogleUnauthorizedResponseBody builds the HTTP response body from +// the result of the "LoginGoogle" endpoint of the "web" service. +func NewLoginGoogleUnauthorizedResponseBody(res *goa.ServiceError) *LoginGoogleUnauthorizedResponseBody { + body := &LoginGoogleUnauthorizedResponseBody{ + Name: res.Name, + ID: res.ID, + Message: res.Message, + Temporary: res.Temporary, + Timeout: res.Timeout, + Fault: res.Fault, + } + return body +} + +// NewLoginGoogleCallbackUnauthorizedResponseBody builds the HTTP response body +// from the result of the "LoginGoogleCallback" endpoint of the "web" service. +func NewLoginGoogleCallbackUnauthorizedResponseBody(res *goa.ServiceError) *LoginGoogleCallbackUnauthorizedResponseBody { + body := &LoginGoogleCallbackUnauthorizedResponseBody{ + Name: res.Name, + ID: res.ID, + Message: res.Message, + Temporary: res.Temporary, + Timeout: res.Timeout, + Fault: res.Fault, + } + return body +} + +// NewLogoutUnauthorizedResponseBody builds the HTTP response body from the +// result of the "Logout" endpoint of the "web" service. +func NewLogoutUnauthorizedResponseBody(res *goa.ServiceError) *LogoutUnauthorizedResponseBody { + body := &LogoutUnauthorizedResponseBody{ + Name: res.Name, + ID: res.ID, + Message: res.Message, + Temporary: res.Temporary, + Timeout: res.Timeout, + Fault: res.Fault, + } + return body +} + +// NewSessionTokenUnauthorizedResponseBody builds the HTTP response body from +// the result of the "SessionToken" endpoint of the "web" service. +func NewSessionTokenUnauthorizedResponseBody(res *goa.ServiceError) *SessionTokenUnauthorizedResponseBody { + body := &SessionTokenUnauthorizedResponseBody{ + Name: res.Name, + ID: res.ID, + Message: res.Message, + Temporary: res.Temporary, + Timeout: res.Timeout, + Fault: res.Fault, + } + return body +} + +// NewLoginGoogleCallbackPayload builds a web service LoginGoogleCallback +// endpoint payload. +func NewLoginGoogleCallbackPayload(code string, state string, sessionCookie string) *web.LoginGoogleCallbackPayload { + v := &web.LoginGoogleCallbackPayload{} + v.Code = code + v.State = state + v.SessionCookie = sessionCookie + + return v +} + +// NewLogoutPayload builds a web service Logout endpoint payload. +func NewLogoutPayload(sessionCookie string) *web.LogoutPayload { + v := &web.LogoutPayload{} + v.SessionCookie = sessionCookie + + return v +} + +// NewSessionTokenPayload builds a web service SessionToken endpoint payload. +func NewSessionTokenPayload(sessionCookie string) *web.SessionTokenPayload { + v := &web.SessionTokenPayload{} + v.SessionCookie = sessionCookie + + return v +} diff --git a/app/api/v1/gen/teapot/client.go b/app/api/v1/gen/teapot/client.go new file mode 100644 index 0000000..7a51aae --- /dev/null +++ b/app/api/v1/gen/teapot/client.go @@ -0,0 +1,39 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// teapot client +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package teapot + +import ( + "context" + + goa "goa.design/goa/v3/pkg" +) + +// Client is the "teapot" service client. +type Client struct { + EchoEndpoint goa.Endpoint +} + +// NewClient initializes a "teapot" service client given the endpoints. +func NewClient(echo goa.Endpoint) *Client { + return &Client{ + EchoEndpoint: echo, + } +} + +// Echo calls the "Echo" endpoint of the "teapot" service. +// Echo may return the following errors: +// - "unwell" (type *goa.ServiceError) +// - error: internal error +func (c *Client) Echo(ctx context.Context, p *EchoPayload) (res *EchoResult, err error) { + var ires any + ires, err = c.EchoEndpoint(ctx, p) + if err != nil { + return + } + return ires.(*EchoResult), nil +} diff --git a/app/api/v1/gen/teapot/endpoints.go b/app/api/v1/gen/teapot/endpoints.go new file mode 100644 index 0000000..45c78c9 --- /dev/null +++ b/app/api/v1/gen/teapot/endpoints.go @@ -0,0 +1,40 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// teapot endpoints +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package teapot + +import ( + "context" + + goa "goa.design/goa/v3/pkg" +) + +// Endpoints wraps the "teapot" service endpoints. +type Endpoints struct { + Echo goa.Endpoint +} + +// NewEndpoints wraps the methods of the "teapot" service with endpoints. +func NewEndpoints(s Service) *Endpoints { + return &Endpoints{ + Echo: NewEchoEndpoint(s), + } +} + +// Use applies the given middleware to all the "teapot" service endpoints. +func (e *Endpoints) Use(m func(goa.Endpoint) goa.Endpoint) { + e.Echo = m(e.Echo) +} + +// NewEchoEndpoint returns an endpoint function that calls the method "Echo" of +// service "teapot". +func NewEchoEndpoint(s Service) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + p := req.(*EchoPayload) + return s.Echo(ctx, p) + } +} diff --git a/app/api/v1/gen/teapot/service.go b/app/api/v1/gen/teapot/service.go new file mode 100644 index 0000000..95c51f1 --- /dev/null +++ b/app/api/v1/gen/teapot/service.go @@ -0,0 +1,51 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// teapot service +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package teapot + +import ( + "context" + + goa "goa.design/goa/v3/pkg" +) + +// Service is the teapot service interface. +type Service interface { + // Echo implements Echo. + Echo(context.Context, *EchoPayload) (res *EchoResult, err error) +} + +// APIName is the name of the API as defined in the design. +const APIName = "countup" + +// APIVersion is the version of the API as defined in the design. +const APIVersion = "1.0.0" + +// ServiceName is the name of the service as defined in the design. This is the +// same value that is set in the endpoint request contexts under the ServiceKey +// key. +const ServiceName = "teapot" + +// MethodNames lists the service method names as defined in the design. These +// are the same values that are set in the endpoint request contexts under the +// MethodKey key. +var MethodNames = [1]string{"Echo"} + +// EchoPayload is the payload type of the teapot service Echo method. +type EchoPayload struct { + Text string +} + +// EchoResult is the result type of the teapot service Echo method. +type EchoResult struct { + Text string +} + +// MakeUnwell builds a goa.ServiceError from an error. +func MakeUnwell(err error) *goa.ServiceError { + return goa.NewServiceError(err, "unwell", false, false, false) +} diff --git a/app/api/v1/gen/web/client.go b/app/api/v1/gen/web/client.go new file mode 100644 index 0000000..2fc6620 --- /dev/null +++ b/app/api/v1/gen/web/client.go @@ -0,0 +1,115 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// web client +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package web + +import ( + "context" + + goa "goa.design/goa/v3/pkg" +) + +// Client is the "web" service client. +type Client struct { + IndexEndpoint goa.Endpoint + AnotherEndpoint goa.Endpoint + LoginGoogleEndpoint goa.Endpoint + LoginGoogleCallbackEndpoint goa.Endpoint + LogoutEndpoint goa.Endpoint + SessionTokenEndpoint goa.Endpoint +} + +// NewClient initializes a "web" service client given the endpoints. +func NewClient(index, another, loginGoogle, loginGoogleCallback, logout, sessionToken goa.Endpoint) *Client { + return &Client{ + IndexEndpoint: index, + AnotherEndpoint: another, + LoginGoogleEndpoint: loginGoogle, + LoginGoogleCallbackEndpoint: loginGoogleCallback, + LogoutEndpoint: logout, + SessionTokenEndpoint: sessionToken, + } +} + +// Index calls the "Index" endpoint of the "web" service. +// Index may return the following errors: +// - "unauthorized" (type *goa.ServiceError) +// - error: internal error +func (c *Client) Index(ctx context.Context) (res []byte, err error) { + var ires any + ires, err = c.IndexEndpoint(ctx, nil) + if err != nil { + return + } + return ires.([]byte), nil +} + +// Another calls the "Another" endpoint of the "web" service. +// Another may return the following errors: +// - "unauthorized" (type *goa.ServiceError) +// - error: internal error +func (c *Client) Another(ctx context.Context) (res []byte, err error) { + var ires any + ires, err = c.AnotherEndpoint(ctx, nil) + if err != nil { + return + } + return ires.([]byte), nil +} + +// LoginGoogle calls the "LoginGoogle" endpoint of the "web" service. +// LoginGoogle may return the following errors: +// - "unauthorized" (type *goa.ServiceError) +// - error: internal error +func (c *Client) LoginGoogle(ctx context.Context) (res *LoginGoogleResult, err error) { + var ires any + ires, err = c.LoginGoogleEndpoint(ctx, nil) + if err != nil { + return + } + return ires.(*LoginGoogleResult), nil +} + +// LoginGoogleCallback calls the "LoginGoogleCallback" endpoint of the "web" +// service. +// LoginGoogleCallback may return the following errors: +// - "unauthorized" (type *goa.ServiceError) +// - error: internal error +func (c *Client) LoginGoogleCallback(ctx context.Context, p *LoginGoogleCallbackPayload) (res *LoginGoogleCallbackResult, err error) { + var ires any + ires, err = c.LoginGoogleCallbackEndpoint(ctx, p) + if err != nil { + return + } + return ires.(*LoginGoogleCallbackResult), nil +} + +// Logout calls the "Logout" endpoint of the "web" service. +// Logout may return the following errors: +// - "unauthorized" (type *goa.ServiceError) +// - error: internal error +func (c *Client) Logout(ctx context.Context, p *LogoutPayload) (res *LogoutResult, err error) { + var ires any + ires, err = c.LogoutEndpoint(ctx, p) + if err != nil { + return + } + return ires.(*LogoutResult), nil +} + +// SessionToken calls the "SessionToken" endpoint of the "web" service. +// SessionToken may return the following errors: +// - "unauthorized" (type *goa.ServiceError) +// - error: internal error +func (c *Client) SessionToken(ctx context.Context, p *SessionTokenPayload) (res *SessionTokenResult, err error) { + var ires any + ires, err = c.SessionTokenEndpoint(ctx, p) + if err != nil { + return + } + return ires.(*SessionTokenResult), nil +} diff --git a/app/api/v1/gen/web/endpoints.go b/app/api/v1/gen/web/endpoints.go new file mode 100644 index 0000000..cd5e9db --- /dev/null +++ b/app/api/v1/gen/web/endpoints.go @@ -0,0 +1,97 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// web endpoints +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package web + +import ( + "context" + + goa "goa.design/goa/v3/pkg" +) + +// Endpoints wraps the "web" service endpoints. +type Endpoints struct { + Index goa.Endpoint + Another goa.Endpoint + LoginGoogle goa.Endpoint + LoginGoogleCallback goa.Endpoint + Logout goa.Endpoint + SessionToken goa.Endpoint +} + +// NewEndpoints wraps the methods of the "web" service with endpoints. +func NewEndpoints(s Service) *Endpoints { + return &Endpoints{ + Index: NewIndexEndpoint(s), + Another: NewAnotherEndpoint(s), + LoginGoogle: NewLoginGoogleEndpoint(s), + LoginGoogleCallback: NewLoginGoogleCallbackEndpoint(s), + Logout: NewLogoutEndpoint(s), + SessionToken: NewSessionTokenEndpoint(s), + } +} + +// Use applies the given middleware to all the "web" service endpoints. +func (e *Endpoints) Use(m func(goa.Endpoint) goa.Endpoint) { + e.Index = m(e.Index) + e.Another = m(e.Another) + e.LoginGoogle = m(e.LoginGoogle) + e.LoginGoogleCallback = m(e.LoginGoogleCallback) + e.Logout = m(e.Logout) + e.SessionToken = m(e.SessionToken) +} + +// NewIndexEndpoint returns an endpoint function that calls the method "Index" +// of service "web". +func NewIndexEndpoint(s Service) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + return s.Index(ctx) + } +} + +// NewAnotherEndpoint returns an endpoint function that calls the method +// "Another" of service "web". +func NewAnotherEndpoint(s Service) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + return s.Another(ctx) + } +} + +// NewLoginGoogleEndpoint returns an endpoint function that calls the method +// "LoginGoogle" of service "web". +func NewLoginGoogleEndpoint(s Service) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + return s.LoginGoogle(ctx) + } +} + +// NewLoginGoogleCallbackEndpoint returns an endpoint function that calls the +// method "LoginGoogleCallback" of service "web". +func NewLoginGoogleCallbackEndpoint(s Service) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + p := req.(*LoginGoogleCallbackPayload) + return s.LoginGoogleCallback(ctx, p) + } +} + +// NewLogoutEndpoint returns an endpoint function that calls the method +// "Logout" of service "web". +func NewLogoutEndpoint(s Service) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + p := req.(*LogoutPayload) + return s.Logout(ctx, p) + } +} + +// NewSessionTokenEndpoint returns an endpoint function that calls the method +// "SessionToken" of service "web". +func NewSessionTokenEndpoint(s Service) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + p := req.(*SessionTokenPayload) + return s.SessionToken(ctx, p) + } +} diff --git a/app/api/v1/gen/web/service.go b/app/api/v1/gen/web/service.go new file mode 100644 index 0000000..40cbf10 --- /dev/null +++ b/app/api/v1/gen/web/service.go @@ -0,0 +1,94 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// web service +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package web + +import ( + "context" + + goa "goa.design/goa/v3/pkg" +) + +// Service is the web service interface. +type Service interface { + // Index implements Index. + Index(context.Context) (res []byte, err error) + // Another implements Another. + Another(context.Context) (res []byte, err error) + // LoginGoogle implements LoginGoogle. + LoginGoogle(context.Context) (res *LoginGoogleResult, err error) + // LoginGoogleCallback implements LoginGoogleCallback. + LoginGoogleCallback(context.Context, *LoginGoogleCallbackPayload) (res *LoginGoogleCallbackResult, err error) + // Logout implements Logout. + Logout(context.Context, *LogoutPayload) (res *LogoutResult, err error) + // SessionToken implements SessionToken. + SessionToken(context.Context, *SessionTokenPayload) (res *SessionTokenResult, err error) +} + +// APIName is the name of the API as defined in the design. +const APIName = "countup" + +// APIVersion is the version of the API as defined in the design. +const APIVersion = "1.0.0" + +// ServiceName is the name of the service as defined in the design. This is the +// same value that is set in the endpoint request contexts under the ServiceKey +// key. +const ServiceName = "web" + +// MethodNames lists the service method names as defined in the design. These +// are the same values that are set in the endpoint request contexts under the +// MethodKey key. +var MethodNames = [6]string{"Index", "Another", "LoginGoogle", "LoginGoogleCallback", "Logout", "SessionToken"} + +// LoginGoogleCallbackPayload is the payload type of the web service +// LoginGoogleCallback method. +type LoginGoogleCallbackPayload struct { + Code string + State string + SessionCookie string +} + +// LoginGoogleCallbackResult is the result type of the web service +// LoginGoogleCallback method. +type LoginGoogleCallbackResult struct { + RedirectURL string + SessionCookie string +} + +// LoginGoogleResult is the result type of the web service LoginGoogle method. +type LoginGoogleResult struct { + RedirectURL string + SessionCookie string +} + +// LogoutPayload is the payload type of the web service Logout method. +type LogoutPayload struct { + SessionCookie string +} + +// LogoutResult is the result type of the web service Logout method. +type LogoutResult struct { + RedirectURL string + SessionCookie string +} + +// SessionTokenPayload is the payload type of the web service SessionToken +// method. +type SessionTokenPayload struct { + SessionCookie string +} + +// SessionTokenResult is the result type of the web service SessionToken method. +type SessionTokenResult struct { + Token string +} + +// MakeUnauthorized builds a goa.ServiceError from an error. +func MakeUnauthorized(err error) *goa.ServiceError { + return goa.NewServiceError(err, "unauthorized", false, false, false) +} diff --git a/app/atlas.hcl b/app/atlas.hcl new file mode 100644 index 0000000..1a1293d --- /dev/null +++ b/app/atlas.hcl @@ -0,0 +1,9 @@ +env "local" { + src = "file://schema/schema.sql" + dev = "docker://postgres/15/dev" + + migration { + dir = "file://schema/migrations" + format = "goose" + } +} \ No newline at end of file diff --git a/app/cmd/countup-cli/grpc.go b/app/cmd/countup-cli/grpc.go new file mode 100644 index 0000000..753d3c1 --- /dev/null +++ b/app/cmd/countup-cli/grpc.go @@ -0,0 +1,19 @@ +package main + +import ( + "fmt" + "os" + + cli "github.com/jace-ys/countup/api/v1/gen/grpc/cli/countup" + goa "goa.design/goa/v3/pkg" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +func doGRPC(_, host string, _ int, _ bool) (goa.Endpoint, any, error) { + conn, err := grpc.NewClient(host, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + fmt.Fprintf(os.Stderr, "could not connect to gRPC server at %s: %v\n", host, err) + } + return cli.ParseEndpoint(conn) +} diff --git a/app/cmd/countup-cli/http.go b/app/cmd/countup-cli/http.go new file mode 100644 index 0000000..dd87b0f --- /dev/null +++ b/app/cmd/countup-cli/http.go @@ -0,0 +1,39 @@ +package main + +import ( + "net/http" + "time" + + cli "github.com/jace-ys/countup/api/v1/gen/http/cli/countup" + goahttp "goa.design/goa/v3/http" + goa "goa.design/goa/v3/pkg" +) + +func doHTTP(scheme, host string, timeout int, debug bool) (goa.Endpoint, any, error) { + var ( + doer goahttp.Doer + ) + { + doer = &http.Client{Timeout: time.Duration(timeout) * time.Second} + if debug { + doer = goahttp.NewDebugDoer(doer) + } + } + + return cli.ParseEndpoint( + scheme, + host, + doer, + goahttp.RequestEncoder, + goahttp.ResponseDecoder, + debug, + ) +} + +func httpUsageCommands() string { + return cli.UsageCommands() +} + +func httpUsageExamples() string { + return cli.UsageExamples() +} diff --git a/app/cmd/countup-cli/main.go b/app/cmd/countup-cli/main.go new file mode 100644 index 0000000..f4c489d --- /dev/null +++ b/app/cmd/countup-cli/main.go @@ -0,0 +1,124 @@ +package main + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "net/url" + "os" + "strings" + + goa "goa.design/goa/v3/pkg" +) + +func main() { + var ( + hostF = flag.String("host", "local-http", "Server host (valid values: local-http, local-grpc)") + addrF = flag.String("url", "", "URL to service host") + + verboseF = flag.Bool("verbose", false, "Print request and response details") + vF = flag.Bool("v", false, "Print request and response details") + timeoutF = flag.Int("timeout", 30, "Maximum number of seconds to wait for response") + ) + flag.Usage = usage + flag.Parse() + var ( + addr string + timeout int + debug bool + ) + { + addr = *addrF + if addr == "" { + switch *hostF { + case "local-http": + addr = "http://localhost:8080" + case "local-grpc": + addr = "grpc://localhost:8081" + default: + fmt.Fprintf(os.Stderr, "invalid host argument: %q (valid hosts: local-http|local-grpc)\n", *hostF) + os.Exit(1) + } + } + timeout = *timeoutF + debug = *verboseF || *vF + } + + var ( + scheme string + host string + ) + { + u, err := url.Parse(addr) + if err != nil { + fmt.Fprintf(os.Stderr, "invalid URL %#v: %s\n", addr, err) + os.Exit(1) + } + scheme = u.Scheme + host = u.Host + } + var ( + endpoint goa.Endpoint + payload any + err error + ) + { + switch scheme { + case "http", "https": + endpoint, payload, err = doHTTP(scheme, host, timeout, debug) + case "grpc", "grpcs": + endpoint, payload, err = doGRPC(scheme, host, timeout, debug) + default: + fmt.Fprintf(os.Stderr, "invalid scheme: %q (valid schemes: grpc|http)\n", scheme) + os.Exit(1) + } + } + if err != nil { + if err == flag.ErrHelp { + os.Exit(0) + } + fmt.Fprintln(os.Stderr, err.Error()) + fmt.Fprintln(os.Stderr, "run '"+os.Args[0]+" --help' for detailed usage.") + os.Exit(1) + } + + data, err := endpoint(context.Background(), payload) + if err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + os.Exit(1) + } + + if data != nil { + m, _ := json.MarshalIndent(data, "", " ") + fmt.Println(string(m)) + } +} + +func usage() { + fmt.Fprintf(os.Stderr, `%s is a command line client for the countup API. + +Usage: + %s [-host HOST][-url URL][-timeout SECONDS][-verbose|-v] SERVICE ENDPOINT [flags] + + -host HOST: server host (local-http). valid values: local-http, local-grpc + -url URL: specify service URL overriding host URL (http://localhost:8080) + -timeout: maximum number of seconds to wait for response (30) + -verbose|-v: print request and response details (false) + +Commands: +%s +Additional help: + %s SERVICE [ENDPOINT] --help + +Example: +%s +`, os.Args[0], os.Args[0], indent(httpUsageCommands()), os.Args[0], indent(httpUsageExamples())) +} + +func indent(s string) string { + if s == "" { + return "" + } + return " " + strings.Replace(s, "\n", "\n ", -1) +} diff --git a/app/cmd/countup/main.go b/app/cmd/countup/main.go new file mode 100644 index 0000000..158aa8a --- /dev/null +++ b/app/cmd/countup/main.go @@ -0,0 +1,51 @@ +package main + +import ( + "context" + "io" + "os" + + "github.com/alecthomas/kong" + + "github.com/jace-ys/countup/api/v1/gen/api" + "github.com/jace-ys/countup/internal/ctxlog" +) + +type RootCmd struct { + Globals + + Migrate MigrateCmd `cmd:"" help:"Run database migrations."` + Server ServerCmd `cmd:"" help:"Run the countup server."` + Version VersionCmd `cmd:"" help:"Show version information."` +} + +type Globals struct { + Debug bool `env:"DEBUG" help:"Enable debug logging."` + Writer io.Writer `kong:"-"` +} + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + root := RootCmd{ + Globals: Globals{ + Writer: os.Stdout, + }, + } + + cli := kong.Parse(&root, + kong.Name(api.APIName), + kong.UsageOnError(), + kong.ConfigureHelp(kong.HelpOptions{ + Compact: true, + FlagsLast: true, + NoExpandSubcommands: true, + }), + ) + + ctx = ctxlog.NewContext(ctx, root.Writer, root.Debug) + + cli.BindTo(ctx, (*context.Context)(nil)) + cli.FatalIfErrorf(cli.Run(&root.Globals)) +} diff --git a/app/cmd/countup/migrate.go b/app/cmd/countup/migrate.go new file mode 100644 index 0000000..27de4c5 --- /dev/null +++ b/app/cmd/countup/migrate.go @@ -0,0 +1,51 @@ +package main + +import ( + "context" + "fmt" + + "github.com/jackc/pgx/v5/stdlib" + "github.com/pressly/goose/v3" + + "github.com/jace-ys/countup/internal/postgres" + "github.com/jace-ys/countup/schema/migrations" +) + +type MigrateCmd struct { + Command string `arg:"" help:"Command to pass to goose migrate."` + Args []string `arg:"" optional:"" passthrough:"" help:"Additional args to pass to goose migrate."` + + Database struct { + ConnectionURI string `env:"CONNECTION_URI" required:"" help:"Connection URI for connecting to the database."` + } `embed:"" envprefix:"DATABASE_" prefix:"database."` + + Migrations struct { + Dir string `env:"DIR" default:"." help:"Path to the directory containing migration files."` + LocalFS bool `env:"LOCALFS" help:"Allows discovering of migration files from OS filesystem."` + } `embed:"" envprefix:"MIGRATIONS_" prefix:"migrations."` +} + +func (c *MigrateCmd) Run(ctx context.Context, g *Globals) error { + db, err := postgres.NewPool(ctx, c.Database.ConnectionURI) + if err != nil { + return fmt.Errorf("init db pool: %w", err) + } + defer db.Close() + + conn := stdlib.OpenDBFromPool(db.Pool) + defer conn.Close() + + if err := goose.SetDialect(string(goose.DialectPostgres)); err != nil { + return fmt.Errorf("set goose dialect: %w", err) + } + + if !c.Migrations.LocalFS { + goose.SetBaseFS(migrations.FSDir) + } + + if err := goose.RunContext(ctx, c.Command, conn, c.Migrations.Dir, c.Args...); err != nil { + return fmt.Errorf("run goose command: %w", err) + } + + return nil +} diff --git a/app/cmd/countup/server.go b/app/cmd/countup/server.go new file mode 100644 index 0000000..7a116f2 --- /dev/null +++ b/app/cmd/countup/server.go @@ -0,0 +1,164 @@ +package main + +import ( + "context" + "fmt" + "time" + + "github.com/gorilla/securecookie" + "github.com/markbates/goth/providers/google" + + apiv1 "github.com/jace-ys/countup/api/v1" + genapi "github.com/jace-ys/countup/api/v1/gen/api" + grpcapiclient "github.com/jace-ys/countup/api/v1/gen/grpc/api/client" + apipb "github.com/jace-ys/countup/api/v1/gen/grpc/api/pb" + grpcapi "github.com/jace-ys/countup/api/v1/gen/grpc/api/server" + teapotpb "github.com/jace-ys/countup/api/v1/gen/grpc/teapot/pb" + grpcteapot "github.com/jace-ys/countup/api/v1/gen/grpc/teapot/server" + httpapi "github.com/jace-ys/countup/api/v1/gen/http/api/server" + httpteapot "github.com/jace-ys/countup/api/v1/gen/http/teapot/server" + httpweb "github.com/jace-ys/countup/api/v1/gen/http/web/server" + genteapot "github.com/jace-ys/countup/api/v1/gen/teapot" + genweb "github.com/jace-ys/countup/api/v1/gen/web" + "github.com/jace-ys/countup/internal/app" + "github.com/jace-ys/countup/internal/ctxlog" + "github.com/jace-ys/countup/internal/endpoint" + "github.com/jace-ys/countup/internal/handler/api" + "github.com/jace-ys/countup/internal/handler/teapot" + "github.com/jace-ys/countup/internal/handler/web" + "github.com/jace-ys/countup/internal/instrument" + "github.com/jace-ys/countup/internal/postgres" + "github.com/jace-ys/countup/internal/service/counter" + counterstore "github.com/jace-ys/countup/internal/service/counter/store" + "github.com/jace-ys/countup/internal/service/user" + userstore "github.com/jace-ys/countup/internal/service/user/store" + "github.com/jace-ys/countup/internal/transport" + "github.com/jace-ys/countup/internal/worker" +) + +type ServerCmd struct { + Port int `env:"PORT" default:"8080" help:"Port for application server to listen on."` + AdminPort int `env:"ADMIN_PORT" default:"9090" help:"Port for admin server to listen on."` + + OTLP struct { + MetricsEndpoint string `env:"METRICS_ENDPOINT" default:"127.0.0.1:4317" help:"OTLP gRPC endpoint to send OpenTelemetry metrics to."` + TracesEndpoint string `env:"TRACES_ENDPOINT" default:"127.0.0.1:4317" help:"OTLP gRPC endpoint to send OpenTelemetry traces to."` + } `embed:"" envprefix:"OTLP_" prefix:"otlp."` + + Database struct { + ConnectionURI string `env:"CONNECTION_URI" required:"" help:"Connection URI for connecting to the database."` + } `embed:"" envprefix:"DATABASE_" prefix:"database."` + + Worker struct { + Concurrency int `env:"CONCURRENCY" default:"50" help:"Number of workers to run in the worker pool."` + } `embed:"" envprefix:"WORKER_" prefix:"worker."` + + OAuth struct { + ClientID string `env:"CLIENT_ID" required:"" help:"Client ID for the Google OAuth2 configuration."` + ClientSecret string `env:"CLIENT_SECRET" required:"" help:"Client secret for the Google OAuth2 configuration."` + RedirectURL string `env:"REDIRECT_URL" default:"http://localhost:8080/login/google/callback" help:"URL to redirect to upon successful OAuth2 authentication."` + } `embed:"" envprefix:"OAUTH_" prefix:"oauth."` + + Counter struct { + FinalizeWindow time.Duration `env:"FINALIZE_WINDOW" default:"1m" help:"Time period to wait before finalizing counter increments."` + } `embed:"" envprefix:"COUNTER_" prefix:"counter."` +} + +func (c *ServerCmd) Run(ctx context.Context, g *Globals) error { + instrument.MustInitOTelProvider(ctx, genapi.APIName, genapi.APIVersion, c.OTLP.MetricsEndpoint, c.OTLP.TracesEndpoint) + defer func() { + if err := instrument.OTel.Shutdown(ctx); err != nil { + ctxlog.Error(ctx, "error shutting down otel provider", err) + } + }() + + db, err := postgres.NewPool(ctx, c.Database.ConnectionURI) + if err != nil { + return fmt.Errorf("init db pool: %w", err) + } + defer db.Close() + + worker, err := worker.NewPool(ctx, "app.worker", db, c.Worker.Concurrency) + if err != nil { + return fmt.Errorf("init worker pool: %w", err) + } + + authn := google.New(c.OAuth.ClientID, c.OAuth.ClientSecret, c.OAuth.RedirectURL) + + httpSrv := app.NewHTTPServer(ctx, "app.http", c.Port) + grpcSrv := app.NewGRPCServer[apipb.APIServer](ctx, "app.grpc", c.Port+1) + + admin := app.NewAdminServer(ctx, c.AdminPort, g.Debug) + admin.Administer(httpSrv, grpcSrv, worker) + + { + counterSvc := counter.New(db, worker, counterstore.New(), c.Counter.FinalizeWindow) + userSvc := user.New(db, userstore.New()) + + handler, err := api.NewHandler(authn, counterSvc, userSvc) + if err != nil { + return fmt.Errorf("init api handler: %w", err) + } + admin.Administer(handler) + + ep := endpoint.Goa(genapi.NewEndpoints).Adapt(handler) + + { + transport := transport.GoaHTTP(httpapi.New, httpapi.Mount) + httpSrv.RegisterHandler("/api/v1", transport.Adapt(ctx, ep, apiv1.OpenAPIFS)) + } + { + transport := transport.GoaGRPC(grpcapi.New) + grpcSrv.RegisterHandler(&apipb.API_ServiceDesc, transport.Adapt(ctx, ep)) + } + } + + { + handler, err := teapot.NewHandler(worker) + if err != nil { + return fmt.Errorf("init teapot handler: %w", err) + } + admin.Administer(handler) + + ep := endpoint.Goa(genteapot.NewEndpoints).Adapt(handler) + + { + transport := transport.GoaHTTP(httpteapot.New, httpteapot.Mount) + httpSrv.RegisterHandler("/teapot", transport.Adapt(ctx, ep, apiv1.OpenAPIFS)) + } + { + transport := transport.GoaGRPC(grpcteapot.New) + grpcSrv.RegisterHandler(&teapotpb.Teapot_ServiceDesc, transport.Adapt(ctx, ep)) + } + } + + { + cookies := securecookie.New([]byte("secret-hash-key"), []byte("secret-block-key")) + + tc, err := transport.GoaGRPCClient(grpcapiclient.NewClient).Adapt(grpcSrv.Addr()) + if err != nil { + return fmt.Errorf("init api client: %w", err) + } + defer tc.Close() + + apiClient := genapi.NewClient(tc.Client().AuthToken(), tc.Client().CounterGet(), tc.Client().CounterIncrement()) + + handler, err := web.NewHandler(authn, cookies, apiClient) + if err != nil { + return fmt.Errorf("init web handler: %w", err) + } + admin.Administer(handler) + + ep := endpoint.Goa(genweb.NewEndpoints).Adapt(handler) + + transport := transport.GoaHTTP(httpweb.New, httpweb.Mount) + httpSrv.RegisterHandler("/", transport.Adapt(ctx, ep, web.StaticFS)) + } + + if err := app.New(httpSrv, grpcSrv, admin, worker).Run(ctx); err != nil { + ctxlog.Error(ctx, "encountered error while running app", err) + return fmt.Errorf("app run: %w", err) + } + + return nil +} diff --git a/app/cmd/countup/version.go b/app/cmd/countup/version.go new file mode 100644 index 0000000..b6c510f --- /dev/null +++ b/app/cmd/countup/version.go @@ -0,0 +1,20 @@ +package main + +import ( + "context" + "fmt" + "runtime" + + "github.com/jace-ys/countup/internal/versioninfo" +) + +type VersionCmd struct { +} + +func (c *VersionCmd) Run(ctx context.Context, g *Globals) error { + fmt.Printf( + "Version: %v\nGit SHA: %v\nGo Version: %v\nGo OS/Arch: %v/%v\n", + versioninfo.Version, versioninfo.CommitSHA, runtime.Version(), runtime.GOOS, runtime.GOARCH, + ) + return nil +} diff --git a/app/go.mod b/app/go.mod new file mode 100644 index 0000000..5efaade --- /dev/null +++ b/app/go.mod @@ -0,0 +1,83 @@ +module github.com/jace-ys/countup + +go 1.23.4 + +require ( + github.com/alecthomas/kong v1.6.0 + github.com/alexliesenfeld/health v0.8.0 + github.com/exaring/otelpgx v0.7.0 + github.com/go-chi/chi/v5 v5.1.0 + github.com/golang-jwt/jwt/v5 v5.2.1 + github.com/gorilla/securecookie v1.1.2 + github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 + github.com/jackc/pgx/v5 v5.7.1 + github.com/markbates/goth v1.80.0 + github.com/pressly/goose/v3 v3.23.0 + github.com/riverqueue/river v0.14.2 + github.com/riverqueue/river/riverdriver/riverdatabasesql v0.14.2 + github.com/riverqueue/river/riverdriver/riverpgxv5 v0.14.2 + github.com/riverqueue/river/rivertype v0.14.2 + github.com/rs/xid v1.6.0 + github.com/stretchr/testify v1.10.0 + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.57.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 + go.opentelemetry.io/contrib/instrumentation/runtime v0.57.0 + go.opentelemetry.io/otel v1.32.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 + go.opentelemetry.io/otel/metric v1.32.0 + go.opentelemetry.io/otel/sdk v1.32.0 + go.opentelemetry.io/otel/trace v1.32.0 + goa.design/clue v1.0.7 + goa.design/goa/v3 v3.19.1 + golang.org/x/sync v0.10.0 + google.golang.org/grpc v1.68.1 + google.golang.org/protobuf v1.35.2 +) + +require ( + cloud.google.com/go/compute/metadata v0.5.2 // indirect + github.com/aws/smithy-go v1.22.1 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/lib/pq v1.10.9 // indirect + github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d // indirect + github.com/mfridman/interpolate v0.0.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/riverqueue/river/riverdriver v0.14.2 // indirect + github.com/riverqueue/river/rivershared v0.14.2 // indirect + github.com/sethvargo/go-retry v0.3.0 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.32.0 // indirect + go.opentelemetry.io/proto/otlp v1.4.0 // indirect + go.uber.org/goleak v1.3.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/crypto v0.30.0 // indirect + golang.org/x/mod v0.22.0 // indirect + golang.org/x/net v0.32.0 // indirect + golang.org/x/oauth2 v0.24.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/term v0.27.0 // indirect + golang.org/x/text v0.21.0 // indirect + golang.org/x/tools v0.28.0 // indirect + google.golang.org/genproto v0.0.0-20241206012308-a4fef0638583 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20241206012308-a4fef0638583 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241206012308-a4fef0638583 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/app/go.sum b/app/go.sum new file mode 100644 index 0000000..1806910 --- /dev/null +++ b/app/go.sum @@ -0,0 +1,211 @@ +cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= +cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= +github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= +github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/kong v1.6.0 h1:mwOzbdMR7uv2vul9J0FU3GYxE7ls/iX1ieMg5WIM6gE= +github.com/alecthomas/kong v1.6.0/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/alexliesenfeld/health v0.8.0 h1:lCV0i+ZJPTbqP7LfKG7p3qZBl5VhelwUFCIVWl77fgk= +github.com/alexliesenfeld/health v0.8.0/go.mod h1:TfNP0f+9WQVWMQRzvMUjlws4ceXKEL3WR+6Hp95HUFc= +github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro= +github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/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/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598 h1:MGKhKyiYrvMDZsmLR/+RGffQSXwEkXgfLSA08qDn9AI= +github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598/go.mod h1:0FpDmbrt36utu8jEmeU05dPC9AB5tsLYVVi+ZHfyuwI= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/exaring/otelpgx v0.7.0 h1:Wv1x53y6zmmBsEPbWNae6XJAbMNC3KSJmpWRoZxtZr8= +github.com/exaring/otelpgx v0.7.0/go.mod h1:2oRpYkkPBXpvRqQqP0gqkkFPwITRObbpsrA8NT1Fu/I= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= +github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +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/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 h1:Dj0L5fhJ9F82ZJyVOmBx6msDp/kfd1t9GRfny/mfJA0= +github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= +github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +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/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d h1:Zj+PHjnhRYWBK6RqCDBcAhLXoi3TzC27Zad/Vn+gnVQ= +github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d/go.mod h1:WZy8Q5coAB1zhY9AOBJP0O6J4BuDfbupUDavKY+I3+s= +github.com/manveru/gobdd v0.0.0-20131210092515-f1a17fdd710b h1:3E44bLeN8uKYdfQqVQycPnaVviZdBLbizFhU49mtbe4= +github.com/manveru/gobdd v0.0.0-20131210092515-f1a17fdd710b/go.mod h1:Bj8LjjP0ReT1eKt5QlKjwgi5AFm5mI6O1A2G4ChI0Ag= +github.com/markbates/goth v1.80.0 h1:NnvatczZDzOs1hn9Ug+dVYf2Viwwkp/ZDX5K+GLjan8= +github.com/markbates/goth v1.80.0/go.mod h1:4/GYHo+W6NWisrMPZnq0Yr2Q70UntNLn7KXEFhrIdAY= +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/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= +github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +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/pressly/goose/v3 v3.23.0 h1:57hqKos8izGek4v6D5+OXBa+Y4Rq8MU//+MmnevdpVA= +github.com/pressly/goose/v3 v3.23.0/go.mod h1:rpx+D9GX/+stXmzKa+uh1DkjPnNVMdiOCV9iLdle4N8= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/riverqueue/river v0.14.2 h1:I2VJ5HawamDDiL7QIy1XF2/PtwC+Re2y0OBR9Si6v/s= +github.com/riverqueue/river v0.14.2/go.mod h1:RHcZSKQuaYfylhbkIHcw+xUdBia3LtPJr66qsFCMj3Q= +github.com/riverqueue/river/riverdriver v0.14.2 h1:7WSg3m8gjMbmwMeavBklkABLqy/+ox+Zg7d96zT0yBc= +github.com/riverqueue/river/riverdriver v0.14.2/go.mod h1:Z3mKValBmOfJGfOxd7FWeAQDR9tFkpYObqNS0BSJtMU= +github.com/riverqueue/river/riverdriver/riverdatabasesql v0.14.2 h1:SRUAmpZBIwJj8P8l6DK86z1jp7o31YSg4aresvnC8yc= +github.com/riverqueue/river/riverdriver/riverdatabasesql v0.14.2/go.mod h1:ThZfeDtiphBt3a0iwFQTy7uVTQ1DliC9cXdwNaPDQ2w= +github.com/riverqueue/river/riverdriver/riverpgxv5 v0.14.2 h1:+zhN3LGdzEoi/EmmNxMkunmeFd84XAB8ykDD8Rfqoq8= +github.com/riverqueue/river/riverdriver/riverpgxv5 v0.14.2/go.mod h1:OrpOHxAy4wOHGK2bwFKyOgKG353e2Z2EaPwO6RWlZp4= +github.com/riverqueue/river/rivershared v0.14.2 h1:5cUC3jXYi1zLg7tPjsOwDPj9KQuxM1Mf5QBRS6QFHkc= +github.com/riverqueue/river/rivershared v0.14.2/go.mod h1:WZnOZV9KQgittVA01UH3/GI9RSgG0JfDkA/cohqV7v0= +github.com/riverqueue/river/rivertype v0.14.2 h1:otCEcibq2y5+HAxqvVPpc4tgShwISbFWrtqyL8qnI0M= +github.com/riverqueue/river/rivertype v0.14.2/go.mod h1:4vpt5ZSdZ35mFbRAV4oXgeRdH3Mq5h1pUzQTvaGfCUA= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +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 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= +github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.57.0 h1:qtFISDHKolvIxzSs0gIaiPUPR0Cucb0F2coHC7ZLdps= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.57.0/go.mod h1:Y+Pop1Q6hCOnETWTW4NROK/q1hv50hM7yDaUTjG8lp8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 h1:DheMAlT6POBP+gh8RUH19EOTnQIor5QE0uSRPtzCpSw= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0/go.mod h1:wZcGmeVO9nzP67aYSLDqXNWK87EZWhi7JWj1v7ZXf94= +go.opentelemetry.io/contrib/instrumentation/runtime v0.57.0 h1:kJB5wMVorwre8QzEodzTAbzm9FOOah0zvG+V4abNlEE= +go.opentelemetry.io/contrib/instrumentation/runtime v0.57.0/go.mod h1:Nup4TgnOyEJWmVq9sf/ASH3ZJiAXwWHd5xZCHG7Sg9M= +go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= +go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 h1:j7ZSD+5yn+lo3sGV69nW04rRR0jhYnBwjuX3r0HvnK0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0/go.mod h1:WXbYJTUaZXAbYd8lbgGuvih0yuCfOFC5RJoYnoLcGz8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0 h1:t/Qur3vKSkUCcDVaSumWF2PKHt85pc7fRvFuoVT8qFU= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0/go.mod h1:Rl61tySSdcOJWoEgYZVtmnKdA0GeKrSqkHC1t+91CH8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 h1:IJFEoHiytixx8cMiVAO+GmHR6Frwu+u5Ur8njpFO6Ac= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0/go.mod h1:3rHrKNtLIoS0oZwkY2vxi+oJcwFRWdtUyRII+so45p8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 h1:9kV11HXBHZAvuPUZxmMWrH8hZn/6UnHX4K0mu36vNsU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0/go.mod h1:JyA0FHXe22E1NeNiHmVp7kFHglnexDQ7uRWDiiJ1hKQ= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 h1:cMyu9O88joYEaI47CnQkxO1XZdpoTF9fEnW2duIddhw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0/go.mod h1:6Am3rn7P9TVVeXYG+wtcGE7IE1tsQ+bP3AuWcKt/gOI= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.31.0 h1:HZgBIps9wH0RDrwjrmNa3DVbNRW60HEhdzqZFyAp3fI= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.31.0/go.mod h1:RDRhvt6TDG0eIXmonAx5bd9IcwpqCkziwkOClzWKwAQ= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.31.0 h1:UGZ1QwZWY67Z6BmckTU+9Rxn04m2bD3gD6Mk0OIOCPk= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.31.0/go.mod h1:fcwWuDuaObkkChiDlhEpSq9+X1C0omv+s5mBtToAQ64= +go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= +go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= +go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4= +go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU= +go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= +go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= +go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= +go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= +go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg= +go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +goa.design/clue v1.0.7 h1:Z0qhUTvMMo2C7bxn9X7Wt4DXahGMdYuIg7pr3F+iLOs= +goa.design/clue v1.0.7/go.mod h1:z9vhVyNCV02Aggr20KilzR/QQigD/wuz+0uGvWr4MYk= +goa.design/goa/v3 v3.19.1 h1:jpV3LEy7YANzPMwm++Lu17RoThRJgXrPxdEM0A1nlOE= +goa.design/goa/v3 v3.19.1/go.mod h1:astNE9ube0YCxqq7DQkt1MtLxB/b3kRPEFkEZovcO2I= +golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY= +golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= +golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= +golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= +golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= +golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= +golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= +google.golang.org/genproto v0.0.0-20241206012308-a4fef0638583 h1:pjPnE7Rv3PAwHISLRJhA3HQTnM2uu5qcnroxTkRb5G8= +google.golang.org/genproto v0.0.0-20241206012308-a4fef0638583/go.mod h1:dW27OyXi0Ph+N43jeCWMFC86aTT5VgdeQtOSf0Hehdw= +google.golang.org/genproto/googleapis/api v0.0.0-20241206012308-a4fef0638583 h1:v+j+5gpj0FopU0KKLDGfDo9ZRRpKdi5UBrCP0f76kuY= +google.golang.org/genproto/googleapis/api v0.0.0-20241206012308-a4fef0638583/go.mod h1:jehYqy3+AhJU9ve55aNOaSml7wUXjF9x6z2LcCfpAhY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241206012308-a4fef0638583 h1:IfdSdTcLFy4lqUQrQJLkLt1PB+AsqVz6lwkWPzWEz10= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241206012308-a4fef0638583/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= +google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0= +google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw= +google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= +google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +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= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= +modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U= +modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= +modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= +modernc.org/sqlite v1.34.1 h1:u3Yi6M0N8t9yKRDwhXcyp1eS5/ErhPTBggxWFuR6Hfk= +modernc.org/sqlite v1.34.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/app/internal/app/admin.go b/app/internal/app/admin.go new file mode 100644 index 0000000..75b7ddc --- /dev/null +++ b/app/internal/app/admin.go @@ -0,0 +1,83 @@ +package app + +import ( + "context" + "errors" + "fmt" + "net/http" + + "github.com/alexliesenfeld/health" + "github.com/go-chi/chi/v5" + "goa.design/clue/debug" + "goa.design/clue/log" + goahttp "goa.design/goa/v3/http" + "goa.design/goa/v3/http/middleware" + + "github.com/jace-ys/countup/internal/ctxlog" + "github.com/jace-ys/countup/internal/healthz" + "github.com/jace-ys/countup/internal/transport/middleware/recovery" +) + +type AdminServer struct { + debug bool + srv *HTTPServer + mux *chi.Mux + checks []health.Check +} + +func NewAdminServer(ctx context.Context, port int, debug bool) *AdminServer { + return &AdminServer{ + debug: debug, + srv: NewHTTPServer(ctx, "admin", port), + mux: chi.NewRouter(), + } +} + +var _ Server = (*AdminServer)(nil) + +func (s *AdminServer) Name() string { + return s.srv.Name() +} + +func (s *AdminServer) Kind() string { + return s.srv.Kind() +} + +func (s *AdminServer) Addr() string { + return s.srv.Addr() +} + +func (s *AdminServer) Serve(ctx context.Context) error { + s.srv.srv.Handler = s.router(ctx) + if err := s.srv.srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { + return fmt.Errorf("serving admin server: %w", err) + } + return nil +} + +func (s *AdminServer) router(ctx context.Context) http.Handler { + s.mux.Get("/healthz", health.NewHandler(healthz.NewChecker(s.checks...))) + + debugMux := goahttp.NewMuxer() + debug.MountPprofHandlers(debug.Adapt(debugMux), debug.WithPrefix("/pprof")) + if s.debug { + debug.MountDebugLogEnabler(debug.Adapt(debugMux), debug.WithPath("/settings")) + } + s.mux.Mount("/debug", debugMux) + + logCtx := log.With(ctx, ctxlog.KV("server", s.Name())) + return chainMiddleware(s.mux, + recovery.HTTP(logCtx), + middleware.PopulateRequestContext(), + ) +} + +func (s *AdminServer) Shutdown(ctx context.Context) error { + return s.srv.Shutdown(ctx) +} + +func (s *AdminServer) Administer(targets ...healthz.Target) { + for _, target := range targets { + s.checks = append(s.checks, target.HealthChecks()...) + } +} diff --git a/app/internal/app/application.go b/app/internal/app/application.go new file mode 100644 index 0000000..fc1e1c2 --- /dev/null +++ b/app/internal/app/application.go @@ -0,0 +1,78 @@ +package app + +import ( + "context" + "os/signal" + "syscall" + "time" + + "golang.org/x/sync/errgroup" + + "github.com/jace-ys/countup/internal/ctxlog" +) + +type Application struct { + servers []Server +} + +func New(servers ...Server) *Application { + return &Application{ + servers: servers, + } +} + +type Server interface { + Name() string + Kind() string + Addr() string + Serve(ctx context.Context) error + Shutdown(ctx context.Context) error +} + +func (a *Application) Run(ctx context.Context) error { + ctx, stop := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM) + go func() { + <-ctx.Done() + stop() + }() + + g, ctx := errgroup.WithContext(ctx) + + for _, srv := range a.servers { + g.Go(func() error { + ctxlog.Print(ctx, "server listening", + ctxlog.KV("server", srv.Name()), + ctxlog.KV("kind", srv.Kind()), + ctxlog.KV("addr", srv.Addr()), + ) + return srv.Serve(ctx) + }) + } + + ctxlog.Print(ctx, "application started") + <-ctx.Done() + ctxlog.Print(ctx, "application shutting down gracefully") + + shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + for _, srv := range a.servers { + g.Go(func() error { + if err := srv.Shutdown(shutdownCtx); err != nil { + ctxlog.Error(ctx, "server shutdown error", err, + ctxlog.KV("server", srv.Name()), + ctxlog.KV("kind", srv.Kind()), + ) + } else { + ctxlog.Print(ctx, "server shutdown complete", + ctxlog.KV("server", srv.Name()), + ctxlog.KV("kind", srv.Kind()), + ) + } + return nil + }) + } + + defer ctxlog.Print(ctx, "application stopped") + return g.Wait() //nolint:wrapcheck +} diff --git a/app/internal/app/grpc.go b/app/internal/app/grpc.go new file mode 100644 index 0000000..7c18821 --- /dev/null +++ b/app/internal/app/grpc.go @@ -0,0 +1,131 @@ +package app + +import ( + "context" + "fmt" + "net" + + "github.com/alexliesenfeld/health" + "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" + "go.opentelemetry.io/otel/attribute" + "goa.design/clue/debug" + "goa.design/clue/log" + "google.golang.org/grpc" + healthpb "google.golang.org/grpc/health/grpc_health_v1" + "google.golang.org/grpc/reflection" + "google.golang.org/grpc/reflection/grpc_reflection_v1" + "google.golang.org/grpc/reflection/grpc_reflection_v1alpha" + "google.golang.org/grpc/stats" + + "github.com/jace-ys/countup/internal/ctxlog" + "github.com/jace-ys/countup/internal/healthz" + "github.com/jace-ys/countup/internal/transport/middleware/recovery" + "github.com/jace-ys/countup/internal/transport/middleware/reqid" +) + +type GRPCServer struct { + name string + addr string + srv *grpc.Server +} + +func NewGRPCServer[SS any](ctx context.Context, name string, port int) *GRPCServer { + addr := fmt.Sprintf(":%d", port) + + excludedMethods := map[string]bool{ + grpc_reflection_v1.ServerReflection_ServerReflectionInfo_FullMethodName: true, + grpc_reflection_v1alpha.ServerReflection_ServerReflectionInfo_FullMethodName: true, + healthpb.Health_Check_FullMethodName: true, + healthpb.Health_Watch_FullMethodName: true, + } + + logCtx := log.With(ctx, ctxlog.KV("server", name)) + srv := grpc.NewServer( + grpc.ChainUnaryInterceptor( + recovery.UnaryServerInterceptor(logCtx), + withMethodFilter(reqid.UnaryServerInterceptor(), excludedMethods), + withMethodFilter(ctxlog.UnaryServerInterceptor(logCtx), excludedMethods), + withMethodFilter(debug.UnaryServerInterceptor(), excludedMethods), + ), + grpc.StatsHandler(otelgrpc.NewServerHandler( + otelgrpc.WithSpanAttributes(attribute.String("rpc.server.name", name)), + otelgrpc.WithFilter(func(info *stats.RPCTagInfo) bool { + return !excludedMethods[info.FullMethodName] + }), + )), + ) + + reflection.Register(srv) + healthpb.RegisterHealthServer(srv, healthz.NewGRPCHandler()) + + return &GRPCServer{ + name: name, + addr: addr, + srv: srv, + } +} + +func withMethodFilter(interceptor grpc.UnaryServerInterceptor, excluded map[string]bool) grpc.UnaryServerInterceptor { + return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + if excluded := excluded[info.FullMethod]; excluded { + return handler(ctx, req) + } + return interceptor(ctx, req, info, handler) + } +} + +func (s *GRPCServer) RegisterHandler(sd *grpc.ServiceDesc, ss any) { + s.srv.RegisterService(sd, ss) +} + +var _ Server = (*GRPCServer)(nil) + +func (s *GRPCServer) Name() string { + return s.name +} + +func (s *GRPCServer) Kind() string { + return "grpc" +} + +func (s *GRPCServer) Addr() string { + return s.addr +} + +func (s *GRPCServer) Serve(ctx context.Context) error { + lis, err := net.Listen("tcp", s.addr) + if err != nil { + return fmt.Errorf("tcp listener: %w", err) + } + + if err := s.srv.Serve(lis); err != nil { + return fmt.Errorf("serving gRPC server: %w", err) + } + + return nil +} + +func (s *GRPCServer) Shutdown(ctx context.Context) error { + ok := make(chan struct{}) + + go func() { + s.srv.GracefulStop() + close(ok) + }() + + select { + case <-ok: + return nil + case <-ctx.Done(): + s.srv.Stop() + return ctx.Err() + } +} + +var _ healthz.Target = (*GRPCServer)(nil) + +func (s *GRPCServer) HealthChecks() []health.Check { + return []health.Check{ + healthz.GRPCCheck(s.Name(), s.Addr()), + } +} diff --git a/app/internal/app/http.go b/app/internal/app/http.go new file mode 100644 index 0000000..5c0a176 --- /dev/null +++ b/app/internal/app/http.go @@ -0,0 +1,119 @@ +package app + +import ( + "context" + "errors" + "fmt" + "net/http" + "time" + + "github.com/alexliesenfeld/health" + "github.com/go-chi/chi/v5" + "go.opentelemetry.io/otel/attribute" + "goa.design/clue/debug" + "goa.design/clue/log" + "goa.design/goa/v3/http/middleware" + + "github.com/jace-ys/countup/internal/ctxlog" + "github.com/jace-ys/countup/internal/healthz" + "github.com/jace-ys/countup/internal/transport/middleware/recovery" + "github.com/jace-ys/countup/internal/transport/middleware/reqid" + "github.com/jace-ys/countup/internal/transport/middleware/telemetry" +) + +type HTTPServer struct { + name string + addr string + srv *http.Server + mux *chi.Mux +} + +func NewHTTPServer(ctx context.Context, name string, port int) *HTTPServer { + addr := fmt.Sprintf(":%d", port) + + return &HTTPServer{ + name: name, + addr: addr, + srv: &http.Server{ + Addr: addr, + ReadHeaderTimeout: time.Second, + }, + mux: chi.NewRouter(), + } +} + +func (s *HTTPServer) RegisterHandler(path string, h http.Handler) { + pattern := path + "*" + s.mux.Handle(pattern, h) +} + +var _ Server = (*HTTPServer)(nil) + +func (s *HTTPServer) Name() string { + return s.name +} + +func (s *HTTPServer) Kind() string { + return "http" +} + +func (s *HTTPServer) Addr() string { + return s.addr +} + +func (s *HTTPServer) Serve(ctx context.Context) error { + s.srv.Handler = s.router(ctx) + if err := s.srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { + return fmt.Errorf("serving HTTP server: %w", err) + } + return nil +} + +func (s *HTTPServer) router(ctx context.Context) http.Handler { + s.mux.Get("/healthz", healthz.NewHTTPHandler()) + + excludedPaths := map[string]bool{ + "/healthz": true, + } + + logCtx := log.With(ctx, ctxlog.KV("server", s.Name())) + return chainMiddleware(s.mux, + withPathFilter(telemetry.HTTP(attribute.String("http.server.name", s.Name())), excludedPaths), + recovery.HTTP(logCtx), + withPathFilter(middleware.PopulateRequestContext(), excludedPaths), + withPathFilter(reqid.HTTP(), excludedPaths), + withPathFilter(ctxlog.HTTP(logCtx), excludedPaths), + withPathFilter(debug.HTTP(), excludedPaths), + ) +} + +func chainMiddleware(h http.Handler, m ...func(http.Handler) http.Handler) http.Handler { + for i := len(m) - 1; i >= 0; i-- { + h = m[i](h) + } + return h +} + +func withPathFilter(m func(http.Handler) http.Handler, excluded map[string]bool) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if excluded := excluded[r.URL.Path]; excluded { + next.ServeHTTP(w, r) + return + } + m(next).ServeHTTP(w, r) + }) + } +} + +func (s *HTTPServer) Shutdown(ctx context.Context) error { + return s.srv.Shutdown(ctx) //nolint:wrapcheck +} + +var _ healthz.Target = (*HTTPServer)(nil) + +func (s *HTTPServer) HealthChecks() []health.Check { + return []health.Check{ + healthz.HTTPCheck(s.Name(), fmt.Sprintf("http://%s/healthz", s.Addr())), + } +} diff --git a/app/internal/ctxlog/clue.go b/app/internal/ctxlog/clue.go new file mode 100644 index 0000000..5852095 --- /dev/null +++ b/app/internal/ctxlog/clue.go @@ -0,0 +1,63 @@ +package ctxlog + +import ( + "context" + "io" + + "goa.design/clue/log" +) + +func NewContext(ctx context.Context, w io.Writer, debug bool) context.Context { + format := log.FormatJSON + if log.IsTerminal() { + format = log.FormatTerminal + } + + opts := []log.LogOption{ + log.WithOutput(w), + log.WithFormat(format), + log.WithFileLocation(), + log.WithFunc(log.Span), + } + + if debug { + opts = append(opts, log.WithDebug()) + } + + return log.Context(ctx, opts...) +} + +func KV(key string, val any) log.KV { + return log.KV{K: key, V: val} +} + +func With(ctx context.Context, kv ...log.Fielder) context.Context { + return log.With(ctx, kv...) +} + +func Print(ctx context.Context, msg string, kv ...log.Fielder) { + log.Print(ctx, buildKVs(msg, kv...)...) +} + +func Debug(ctx context.Context, msg string, kv ...log.Fielder) { + log.Debug(ctx, buildKVs(msg, kv...)...) +} + +func Info(ctx context.Context, msg string, kv ...log.Fielder) { + log.Info(ctx, buildKVs(msg, kv...)...) +} + +func Error(ctx context.Context, msg string, err error, kv ...log.Fielder) { + log.Error(ctx, err, buildKVs(msg, kv...)...) +} + +func Fatal(ctx context.Context, msg string, err error, kv ...log.Fielder) { + log.Fatal(ctx, err, buildKVs(msg, kv...)...) +} + +func buildKVs(msg string, kv ...log.Fielder) []log.Fielder { + kvs := make([]log.Fielder, 0, len(kv)+1) + kvs = append(kvs, KV(log.MessageKey, msg)) + kvs = append(kvs, kv...) + return kvs +} diff --git a/app/internal/ctxlog/middleware.go b/app/internal/ctxlog/middleware.go new file mode 100644 index 0000000..4760661 --- /dev/null +++ b/app/internal/ctxlog/middleware.go @@ -0,0 +1,47 @@ +package ctxlog + +import ( + "context" + "net/http" + + "goa.design/clue/log" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + + "github.com/jace-ys/countup/internal/transport/middleware/reqid" +) + +func UnaryServerInterceptor(logCtx context.Context) grpc.UnaryServerInterceptor { + return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, next grpc.UnaryHandler) (_ any, err error) { + requestID := reqid.RequestIDFromContext(ctx) + + ctx = log.WithContext(ctx, logCtx) + ctx = log.With(ctx, KV(log.RequestIDKey, requestID)) + + opts := []log.GRPCLogOption{ + log.WithErrorFunc(func(c codes.Code) bool { return false }), + log.WithDisableCallID(), + } + + return log.UnaryServerInterceptor(ctx, opts...)(ctx, req, info, next) + } +} + +func HTTP(logCtx context.Context) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + requestID := reqid.RequestIDFromContext(ctx) + + ctx = log.WithContext(ctx, logCtx) + ctx = log.With(ctx, KV(log.RequestIDKey, requestID)) + + opts := []log.HTTPLogOption{ + log.WithDisableRequestID(), + } + + handler := log.HTTP(ctx, opts...)(next) + handler.ServeHTTP(w, r.WithContext(ctx)) + }) + } +} diff --git a/app/internal/ctxlog/slog.go b/app/internal/ctxlog/slog.go new file mode 100644 index 0000000..b720ee1 --- /dev/null +++ b/app/internal/ctxlog/slog.go @@ -0,0 +1,105 @@ +package ctxlog + +import ( + "context" + "errors" + "log/slog" + + "goa.design/clue/log" +) + +type SlogHandler struct { + ctx context.Context + level slog.Level + group string +} + +func AsSlogHandler(ctx context.Context, level slog.Level) *SlogHandler { + return &SlogHandler{ + ctx: log.Context(ctx, log.WithDebug()), + level: level, + } +} + +var _ slog.Handler = (*SlogHandler)(nil) + +func (l *SlogHandler) Enabled(ctx context.Context, level slog.Level) bool { + return level >= l.level +} + +func (l *SlogHandler) Handle(ctx context.Context, record slog.Record) error { + var attrs []log.Fielder + record.Attrs(func(a slog.Attr) bool { + attrs = append(attrs, KV(a.Key, a.Value.Any())) + return true + }) + + logCtx := log.WithContext(ctx, l.ctx) + logCtx = log.With(logCtx, attrs...) + + switch record.Level { + case slog.LevelDebug: + Debug(logCtx, record.Message) + case slog.LevelInfo: + Info(logCtx, record.Message) + case slog.LevelWarn: + Error(logCtx, "warn", errors.New(record.Message)) + case slog.LevelError: + Error(logCtx, "error", errors.New(record.Message)) + default: + Print(logCtx, record.Message) + } + + return nil +} + +func (l *SlogHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + kvs := make([]log.Fielder, len(attrs)) + for i, attr := range attrs { + key := attr.Key + if l.group != "" { + key = l.group + "." + attr.Key + } + kvs[i] = KV(key, attr.Value.Any()) + } + + cp := l.clone() + cp.ctx = log.With(cp.ctx, kvs...) + return cp +} + +func (l *SlogHandler) WithGroup(name string) slog.Handler { + cp := l.clone() + cp.group = name + return cp +} + +func (l *SlogHandler) clone() *SlogHandler { + cp := *l + return &cp +} + +type NopHandler struct { +} + +func AsNopHandler() *NopHandler { + return &NopHandler{} +} + +var _ slog.Handler = (*NopHandler)(nil) + +func (l NopHandler) Enabled(ctx context.Context, level slog.Level) bool { + return false +} + +func (l NopHandler) Handle(ctx context.Context, record slog.Record) error { + return nil +} + +func (l NopHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + return l +} + +func (l NopHandler) WithGroup(name string) slog.Handler { + return l +} diff --git a/app/internal/ctxlog/slog_test.go b/app/internal/ctxlog/slog_test.go new file mode 100644 index 0000000..6589233 --- /dev/null +++ b/app/internal/ctxlog/slog_test.go @@ -0,0 +1,184 @@ +package ctxlog_test + +import ( + "bufio" + "bytes" + "context" + "encoding/json" + "io" + "log/slog" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/jace-ys/countup/internal/ctxlog" +) + +func TestSlogHandler(t *testing.T) { + buf := &bytes.Buffer{} + ctx := ctxlog.NewContext(context.Background(), buf, false) + + logger := slog.New(ctxlog.AsSlogHandler(ctx, slog.LevelDebug)) + + logger.Debug("test debug", "count", 1.0) + logger.Info("test info", "count", 2.0) + logger.Warn("test warn", "count", 3.0) + logger.Error("test error", "count", 4.0) + + assertLogOutput(t, buf, []map[string]any{ + {"level": "debug", "msg": "test debug", "count": 1.0}, + {"level": "info", "msg": "test info", "count": 2.0}, + {"level": "error", "msg": "warn", "err": "test warn", "count": 3.0}, + {"level": "error", "msg": "error", "err": "test error", "count": 4.0}, + }) +} + +func TestSlogHandlerLevel(t *testing.T) { + buf := &bytes.Buffer{} + ctx := ctxlog.NewContext(context.Background(), buf, false) + + logger := slog.New(ctxlog.AsSlogHandler(ctx, slog.LevelWarn)) + + logger.DebugContext(ctx, "test debug", "count", 1.0) + logger.InfoContext(ctx, "test info", "count", 2.0) + logger.WarnContext(ctx, "test warn", "count", 3.0) + logger.ErrorContext(ctx, "test error", "count", 4.0) + + assertLogOutput(t, buf, []map[string]any{ + {"level": "error", "msg": "warn", "err": "test warn", "count": 3.0}, + {"level": "error", "msg": "error", "err": "test error", "count": 4.0}, + }) +} + +func TestSlogHandlerContext(t *testing.T) { + buf := &bytes.Buffer{} + ctx := ctxlog.NewContext(context.Background(), buf, false) + ctx = ctxlog.With(ctx, ctxlog.KV("foo", "bar")) + + logger := slog.New(ctxlog.AsSlogHandler(ctx, slog.LevelDebug)) + + logger.DebugContext(ctx, "test debug", "count", 1.0) + logger.InfoContext(ctx, "test info", "count", 2.0) + logger.WarnContext(ctx, "test warn", "count", 3.0) + logger.ErrorContext(ctx, "test error", "count", 4.0) + + ctx = ctxlog.With(ctx, ctxlog.KV("ping", "pong")) + + logger.DebugContext(ctx, "test debug", "count", 1.0) + logger.InfoContext(ctx, "test info", "count", 2.0) + logger.WarnContext(ctx, "test warn", "count", 3.0) + logger.ErrorContext(ctx, "test error", "count", 4.0) + + assertLogOutput(t, buf, []map[string]any{ + {"level": "debug", "msg": "test debug", "count": 1.0, "foo": "bar"}, + {"level": "info", "msg": "test info", "count": 2.0, "foo": "bar"}, + {"level": "error", "msg": "warn", "err": "test warn", "count": 3.0, "foo": "bar"}, + {"level": "error", "msg": "error", "err": "test error", "count": 4.0, "foo": "bar"}, + {"level": "debug", "msg": "test debug", "count": 1.0, "foo": "bar"}, + {"level": "info", "msg": "test info", "count": 2.0, "foo": "bar"}, + {"level": "error", "msg": "warn", "err": "test warn", "count": 3.0, "foo": "bar"}, + {"level": "error", "msg": "error", "err": "test error", "count": 4.0, "foo": "bar"}, + }) +} + +func TestSlogHandlerWith(t *testing.T) { + buf := &bytes.Buffer{} + ctx := ctxlog.NewContext(context.Background(), buf, false) + + logger := slog.New(ctxlog.AsSlogHandler(ctx, slog.LevelDebug)) + + logger.DebugContext(ctx, "test debug", "count", 1.0) + logger.InfoContext(ctx, "test info", "count", 2.0) + logger.WarnContext(ctx, "test warn", "count", 3.0) + logger.ErrorContext(ctx, "test error", "count", 4.0) + + logger = logger.With("ping", "pong") + + logger.DebugContext(ctx, "test debug", "count", 1.0) + logger.InfoContext(ctx, "test info", "count", 2.0) + logger.WarnContext(ctx, "test warn", "count", 3.0) + logger.ErrorContext(ctx, "test error", "count", 4.0) + + assertLogOutput(t, buf, []map[string]any{ + {"level": "debug", "msg": "test debug", "count": 1.0}, + {"level": "info", "msg": "test info", "count": 2.0}, + {"level": "error", "msg": "warn", "err": "test warn", "count": 3.0}, + {"level": "error", "msg": "error", "err": "test error", "count": 4.0}, + {"level": "debug", "msg": "test debug", "count": 1.0, "ping": "pong"}, + {"level": "info", "msg": "test info", "count": 2.0, "ping": "pong"}, + {"level": "error", "msg": "warn", "err": "test warn", "count": 3.0, "ping": "pong"}, + {"level": "error", "msg": "error", "err": "test error", "count": 4.0, "ping": "pong"}, + }) +} + +func TestSlogHandlerGroup(t *testing.T) { + buf := &bytes.Buffer{} + ctx := ctxlog.NewContext(context.Background(), buf, false) + + logger := slog.New(ctxlog.AsSlogHandler(ctx, slog.LevelDebug)) + + logger.DebugContext(ctx, "test debug", "count", 1.0) + logger.InfoContext(ctx, "test info", "count", 2.0) + logger.WarnContext(ctx, "test warn", "count", 3.0) + logger.ErrorContext(ctx, "test error", "count", 4.0) + + logger = logger.WithGroup("foo").With("bar", "baz").WithGroup("ping").With("pong", "table") + + logger.DebugContext(ctx, "test debug", "count", 1.0) + logger.InfoContext(ctx, "test info", "count", 2.0) + logger.WarnContext(ctx, "test warn", "count", 3.0) + logger.ErrorContext(ctx, "test error", "count", 4.0) + + assertLogOutput(t, buf, []map[string]any{ + {"level": "debug", "msg": "test debug", "count": 1.0}, + {"level": "info", "msg": "test info", "count": 2.0}, + {"level": "error", "msg": "warn", "err": "test warn", "count": 3.0}, + {"level": "error", "msg": "error", "err": "test error", "count": 4.0}, + {"level": "debug", "msg": "test debug", "count": 1.0, "foo.bar": "baz", "ping.pong": "table"}, + {"level": "info", "msg": "test info", "count": 2.0, "foo.bar": "baz", "ping.pong": "table"}, + {"level": "error", "msg": "warn", "err": "test warn", "count": 3.0, "foo.bar": "baz", "ping.pong": "table"}, + {"level": "error", "msg": "error", "err": "test error", "count": 4.0, "foo.bar": "baz", "ping.pong": "table"}, + }) +} + +func TestSlogHandlerConcurrent(t *testing.T) { + ctx := ctxlog.NewContext(context.Background(), io.Discard, false) + logger := slog.New(ctxlog.AsSlogHandler(ctx, slog.LevelDebug)) + + var wg sync.WaitGroup + for i := range 100 { + wg.Add(1) + go func(id int) { + defer wg.Done() + + if id%2 == 0 { + logger.Info("table", "ping", "pong") + } else { + logger.WithGroup("foo").With("bar", "baz").Info("table", "ping", "pong") + } + }(i) + } + + wg.Wait() +} + +func assertLogOutput(t *testing.T, r io.Reader, expected []map[string]any) { + scanner := bufio.NewScanner(r) + var actual []map[string]any + + for scanner.Scan() { + var data map[string]any + require.NoError(t, json.Unmarshal(scanner.Bytes(), &data)) + delete(data, "time") + delete(data, "file") + actual = append(actual, data) + } + + require.Len(t, actual, len(expected)) + + for i, data := range actual { + assert.Equal(t, expected[i], data) + } +} diff --git a/app/internal/endpoint/goa.go b/app/internal/endpoint/goa.go new file mode 100644 index 0000000..b3428ad --- /dev/null +++ b/app/internal/endpoint/goa.go @@ -0,0 +1,45 @@ +package endpoint + +import ( + "goa.design/clue/debug" + "goa.design/clue/log" + goa "goa.design/goa/v3/pkg" + + "github.com/jace-ys/countup/internal/endpoint/middleware/goaerror" + "github.com/jace-ys/countup/internal/endpoint/middleware/tracer" +) + +type GoaAdapter[S any, E GoaEndpoints] struct { + newFunc GoaNewFunc[S, E] +} + +type GoaEndpoints interface { + Use(func(goa.Endpoint) goa.Endpoint) +} + +type GoaNewFunc[S any, E GoaEndpoints] func(svc S) E + +func Goa[S any, E GoaEndpoints](newFunc GoaNewFunc[S, E]) *GoaAdapter[S, E] { + return &GoaAdapter[S, E]{ + newFunc: newFunc, + } +} + +func (a *GoaAdapter[S, E]) Adapt(svc S) E { + ep := a.newFunc(svc) + + chainMiddleware(ep, + tracer.Endpoint, + log.Endpoint, + debug.LogPayloads(), + goaerror.Reporter, + ) + + return ep +} + +func chainMiddleware[E GoaEndpoints](ep E, m ...func(goa.Endpoint) goa.Endpoint) { + for i := len(m) - 1; i >= 0; i-- { + ep.Use(m[i]) + } +} diff --git a/app/internal/endpoint/middleware/goaerror/reporter.go b/app/internal/endpoint/middleware/goaerror/reporter.go new file mode 100644 index 0000000..e1e9762 --- /dev/null +++ b/app/internal/endpoint/middleware/goaerror/reporter.go @@ -0,0 +1,37 @@ +package goaerror + +import ( + "context" + "errors" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" + goa "goa.design/goa/v3/pkg" + + "github.com/jace-ys/countup/internal/ctxlog" + "github.com/jace-ys/countup/internal/transport/middleware/reqid" +) + +func Reporter(e goa.Endpoint) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + res, err := e(ctx, req) + if err == nil { + return res, nil + } + + var gerr *goa.ServiceError + if !errors.As(err, &gerr) { + gerr = goa.Fault("an unexpected error occurred") + } + gerr.ID = reqid.RequestIDFromContext(ctx) + + ctxlog.Error(ctx, "endpoint error", err, ctxlog.KV("err.name", gerr.Name)) + + span := trace.SpanFromContext(ctx) + span.SetStatus(codes.Error, gerr.Name) + span.SetAttributes(attribute.String("error", err.Error())) + + return res, gerr + } +} diff --git a/app/internal/endpoint/middleware/tracer/endpoint.go b/app/internal/endpoint/middleware/tracer/endpoint.go new file mode 100644 index 0000000..1d70ce1 --- /dev/null +++ b/app/internal/endpoint/middleware/tracer/endpoint.go @@ -0,0 +1,32 @@ +package tracer + +import ( + "context" + "fmt" + + "go.opentelemetry.io/otel/attribute" + goa "goa.design/goa/v3/pkg" + + "github.com/jace-ys/countup/internal/instrument" +) + +func Endpoint(e goa.Endpoint) goa.Endpoint { + return func(ctx context.Context, req interface{}) (interface{}, error) { + service := "unknown" + if s, ok := ctx.Value(goa.ServiceKey).(string); ok { + service = s + } + + method := "Unknown" + if m, ok := ctx.Value(goa.MethodKey).(string); ok { + method = m + } + + source := fmt.Sprintf("goa.endpoint/%s.%s", service, method) + ctx, span := instrument.OTel.Tracer().Start(ctx, source) + span.SetAttributes(attribute.String("endpoint.service", service), attribute.String("endpoint.method", method)) + defer span.End() + + return e(ctx, req) + } +} diff --git a/app/internal/handler/api/auth.go b/app/internal/handler/api/auth.go new file mode 100644 index 0000000..114f7ab --- /dev/null +++ b/app/internal/handler/api/auth.go @@ -0,0 +1,116 @@ +package api + +import ( + "context" + "errors" + "fmt" + "slices" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/markbates/goth/providers/google" + goa "goa.design/goa/v3/pkg" + "goa.design/goa/v3/security" + + apiv1 "github.com/jace-ys/countup/api/v1" + "github.com/jace-ys/countup/api/v1/gen/api" + "github.com/jace-ys/countup/internal/service/user" +) + +const ( + jwtClaimsIssuer = "countup.api.AuthToken" + jwtClaimsAudience = "countup.api.JWTAuth" +) + +type AuthTokenClaims struct { + jwt.RegisteredClaims + + Scopes jwt.ClaimStrings `json:"scopes"` +} + +func (h *Handler) AuthToken(ctx context.Context, req *api.AuthTokenPayload) (*api.AuthTokenResult, error) { + session := &google.Session{ + AccessToken: req.AccessToken, + } + + fetchedUser, err := h.authn.FetchUser(session) + if err != nil { + return nil, goa.Fault("fetch user context from provider %s: %s", req.Provider, err) + } + + if fetchedUser.Email == "" { + return nil, api.MakeIncompleteAuthInfo(errors.New("missing field in fetched user context: email")) + } + + user, err := h.users.CreateUserIfNotExists(ctx, fetchedUser.Email) + if err != nil { + return nil, goa.Fault("create new user: %s", err) + } + + now := time.Now() + + claims := jwt.NewWithClaims(jwt.SigningMethodHS256, AuthTokenClaims{ + RegisteredClaims: jwt.RegisteredClaims{ + Issuer: jwtClaimsIssuer, + Audience: jwt.ClaimStrings{jwtClaimsAudience}, + Subject: user.ID, + IssuedAt: jwt.NewNumericDate(now), + NotBefore: jwt.NewNumericDate(now), + ExpiresAt: jwt.NewNumericDate(now.Add(24 * time.Hour)), + }, + Scopes: jwt.ClaimStrings{apiv1.AuthScopeAPIUser}, + }) + + tok, err := claims.SignedString(h.jwtSigningSecret) + if err != nil { + return nil, goa.Fault("sign JWT token: %s", err) + } + + return &api.AuthTokenResult{ + Token: tok, + }, nil +} + +var _ api.Auther = (*Handler)(nil) + +func (h *Handler) JWTAuth(ctx context.Context, token string, scheme *security.JWTScheme) (context.Context, error) { + parser := jwt.NewParser( + jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}), + jwt.WithIssuer(jwtClaimsIssuer), + jwt.WithAudience(jwtClaimsAudience), + jwt.WithIssuedAt(), + jwt.WithExpirationRequired(), + ) + + var claims AuthTokenClaims + tok, err := parser.ParseWithClaims(token, &claims, func(t *jwt.Token) (interface{}, error) { + return h.jwtSigningSecret, nil + }) + if err != nil { + return nil, api.MakeForbidden(fmt.Errorf("parse JWT auth token: %w", err)) + } + + if !tok.Valid { + return nil, api.MakeForbidden(errors.New("invalid JWT auth token")) + } + + var missingScopes []string + for _, scope := range scheme.RequiredScopes { + if !slices.Contains(claims.Scopes, scope) { + missingScopes = append(missingScopes, scope) + } + } + + if len(missingScopes) > 0 { + return nil, api.MakeForbidden( + fmt.Errorf("missing scopes in JWT auth token: %v", missingScopes), + ) + } + + usr, err := h.users.GetUser(ctx, claims.Subject) + if err != nil { + return nil, api.MakeForbidden(errors.New("access denied")) + } + + return user.ContextWithUser(ctx, usr), nil +} diff --git a/app/internal/handler/api/counter.go b/app/internal/handler/api/counter.go new file mode 100644 index 0000000..3656554 --- /dev/null +++ b/app/internal/handler/api/counter.go @@ -0,0 +1,43 @@ +package api + +import ( + "context" + "errors" + + goa "goa.design/goa/v3/pkg" + + "github.com/jace-ys/countup/api/v1/gen/api" + "github.com/jace-ys/countup/internal/service/counter" + "github.com/jace-ys/countup/internal/service/user" +) + +func (h *Handler) CounterGet(ctx context.Context) (*api.CounterInfo, error) { + info, err := h.counter.GetInfo(ctx) + if err != nil { + return nil, goa.Fault("get counter info: %s", err) + } + + return &api.CounterInfo{ + Count: info.Count, + LastIncrementBy: info.LastIncrementBy, + LastIncrementAt: info.LastIncrementAtTimestamp(), + NextFinalizeAt: info.NextFinalizeAtTimestamp(), + }, nil +} + +func (h *Handler) CounterIncrement(ctx context.Context, req *api.CounterIncrementPayload) (*api.CounterInfo, error) { + usr := user.UserFromContext(ctx) + + if err := h.counter.RequestIncrement(ctx, usr.Email); err != nil { + switch { + case errors.Is(err, &counter.MultipleIncrementRequestError{}): + return nil, api.MakeExistingIncrementRequest( + errors.New("user already made an increment request in the recent finalize window, please try again after the next finalize time"), + ) + default: + return nil, goa.Fault("request increment: %s", err) + } + } + + return h.CounterGet(ctx) +} diff --git a/app/internal/handler/api/handler.go b/app/internal/handler/api/handler.go new file mode 100644 index 0000000..0d4870a --- /dev/null +++ b/app/internal/handler/api/handler.go @@ -0,0 +1,55 @@ +package api + +import ( + "context" + + "github.com/alexliesenfeld/health" + "github.com/markbates/goth" + + "github.com/jace-ys/countup/api/v1/gen/api" + "github.com/jace-ys/countup/internal/healthz" + "github.com/jace-ys/countup/internal/service/counter" + "github.com/jace-ys/countup/internal/service/user" +) + +var _ api.Service = (*Handler)(nil) + +type Handler struct { + authn goth.Provider + counter CounterService + users UserService + + jwtSigningSecret []byte +} + +type CounterService interface { + GetInfo(ctx context.Context) (*counter.Info, error) + RequestIncrement(ctx context.Context, user string) error +} + +type UserService interface { + GetUser(ctx context.Context, id string) (*user.User, error) + CreateUserIfNotExists(ctx context.Context, email string) (*user.User, error) +} + +func NewHandler(authn goth.Provider, counter CounterService, users UserService) (*Handler, error) { + return &Handler{ + authn: authn, + counter: counter, + users: users, + jwtSigningSecret: []byte("secret"), + }, nil +} + +var _ healthz.Target = (*Handler)(nil) + +func (h *Handler) HealthChecks() []health.Check { + return []health.Check{ + { + Name: "handler:api", + Check: func(ctx context.Context) error { + return nil + }, + }, + } +} diff --git a/app/internal/handler/teapot/echo.go b/app/internal/handler/teapot/echo.go new file mode 100644 index 0000000..004725f --- /dev/null +++ b/app/internal/handler/teapot/echo.go @@ -0,0 +1,72 @@ +package teapot + +import ( + "context" + "errors" + "strings" + "time" + + "github.com/riverqueue/river" + + "github.com/jace-ys/countup/api/v1/gen/teapot" +) + +func (h *Handler) Echo(ctx context.Context, req *teapot.EchoPayload) (*teapot.EchoResult, error) { + switch { + case strings.HasPrefix(req.Text, "error: "): + msg := strings.TrimPrefix(req.Text, "error: ") + h.workers.Enqueue(ctx, &EchoJobArgs{Error: msg}) + return nil, teapot.MakeUnwell(errors.New(msg)) + + case strings.HasPrefix(req.Text, "panic: "): + msg := strings.TrimPrefix(req.Text, "panic: ") + h.workers.Enqueue(ctx, &EchoJobArgs{Panic: msg}) + panic(msg) + + case strings.HasPrefix(req.Text, "cancel: "): + msg := strings.TrimPrefix(req.Text, "cancel: ") + h.workers.Enqueue(ctx, &EchoJobArgs{Cancel: msg}) + return nil, teapot.MakeUnwell(errors.New(msg)) + + case strings.HasPrefix(req.Text, "sleep: "): + msg := strings.TrimPrefix(req.Text, "sleep: ") + duration, err := time.ParseDuration(msg) + if err != nil { + duration = time.Second + } + time.Sleep(duration) + h.workers.Enqueue(ctx, &EchoJobArgs{Sleep: duration}) + } + + return &teapot.EchoResult{Text: req.Text}, nil +} + +type EchoJobArgs struct { + Error string + Panic string + Cancel string + Sleep time.Duration +} + +func (EchoJobArgs) Kind() string { + return "countup.Echo" +} + +type EchoWorker struct { + river.WorkerDefaults[EchoJobArgs] +} + +func (w *EchoWorker) Work(ctx context.Context, job *river.Job[EchoJobArgs]) error { + switch { + case job.Args.Error != "": + return errors.New(job.Args.Error) + case job.Args.Panic != "": + panic(job.Args.Panic) + case job.Args.Cancel != "": + return river.JobCancel(errors.New(job.Args.Cancel)) + case job.Args.Sleep != 0: + time.Sleep(job.Args.Sleep) + } + + return nil +} diff --git a/app/internal/handler/teapot/handler.go b/app/internal/handler/teapot/handler.go new file mode 100644 index 0000000..3c71afb --- /dev/null +++ b/app/internal/handler/teapot/handler.go @@ -0,0 +1,38 @@ +package teapot + +import ( + "context" + + "github.com/alexliesenfeld/health" + + "github.com/jace-ys/countup/api/v1/gen/teapot" + "github.com/jace-ys/countup/internal/healthz" + "github.com/jace-ys/countup/internal/worker" +) + +var _ teapot.Service = (*Handler)(nil) + +type Handler struct { + workers *worker.Pool +} + +func NewHandler(workers *worker.Pool) (*Handler, error) { + worker.Register(workers, &EchoWorker{}) + + return &Handler{ + workers: workers, + }, nil +} + +var _ healthz.Target = (*Handler)(nil) + +func (h *Handler) HealthChecks() []health.Check { + return []health.Check{ + { + Name: "handler:teapot", + Check: func(ctx context.Context) error { + return nil + }, + }, + } +} diff --git a/app/internal/handler/web/authn.go b/app/internal/handler/web/authn.go new file mode 100644 index 0000000..e038b02 --- /dev/null +++ b/app/internal/handler/web/authn.go @@ -0,0 +1,119 @@ +package web + +import ( + "context" + "errors" + "fmt" + "net/url" + + goa "goa.design/goa/v3/pkg" + + apiv1 "github.com/jace-ys/countup/api/v1" + "github.com/jace-ys/countup/api/v1/gen/api" + "github.com/jace-ys/countup/api/v1/gen/web" + "github.com/jace-ys/countup/internal/transport/middleware/reqid" +) + +const sessionName = "countup.session" + +type authSession struct { + State string + Token string + Session string +} + +func (h *Handler) LoginGoogle(ctx context.Context) (*web.LoginGoogleResult, error) { + reqID := reqid.RequestIDFromContext(ctx) + + session, err := h.authn.BeginAuth(reqID) + if err != nil { + return nil, web.MakeUnauthorized(fmt.Errorf("decode session cookie: %w", err)) + } + + redirectURL, err := session.GetAuthURL() + if err != nil { + return nil, web.MakeUnauthorized(fmt.Errorf("get redirection URL: %w", err)) + } + + sessionCookie, err := h.cookies.Encode(sessionName, &authSession{ + State: reqID, + Session: session.Marshal(), + }) + if err != nil { + return nil, web.MakeUnauthorized(fmt.Errorf("encode session cookie: %w", err)) + } + + return &web.LoginGoogleResult{ + RedirectURL: redirectURL, + SessionCookie: sessionCookie, + }, nil +} + +func (h *Handler) LoginGoogleCallback(ctx context.Context, req *web.LoginGoogleCallbackPayload) (*web.LoginGoogleCallbackResult, error) { + var auth *authSession + if err := h.cookies.Decode(sessionName, req.SessionCookie, &auth); err != nil { + return nil, web.MakeUnauthorized(fmt.Errorf("decode session cookie: %w", err)) + } + + if req.State != auth.State { + return nil, web.MakeUnauthorized(errors.New("invalid state")) + } + + session, err := h.authn.UnmarshalSession(auth.Session) + if err != nil { + return nil, web.MakeUnauthorized(fmt.Errorf("unmarshal session data: %w", err)) + } + + params := url.Values{} + params.Set("code", req.Code) + + accessToken, err := session.Authorize(h.authn, params) + if err != nil { + return nil, web.MakeUnauthorized(fmt.Errorf("authorize session: %w", err)) + } + + res, err := h.api.AuthToken(ctx, &api.AuthTokenPayload{ + Provider: "google", + AccessToken: accessToken, + }) + if err != nil { + var gerr *goa.ServiceError + if errors.As(err, &gerr) { + switch gerr.Name { + case apiv1.ErrCodeIncompleteAuthInfo: + return nil, web.MakeUnauthorized(errors.New("authentication failed")) + } + } + return nil, web.MakeUnauthorized(errors.New("failed to exchange auth token")) + } + + sessionCookie, err := h.cookies.Encode(sessionName, &authSession{ + Token: res.Token, + }) + if err != nil { + return nil, web.MakeUnauthorized(fmt.Errorf("encode session cookie: %w", err)) + } + + return &web.LoginGoogleCallbackResult{ + RedirectURL: "/", + SessionCookie: sessionCookie, + }, nil +} + +func (h *Handler) Logout(ctx context.Context, req *web.LogoutPayload) (*web.LogoutResult, error) { + return &web.LogoutResult{ + RedirectURL: "/", + SessionCookie: "", + }, nil +} + +func (h *Handler) SessionToken(ctx context.Context, req *web.SessionTokenPayload) (*web.SessionTokenResult, error) { + var auth *authSession + if err := h.cookies.Decode(sessionName, req.SessionCookie, &auth); err != nil { + return nil, web.MakeUnauthorized(fmt.Errorf("decode session cookie: %w", err)) + } + + return &web.SessionTokenResult{ + Token: auth.Token, + }, nil +} diff --git a/app/internal/handler/web/handler.go b/app/internal/handler/web/handler.go new file mode 100644 index 0000000..9fb07d6 --- /dev/null +++ b/app/internal/handler/web/handler.go @@ -0,0 +1,60 @@ +package web + +import ( + "context" + "embed" + "fmt" + "html/template" + + "github.com/alexliesenfeld/health" + "github.com/gorilla/securecookie" + "github.com/markbates/goth" + + "github.com/jace-ys/countup/api/v1/gen/api" + "github.com/jace-ys/countup/api/v1/gen/web" + "github.com/jace-ys/countup/internal/healthz" +) + +var ( + //go:embed static/* + StaticFS embed.FS + + //go:embed templates/* + templateFS embed.FS +) + +var _ web.Service = (*Handler)(nil) + +type Handler struct { + authn goth.Provider + cookies *securecookie.SecureCookie + api *api.Client + tmpls *template.Template +} + +func NewHandler(authn goth.Provider, cookies *securecookie.SecureCookie, api *api.Client) (*Handler, error) { + tmpls, err := template.New("tmpls").ParseFS(templateFS, "**/*.html") + if err != nil { + return nil, fmt.Errorf("parse templates: %w", err) + } + + return &Handler{ + authn: authn, + cookies: cookies.MaxAge(86400), + api: api, + tmpls: tmpls, + }, nil +} + +var _ healthz.Target = (*Handler)(nil) + +func (h *Handler) HealthChecks() []health.Check { + return []health.Check{ + { + Name: "handler:web", + Check: func(ctx context.Context) error { + return nil + }, + }, + } +} diff --git a/app/internal/handler/web/pages.go b/app/internal/handler/web/pages.go new file mode 100644 index 0000000..6322dfe --- /dev/null +++ b/app/internal/handler/web/pages.go @@ -0,0 +1,39 @@ +package web + +import ( + "bytes" + "context" + "fmt" +) + +func (h *Handler) Index(ctx context.Context) ([]byte, error) { + data := struct { + Name string + }{ + Name: "Count Up!", + } + + return h.render("index.html", data) +} + +func (h *Handler) Another(ctx context.Context) ([]byte, error) { + data := struct { + Name string + }{ + Name: "Page", + } + + return h.render("another.html", data) +} + +type renderData struct { + Data any +} + +func (h *Handler) render(page string, data any) ([]byte, error) { + buf := &bytes.Buffer{} + if err := h.tmpls.ExecuteTemplate(buf, page, renderData{data}); err != nil { + return nil, fmt.Errorf("render: %w", err) + } + return buf.Bytes(), nil +} diff --git a/app/internal/handler/web/static/assets/gopher.png b/app/internal/handler/web/static/assets/gopher.png new file mode 100644 index 0000000..33e8514 Binary files /dev/null and b/app/internal/handler/web/static/assets/gopher.png differ diff --git a/app/internal/handler/web/static/css/main.css b/app/internal/handler/web/static/css/main.css new file mode 100644 index 0000000..595d862 --- /dev/null +++ b/app/internal/handler/web/static/css/main.css @@ -0,0 +1,45 @@ +@import url(https://fonts.googleapis.com/css?family=Montserrat:400,700); +h1 { + text-align: center; +} +body { + background-color: #D2691E; + color: white; + font-family: 'Montserrat', sans-serif; + font-weight:400; +} +.white-box { + background-color: #FFFFFF; + color: #D2691E; + margin: 40px 18%; + padding: 5% 5% 5% 10%; + font-family: 'Montserrat', sans-serif; + font-weight:300; +} + +.random-quote { + font-size: 1.5em; + text-align: center; + font-weight: 500; +} +.random-author { + font-size: 1.2em; + text-align: right; +} +.button { + background-color: #D2691E; + color: #FFFFFF; + border: none; + float: left; + margin: 15px 5px auto auto; + padding: 5px 10px 5px 10px; +} +.button { + opacity: 1; +} +.button:hover { + opacity: 0.75; +} +.new-quote-button { + float: right; +} diff --git a/app/internal/handler/web/static/js/main.js b/app/internal/handler/web/static/js/main.js new file mode 100644 index 0000000..0ce8cc6 --- /dev/null +++ b/app/internal/handler/web/static/js/main.js @@ -0,0 +1,28 @@ +var randomQuote = ""; +var randomAuthor = ""; + +function getQuote() { + $.ajax({ + url: "https://api.forismatic.com/api/1.0/?method=getQuote&lang=en&format=jsonp&jsonp=?", + method: "GET", + dataType: "jsonp", + success: function (request) { + randomQuote = request.quoteText; + randomAuthor = request.quoteAuthor; + $('#text').html(randomQuote); + if (randomAuthor === "") { + randomAuthor = "Unknown"; + } $('#author').html(randomAuthor); + }, + error: function (xhr, status, error) { + $('#quoteText').text('Not sure what happened there! Click again to generate a new quote!'); + $('#quoteAuthor').text('Click Below!'); + } + }); +} + +$(document).ready(function () { + $("#new-quote").click(function () { + getQuote(); + }); +}); \ No newline at end of file diff --git a/app/internal/handler/web/static/quote.html b/app/internal/handler/web/static/quote.html new file mode 100644 index 0000000..de33e10 --- /dev/null +++ b/app/internal/handler/web/static/quote.html @@ -0,0 +1,33 @@ + + + + + + + Countup + + + + +

Random Quote Generator

+
+
+ +

+
+
- +
+
+
+
+
+
+ + + + + + \ No newline at end of file diff --git a/app/internal/handler/web/templates/another.html b/app/internal/handler/web/templates/another.html new file mode 100644 index 0000000..94158a3 --- /dev/null +++ b/app/internal/handler/web/templates/another.html @@ -0,0 +1,15 @@ + + + + + + + Count Up! + + + +

Another {{ .Data.Name }}?

+ Back + + + \ No newline at end of file diff --git a/app/internal/handler/web/templates/index.html b/app/internal/handler/web/templates/index.html new file mode 100644 index 0000000..8ff9222 --- /dev/null +++ b/app/internal/handler/web/templates/index.html @@ -0,0 +1,23 @@ + + + + + + + Count Up! + + + +

Hello {{ .Data.Name }}

+
+ +
+

+ +

+ +

+ + + + \ No newline at end of file diff --git a/app/internal/healthz/checker.go b/app/internal/healthz/checker.go new file mode 100644 index 0000000..70d510e --- /dev/null +++ b/app/internal/healthz/checker.go @@ -0,0 +1,21 @@ +package healthz + +import ( + "github.com/alexliesenfeld/health" +) + +type Target interface { + HealthChecks() []health.Check +} + +func NewChecker(checks ...health.Check) health.Checker { + opts := []health.CheckerOption{ + health.WithDisabledAutostart(), + } + + for _, check := range checks { + opts = append(opts, health.WithCheck(check)) + } + + return health.NewChecker(opts...) +} diff --git a/app/internal/healthz/grpc.go b/app/internal/healthz/grpc.go new file mode 100644 index 0000000..bfeeb78 --- /dev/null +++ b/app/internal/healthz/grpc.go @@ -0,0 +1,39 @@ +package healthz + +import ( + "context" + "fmt" + + "github.com/alexliesenfeld/health" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + healthpb "google.golang.org/grpc/health/grpc_health_v1" +) + +func GRPCCheck(name, target string) health.Check { + return health.Check{ + Name: "grpc:" + name, + Check: func(ctx context.Context) error { + opts := []grpc.DialOption{ + grpc.WithTransportCredentials(insecure.NewCredentials()), + } + + conn, err := grpc.NewClient(target, opts...) + if err != nil { + return fmt.Errorf("create gRPC client: %w", err) + } + defer conn.Close() + + res, err := healthpb.NewHealthClient(conn).Check(ctx, &healthpb.HealthCheckRequest{}) + if err != nil { + return fmt.Errorf("send gRPC request: %w", err) + } + + if res.GetStatus() != healthpb.HealthCheckResponse_SERVING { + return fmt.Errorf("gRPC service reported as non-serving: %q", res.GetStatus().String()) + } + + return nil + }, + } +} diff --git a/app/internal/healthz/handler.go b/app/internal/healthz/handler.go new file mode 100644 index 0000000..23e4993 --- /dev/null +++ b/app/internal/healthz/handler.go @@ -0,0 +1,33 @@ +package healthz + +import ( + "context" + "net/http" + + "google.golang.org/grpc/codes" + healthpb "google.golang.org/grpc/health/grpc_health_v1" + "google.golang.org/grpc/status" +) + +func NewHTTPHandler() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + } +} + +type GRPCHandler struct { +} + +func NewGRPCHandler() healthpb.HealthServer { + return &GRPCHandler{} +} + +func (h *GRPCHandler) Check(ctx context.Context, req *healthpb.HealthCheckRequest) (*healthpb.HealthCheckResponse, error) { + return &healthpb.HealthCheckResponse{ + Status: healthpb.HealthCheckResponse_SERVING, + }, nil +} + +func (h *GRPCHandler) Watch(req *healthpb.HealthCheckRequest, server healthpb.Health_WatchServer) error { + return status.Errorf(codes.Unimplemented, "method Watch not implemented") +} diff --git a/app/internal/healthz/http.go b/app/internal/healthz/http.go new file mode 100644 index 0000000..9ee4816 --- /dev/null +++ b/app/internal/healthz/http.go @@ -0,0 +1,34 @@ +package healthz + +import ( + "context" + "fmt" + "net/http" + + "github.com/alexliesenfeld/health" +) + +func HTTPCheck(name, url string) health.Check { + return health.Check{ + Name: "http:" + name, + Check: func(ctx context.Context) error { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return fmt.Errorf("create HTTP request: %w", err) + } + req.Header.Set("Connection", "close") + + res, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("send HTTP request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode >= http.StatusInternalServerError { + return fmt.Errorf("HTTP service reported as non-healthy: %d", res.StatusCode) + } + + return nil + }, + } +} diff --git a/app/internal/idgen/id.go b/app/internal/idgen/id.go new file mode 100644 index 0000000..e3d813c --- /dev/null +++ b/app/internal/idgen/id.go @@ -0,0 +1,10 @@ +package idgen + +import ( + "github.com/rs/xid" +) + +func NewID(prefix string) string { + id := xid.New() + return prefix + "_" + id.String() +} diff --git a/app/internal/instrument/otel.go b/app/internal/instrument/otel.go new file mode 100644 index 0000000..285cce4 --- /dev/null +++ b/app/internal/instrument/otel.go @@ -0,0 +1,123 @@ +package instrument + +import ( + "context" + "errors" + "fmt" + "time" + + "go.opentelemetry.io/contrib/instrumentation/runtime" + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/resource" + "go.opentelemetry.io/otel/trace" + "goa.design/clue/clue" + + "github.com/jace-ys/countup/internal/versioninfo" +) + +var OTel *OTelProvider + +type OTelProvider struct { + cfg *clue.Config + shutdownFuncs []func(context.Context) error +} + +func MustInitOTelProvider(ctx context.Context, name, version, otlpMetricsEndpoint, otlpTracesEndpoint string) { + if err := InitOTelProvider(ctx, name, version, otlpMetricsEndpoint, otlpTracesEndpoint); err != nil { + panic(err) + } +} + +func InitOTelProvider(ctx context.Context, name, version, otlpMetricsEndpoint, otlpTracesEndpoint string) error { + var shutdownFuncs []func(context.Context) error + + metrics, err := otlpmetricgrpc.New(ctx, + otlpmetricgrpc.WithEndpoint(otlpMetricsEndpoint), + otlpmetricgrpc.WithInsecure(), + ) + if err != nil { + return fmt.Errorf("init metrics exporter: %w", err) + } + shutdownFuncs = append(shutdownFuncs, metrics.Shutdown) + + traces, err := otlptracegrpc.New(ctx, + otlptracegrpc.WithEndpoint(otlpTracesEndpoint), + otlptracegrpc.WithInsecure(), + ) + if err != nil { + return fmt.Errorf("init trace exporter: %w", err) + } + shutdownFuncs = append(shutdownFuncs, traces.Shutdown) + + opts := []clue.Option{ + clue.WithResource(resource.Environment()), + clue.WithReaderInterval(30 * time.Second), + } + + cfg, err := clue.NewConfig(ctx, name, version, metrics, traces, opts...) + if err != nil { + return fmt.Errorf("init otel provider: %w", err) + } + clue.ConfigureOpenTelemetry(ctx, cfg) + + OTel = &OTelProvider{ + cfg: cfg, + shutdownFuncs: shutdownFuncs, + } + + if err := OTel.initMetrics(ctx); err != nil { + return fmt.Errorf("init metrics: %w", err) + } + + return nil +} + +func (i *OTelProvider) initMetrics(ctx context.Context) error { + if err := runtime.Start(); err != nil { + return fmt.Errorf("runtime: %w", err) + } + + up, err := OTel.Meter().Int64Gauge("up") + if err != nil { + return fmt.Errorf("up: %w", err) + } + up.Record(ctx, 1) + + return nil +} + +func (i *OTelProvider) Shutdown(ctx context.Context) error { + select { + case <-time.After(5 * time.Second): + case <-ctx.Done(): + return ctx.Err() + } + + shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + var errs error + for _, shutdown := range i.shutdownFuncs { + errs = errors.Join(errs, shutdown(shutdownCtx)) + } + + i.shutdownFuncs = nil + return errs +} + +const scope = "github.com/jace-ys/countup/internal/instrument" + +func (i *OTelProvider) Meter() metric.Meter { + return i.cfg.MeterProvider.Meter(scope, metric.WithInstrumentationVersion(versioninfo.Version)) +} + +func (i *OTelProvider) Tracer() trace.Tracer { + return i.cfg.TracerProvider.Tracer(scope, trace.WithInstrumentationVersion(versioninfo.Version)) +} + +func (i *OTelProvider) Propagators() propagation.TextMapPropagator { + return i.cfg.Propagators +} diff --git a/app/internal/instrument/panic.go b/app/internal/instrument/panic.go new file mode 100644 index 0000000..e8ee492 --- /dev/null +++ b/app/internal/instrument/panic.go @@ -0,0 +1,53 @@ +package instrument + +import ( + "context" + "fmt" + "sync" + + "github.com/go-chi/chi/v5/middleware" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/metric/noop" + "go.opentelemetry.io/otel/trace" + + "github.com/jace-ys/countup/internal/ctxlog" +) + +var metrics struct { + init sync.Once + goPanicsRecoveredTotal metric.Int64Counter +} + +func initMetrics(ctx context.Context) { + metrics.init.Do(func() { + var err error + metrics.goPanicsRecoveredTotal, err = OTel.Meter().Int64Counter("go.panics.recovered.total") + if err != nil { + ctxlog.Error(ctx, "error initializing metrics", err) + metrics.goPanicsRecoveredTotal, _ = noop.Meter{}.Int64Counter("go.panics.recovered.total") //nolint:errcheck + } + }) +} + +func EmitRecoveredPanicTelemetry(ctx context.Context, rvr any, source string) { + initMetrics(ctx) + + err := fmt.Errorf("%v", rvr) + + ctxlog.Error(ctx, "recovered from panic", err, ctxlog.KV("panic.source", source)) + middleware.PrintPrettyStack(rvr) + + attrs := []attribute.KeyValue{ + attribute.String("panic.source", source), + } + + span := trace.SpanFromContext(ctx) + span.SetStatus(codes.Error, "recovered from panic") + span.SetAttributes(attribute.Bool("panic.recovered", true)) + span.SetAttributes(attrs...) + span.RecordError(err, trace.WithStackTrace(true)) + + metrics.goPanicsRecoveredTotal.Add(ctx, 1, metric.WithAttributes(attrs...)) +} diff --git a/app/internal/postgres/pool.go b/app/internal/postgres/pool.go new file mode 100644 index 0000000..795eabf --- /dev/null +++ b/app/internal/postgres/pool.go @@ -0,0 +1,67 @@ +package postgres + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/exaring/otelpgx" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" + "go.opentelemetry.io/otel/attribute" +) + +type Pool struct { + *pgxpool.Pool +} + +func NewPool(ctx context.Context, connString string) (*Pool, error) { + cfg, err := pgxpool.ParseConfig(connString) + if err != nil { + return nil, fmt.Errorf("parse config: %w", err) + } + + attrs := []attribute.KeyValue{ + attribute.String("db.database", cfg.ConnConfig.Database), + } + + cfg.ConnConfig.Tracer = otelpgx.NewTracer( + otelpgx.WithAttributes(attrs...), + otelpgx.WithTrimSQLInSpanName(), + otelpgx.WithSpanNameFunc(func(stmt string) string { + idx := strings.IndexRune(stmt, '\n') + if idx >= 0 { + return stmt[:idx] + } + return stmt + }), + ) + + cfg.ConnConfig.ConnectTimeout = 10 * time.Second + + pool, err := pgxpool.NewWithConfig(ctx, cfg) + if err != nil { + return nil, fmt.Errorf("create pool: %w", err) + } + + return &Pool{pool}, nil +} + +func (p *Pool) WithinTx(ctx context.Context, fn func(ctx context.Context, tx pgx.Tx) error) error { + tx, err := p.Begin(ctx) + if err != nil { + return fmt.Errorf("tx begin: %w", err) + } + defer tx.Rollback(ctx) + + if err := fn(ctx, tx); err != nil { + return err + } + + if err := tx.Commit(ctx); err != nil { + return fmt.Errorf("tx commit: %w", err) + } + + return nil +} diff --git a/app/internal/service/counter/errors.go b/app/internal/service/counter/errors.go new file mode 100644 index 0000000..37fe879 --- /dev/null +++ b/app/internal/service/counter/errors.go @@ -0,0 +1,28 @@ +package counter + +import ( + "errors" + "time" +) + +var ( + ErrGetCounter = errors.New("store get counter") + ErrIncrementCounter = errors.New("store increment counter") + ErrUpdateCounterFinalizeTime = errors.New("store update counter finalize time") + ErrResetCounter = errors.New("store reset counter") + + ErrListIncrementRequests = errors.New("store list increment requests") + ErrInsertIncrementRequest = errors.New("store insert increment request") + ErrTruncateIncrementRequests = errors.New("store truncate increment requests") + + ErrEnqueueFinalizeIncrement = errors.New("enqueue finalize increment job") +) + +type MultipleIncrementRequestError struct { + User string + FinalizeWindow time.Duration +} + +func (e *MultipleIncrementRequestError) Error() string { + return "multiple increment request by user in finalize window" +} diff --git a/app/internal/service/counter/finalize.go b/app/internal/service/counter/finalize.go new file mode 100644 index 0000000..fd2f419 --- /dev/null +++ b/app/internal/service/counter/finalize.go @@ -0,0 +1,89 @@ +package counter + +import ( + "context" + "fmt" + "time" + + "github.com/jackc/pgx/v5/pgtype" + "github.com/riverqueue/river" + + "github.com/jace-ys/countup/internal/ctxlog" + "github.com/jace-ys/countup/internal/postgres" + counterstore "github.com/jace-ys/countup/internal/service/counter/store" +) + +type FinalizeIncrementJobArgs struct { + FinalizeWindow time.Duration +} + +func (FinalizeIncrementJobArgs) Kind() string { + return "counter.FinalizeIncrement" +} + +type FinalizeIncrementWorker struct { + river.WorkerDefaults[FinalizeIncrementJobArgs] + + db *postgres.Pool + store counterstore.Querier +} + +func NewIncrementWorker(db *postgres.Pool, store counterstore.Querier) *FinalizeIncrementWorker { + return &FinalizeIncrementWorker{ + db: db, + store: store, + } +} + +func (w *FinalizeIncrementWorker) Work(ctx context.Context, job *river.Job[FinalizeIncrementJobArgs]) error { + ctx = ctxlog.With(ctx, ctxlog.KV("finalize.window", job.Args.FinalizeWindow)) + + ctxlog.Info(ctx, "listing increment requests") + + requests, err := w.store.ListIncrementRequests(ctx, w.db) + if err != nil { + return fmt.Errorf("%w: %w", ErrListIncrementRequests, err) + } + + switch len(requests) { + case 0: + ctxlog.Info(ctx, "no increment requests in finalize window, returning") + return nil + + case 1: + ctxlog.Info(ctx, "only one increment request in finalize window, incrementing counter", + ctxlog.KV("finalize.user", requests[0].RequestedBy), + ) + + if err := w.store.IncrementCounter(ctx, w.db, counterstore.IncrementCounterParams{ + LastIncrementBy: pgtype.Text{ + String: requests[0].RequestedBy, + Valid: true, + }, + LastIncrementAt: pgtype.Timestamptz{ + Time: time.Now(), + Valid: true, + }, + }); err != nil { + return fmt.Errorf("%w: %w", ErrIncrementCounter, err) + } + + default: + ctxlog.Info(ctx, "multiple increment requests in finalize window, resetting counter", + ctxlog.KV("requests.count", len(requests)), + ctxlog.KV("finalize.user", requests[0].RequestedBy), + ) + + if err := w.store.ResetCounter(ctx, w.db); err != nil { + return fmt.Errorf("%w: %w", ErrResetCounter, err) + } + } + + ctxlog.Info(ctx, "deleting increment requests") + + if err := w.store.DeleteIncrementRequests(ctx, w.db); err != nil { + return fmt.Errorf("%w: %w", ErrTruncateIncrementRequests, err) + } + + return nil +} diff --git a/app/internal/service/counter/service.go b/app/internal/service/counter/service.go new file mode 100644 index 0000000..e12b4d6 --- /dev/null +++ b/app/internal/service/counter/service.go @@ -0,0 +1,113 @@ +package counter + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/jackc/pgerrcode" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + "github.com/jackc/pgx/v5/pgtype" + + "github.com/jace-ys/countup/internal/ctxlog" + "github.com/jace-ys/countup/internal/postgres" + counterstore "github.com/jace-ys/countup/internal/service/counter/store" + "github.com/jace-ys/countup/internal/worker" +) + +type Service struct { + db *postgres.Pool + workers *worker.Pool + store counterstore.Querier + + finalizeWindow time.Duration +} + +func New(db *postgres.Pool, workers *worker.Pool, store counterstore.Querier, finalizeWindow time.Duration) *Service { + worker.Register(workers, &FinalizeIncrementWorker{ + db: db, + store: store, + }) + + return &Service{ + db: db, + workers: workers, + store: store, + finalizeWindow: finalizeWindow, + } +} + +func (s *Service) GetInfo(ctx context.Context) (*Info, error) { + ctxlog.Info(ctx, "getting counter") + + counter, err := s.store.GetCounter(ctx, s.db) + if err != nil { + return nil, fmt.Errorf("%w: %w", ErrGetCounter, err) + } + + return &Info{ + Count: counter.Count, + LastIncrementBy: counter.LastIncrementBy.String, + LastIncrementAt: counter.LastIncrementAt.Time, + NextFinalizeAt: counter.NextFinalizeAt.Time, + }, nil +} + +func (s *Service) RequestIncrement(ctx context.Context, user string) error { + return s.db.WithinTx(ctx, func(ctx context.Context, tx pgx.Tx) error { + ctxlog.Info(ctx, "inserting increment request") + + reqCount, err := s.store.InsertIncrementRequest(ctx, tx, counterstore.InsertIncrementRequestParams{ + RequestedBy: user, + RequestedAt: pgtype.Timestamptz{ + Time: time.Now(), + Valid: true, + }, + }) + if err != nil { + var pgErr *pgconn.PgError + if errors.As(err, &pgErr) { + switch pgErr.Code { + case pgerrcode.UniqueViolation: + return &MultipleIncrementRequestError{user, s.finalizeWindow} + } + } + return fmt.Errorf("%w: %w", ErrInsertIncrementRequest, err) + } + + if reqCount > 1 { + ctxlog.Info(ctx, "existing increment requests in finalize window, skip enqueuing finalize job", + ctxlog.KV("requests.count", reqCount), + ctxlog.KV("finalize.window", s.finalizeWindow), + ) + return nil + } + + ctxlog.Info(ctx, "first increment request in finalize window, enqueuing finalize job", + ctxlog.KV("finalize.window", s.finalizeWindow), + ) + + finalizeAt := time.Now().Add(s.finalizeWindow) + + if err := s.workers.EnqueueTx(ctx, tx, FinalizeIncrementJobArgs{ + FinalizeWindow: s.finalizeWindow, + }, + worker.WithSchedule(finalizeAt), + ); err != nil { + return fmt.Errorf("%w: %w", ErrEnqueueFinalizeIncrement, err) + } + + ctxlog.Info(ctx, "updating counter finalize time", ctxlog.KV("finalize.at", finalizeAt)) + + if err := s.store.UpdateCounterFinalizeTime(ctx, tx, pgtype.Timestamptz{ + Time: finalizeAt, + Valid: true, + }); err != nil { + return fmt.Errorf("%w: %w", ErrUpdateCounterFinalizeTime, err) + } + + return nil + }) +} diff --git a/app/internal/service/counter/store/counter.sql.go b/app/internal/service/counter/store/counter.sql.go new file mode 100644 index 0000000..da771d7 --- /dev/null +++ b/app/internal/service/counter/store/counter.sql.go @@ -0,0 +1,134 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 +// source: counter.sql + +package counterstore + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const deleteIncrementRequests = `-- name: DeleteIncrementRequests :exec +TRUNCATE TABLE increment_requests +` + +func (q *Queries) DeleteIncrementRequests(ctx context.Context, db DBTX) error { + _, err := db.Exec(ctx, deleteIncrementRequests) + return err +} + +const getCounter = `-- name: GetCounter :one +SELECT id, count, last_increment_by, last_increment_at, next_finalize_at +FROM counter +WHERE id = 1 +` + +func (q *Queries) GetCounter(ctx context.Context, db DBTX) (Counter, error) { + row := db.QueryRow(ctx, getCounter) + var i Counter + err := row.Scan( + &i.ID, + &i.Count, + &i.LastIncrementBy, + &i.LastIncrementAt, + &i.NextFinalizeAt, + ) + return i, err +} + +const incrementCounter = `-- name: IncrementCounter :exec +UPDATE counter +SET + count = count + 1, + last_increment_by = $1, + last_increment_at = $2, + next_finalize_at = NULL +WHERE id = 1 +` + +type IncrementCounterParams struct { + LastIncrementBy pgtype.Text + LastIncrementAt pgtype.Timestamptz +} + +func (q *Queries) IncrementCounter(ctx context.Context, db DBTX, arg IncrementCounterParams) error { + _, err := db.Exec(ctx, incrementCounter, arg.LastIncrementBy, arg.LastIncrementAt) + return err +} + +const insertIncrementRequest = `-- name: InsertIncrementRequest :one +WITH inserted AS ( + INSERT INTO increment_requests (requested_by, requested_at) + VALUES ($1, $2) + RETURNING requested_by, requested_at +) +SELECT COUNT(*) AS num_requests +FROM increment_requests +` + +type InsertIncrementRequestParams struct { + RequestedBy string + RequestedAt pgtype.Timestamptz +} + +func (q *Queries) InsertIncrementRequest(ctx context.Context, db DBTX, arg InsertIncrementRequestParams) (int64, error) { + row := db.QueryRow(ctx, insertIncrementRequest, arg.RequestedBy, arg.RequestedAt) + var num_requests int64 + err := row.Scan(&num_requests) + return num_requests, err +} + +const listIncrementRequests = `-- name: ListIncrementRequests :many +SELECT requested_by, requested_at +FROM increment_requests +` + +func (q *Queries) ListIncrementRequests(ctx context.Context, db DBTX) ([]IncrementRequest, error) { + rows, err := db.Query(ctx, listIncrementRequests) + if err != nil { + return nil, err + } + defer rows.Close() + var items []IncrementRequest + for rows.Next() { + var i IncrementRequest + if err := rows.Scan(&i.RequestedBy, &i.RequestedAt); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const resetCounter = `-- name: ResetCounter :exec +UPDATE counter +SET + count = 0, + last_increment_by = NULL, + last_increment_at = NULL, + next_finalize_at = NULL +WHERE id = 1 +` + +func (q *Queries) ResetCounter(ctx context.Context, db DBTX) error { + _, err := db.Exec(ctx, resetCounter) + return err +} + +const updateCounterFinalizeTime = `-- name: UpdateCounterFinalizeTime :exec +UPDATE counter +SET + next_finalize_at = $1 +WHERE id = 1 +` + +func (q *Queries) UpdateCounterFinalizeTime(ctx context.Context, db DBTX, nextFinalizeAt pgtype.Timestamptz) error { + _, err := db.Exec(ctx, updateCounterFinalizeTime, nextFinalizeAt) + return err +} diff --git a/app/internal/service/counter/store/db.go b/app/internal/service/counter/store/db.go new file mode 100644 index 0000000..26956fd --- /dev/null +++ b/app/internal/service/counter/store/db.go @@ -0,0 +1,25 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 + +package counterstore + +import ( + "context" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" +) + +type DBTX interface { + Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error) + Query(context.Context, string, ...interface{}) (pgx.Rows, error) + QueryRow(context.Context, string, ...interface{}) pgx.Row +} + +func New() *Queries { + return &Queries{} +} + +type Queries struct { +} diff --git a/app/internal/service/counter/store/models.go b/app/internal/service/counter/store/models.go new file mode 100644 index 0000000..63bff9b --- /dev/null +++ b/app/internal/service/counter/store/models.go @@ -0,0 +1,27 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 + +package counterstore + +import ( + "github.com/jackc/pgx/v5/pgtype" +) + +type Counter struct { + ID int32 + Count int32 + LastIncrementBy pgtype.Text + LastIncrementAt pgtype.Timestamptz + NextFinalizeAt pgtype.Timestamptz +} + +type IncrementRequest struct { + RequestedBy string + RequestedAt pgtype.Timestamptz +} + +type User struct { + ID string + Email string +} diff --git a/app/internal/service/counter/store/querier.go b/app/internal/service/counter/store/querier.go new file mode 100644 index 0000000..5ff968b --- /dev/null +++ b/app/internal/service/counter/store/querier.go @@ -0,0 +1,23 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 + +package counterstore + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +type Querier interface { + DeleteIncrementRequests(ctx context.Context, db DBTX) error + GetCounter(ctx context.Context, db DBTX) (Counter, error) + IncrementCounter(ctx context.Context, db DBTX, arg IncrementCounterParams) error + InsertIncrementRequest(ctx context.Context, db DBTX, arg InsertIncrementRequestParams) (int64, error) + ListIncrementRequests(ctx context.Context, db DBTX) ([]IncrementRequest, error) + ResetCounter(ctx context.Context, db DBTX) error + UpdateCounterFinalizeTime(ctx context.Context, db DBTX, nextFinalizeAt pgtype.Timestamptz) error +} + +var _ Querier = (*Queries)(nil) diff --git a/app/internal/service/counter/types.go b/app/internal/service/counter/types.go new file mode 100644 index 0000000..ca3dae2 --- /dev/null +++ b/app/internal/service/counter/types.go @@ -0,0 +1,24 @@ +package counter + +import "time" + +type Info struct { + Count int32 + LastIncrementBy string + LastIncrementAt time.Time + NextFinalizeAt time.Time +} + +func (i *Info) LastIncrementAtTimestamp() string { + if i.LastIncrementAt.IsZero() { + return "" + } + return i.LastIncrementAt.String() +} + +func (i *Info) NextFinalizeAtTimestamp() string { + if i.NextFinalizeAt.IsZero() { + return "" + } + return i.NextFinalizeAt.String() +} diff --git a/app/internal/service/user/errors.go b/app/internal/service/user/errors.go new file mode 100644 index 0000000..f1536c0 --- /dev/null +++ b/app/internal/service/user/errors.go @@ -0,0 +1,8 @@ +package user + +import "errors" + +var ( + ErrGetUser = errors.New("store get user") + ErrInsertUserIfNotExists = errors.New("store insert user if not exists") +) diff --git a/app/internal/service/user/service.go b/app/internal/service/user/service.go new file mode 100644 index 0000000..5e389ab --- /dev/null +++ b/app/internal/service/user/service.go @@ -0,0 +1,60 @@ +package user + +import ( + "context" + "fmt" + + "github.com/jace-ys/countup/internal/ctxlog" + "github.com/jace-ys/countup/internal/idgen" + "github.com/jace-ys/countup/internal/postgres" + userstore "github.com/jace-ys/countup/internal/service/user/store" +) + +type Service struct { + db *postgres.Pool + store userstore.Querier +} + +func New(db *postgres.Pool, store userstore.Querier) *Service { + return &Service{ + db: db, + store: store, + } +} + +func (s *Service) GetUser(ctx context.Context, userID string) (*User, error) { + ctxlog.Info(ctx, "getting user", ctxlog.KV("user.id", userID)) + + user, err := s.store.GetUser(ctx, s.db, userID) + if err != nil { + return nil, fmt.Errorf("%w: %w", ErrGetUser, err) + } + + return &User{ + ID: user.ID, + Email: user.Email, + }, nil +} + +func (s *Service) CreateUserIfNotExists(ctx context.Context, email string) (*User, error) { + ctxlog.Info(ctx, "creating user if not exists", ctxlog.KV("user.email", email)) + + if err := s.store.InsertUserIfNotExists(ctx, s.db, userstore.InsertUserIfNotExistsParams{ + ID: idgen.NewID("usr"), + Email: email, + }); err != nil { + return nil, fmt.Errorf("%w: %w", ErrInsertUserIfNotExists, err) + } + + ctxlog.Info(ctx, "getting user by email", ctxlog.KV("user.email", email)) + + user, err := s.store.GetUserByEmail(ctx, s.db, email) + if err != nil { + return nil, fmt.Errorf("%w: %w", ErrGetUser, err) + } + + return &User{ + ID: user.ID, + Email: user.Email, + }, nil +} diff --git a/app/internal/service/user/store/db.go b/app/internal/service/user/store/db.go new file mode 100644 index 0000000..0ec2d99 --- /dev/null +++ b/app/internal/service/user/store/db.go @@ -0,0 +1,25 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 + +package userstore + +import ( + "context" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" +) + +type DBTX interface { + Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error) + Query(context.Context, string, ...interface{}) (pgx.Rows, error) + QueryRow(context.Context, string, ...interface{}) pgx.Row +} + +func New() *Queries { + return &Queries{} +} + +type Queries struct { +} diff --git a/app/internal/service/user/store/models.go b/app/internal/service/user/store/models.go new file mode 100644 index 0000000..4bc85fe --- /dev/null +++ b/app/internal/service/user/store/models.go @@ -0,0 +1,27 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 + +package userstore + +import ( + "github.com/jackc/pgx/v5/pgtype" +) + +type Counter struct { + ID int32 + Count int32 + LastIncrementBy pgtype.Text + LastIncrementAt pgtype.Timestamptz + NextFinalizeAt pgtype.Timestamptz +} + +type IncrementRequest struct { + RequestedBy string + RequestedAt pgtype.Timestamptz +} + +type User struct { + ID string + Email string +} diff --git a/app/internal/service/user/store/querier.go b/app/internal/service/user/store/querier.go new file mode 100644 index 0000000..a14c6bb --- /dev/null +++ b/app/internal/service/user/store/querier.go @@ -0,0 +1,17 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 + +package userstore + +import ( + "context" +) + +type Querier interface { + GetUser(ctx context.Context, db DBTX, id string) (User, error) + GetUserByEmail(ctx context.Context, db DBTX, email string) (User, error) + InsertUserIfNotExists(ctx context.Context, db DBTX, arg InsertUserIfNotExistsParams) error +} + +var _ Querier = (*Queries)(nil) diff --git a/app/internal/service/user/store/users.sql.go b/app/internal/service/user/store/users.sql.go new file mode 100644 index 0000000..79d7941 --- /dev/null +++ b/app/internal/service/user/store/users.sql.go @@ -0,0 +1,52 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 +// source: users.sql + +package userstore + +import ( + "context" +) + +const getUser = `-- name: GetUser :one +SELECT id, email +FROM users +WHERE id = $1 +` + +func (q *Queries) GetUser(ctx context.Context, db DBTX, id string) (User, error) { + row := db.QueryRow(ctx, getUser, id) + var i User + err := row.Scan(&i.ID, &i.Email) + return i, err +} + +const getUserByEmail = `-- name: GetUserByEmail :one +SELECT id, email +FROM users +WHERE email = $1 +` + +func (q *Queries) GetUserByEmail(ctx context.Context, db DBTX, email string) (User, error) { + row := db.QueryRow(ctx, getUserByEmail, email) + var i User + err := row.Scan(&i.ID, &i.Email) + return i, err +} + +const insertUserIfNotExists = `-- name: InsertUserIfNotExists :exec +INSERT INTO users (id, email) +VALUES ($1, $2) +ON CONFLICT(email) DO NOTHING +` + +type InsertUserIfNotExistsParams struct { + ID string + Email string +} + +func (q *Queries) InsertUserIfNotExists(ctx context.Context, db DBTX, arg InsertUserIfNotExistsParams) error { + _, err := db.Exec(ctx, insertUserIfNotExists, arg.ID, arg.Email) + return err +} diff --git a/app/internal/service/user/types.go b/app/internal/service/user/types.go new file mode 100644 index 0000000..b8b6579 --- /dev/null +++ b/app/internal/service/user/types.go @@ -0,0 +1,37 @@ +package user + +import ( + "context" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + + "github.com/jace-ys/countup/internal/ctxlog" +) + +type User struct { + ID string + Email string +} + +type ctxKeyUserInfo struct{} + +func ContextWithUser(ctx context.Context, user *User) context.Context { + ctx = context.WithValue(ctx, ctxKeyUserInfo{}, user) + + span := trace.SpanFromContext(ctx) + span.SetAttributes(attribute.String("user.id", user.ID)) + span.SetAttributes(attribute.String("user.email", user.Email)) + + ctx = ctxlog.With(ctx, ctxlog.KV("user.id", user.ID), ctxlog.KV("user.email", user.Email)) + + return ctx +} + +func UserFromContext(ctx context.Context) *User { + user, ok := ctx.Value(ctxKeyUserInfo{}).(*User) + if !ok { + return nil + } + return user +} diff --git a/app/internal/transport/client.go b/app/internal/transport/client.go new file mode 100644 index 0000000..3769e26 --- /dev/null +++ b/app/internal/transport/client.go @@ -0,0 +1,83 @@ +package transport + +import ( + "fmt" + "net/http" + + goahttp "goa.design/goa/v3/http" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +type Client[C any] struct { + client *C + close func() error +} + +func (c *Client[C]) Client() *C { + return c.client +} + +func (c *Client[C]) Close() error { + if c.close == nil { + return nil + } + return c.close() +} + +type GoaGRPCClientAdapter[C any] struct { + newFunc GoaGRPCClientNewFunc[C] +} + +type GoaGRPCClientNewFunc[C any] func(cc *grpc.ClientConn, opts ...grpc.CallOption) *C + +func GoaGRPCClient[C any](newFunc GoaGRPCClientNewFunc[C]) *GoaGRPCClientAdapter[C] { + return &GoaGRPCClientAdapter[C]{ + newFunc: newFunc, + } +} + +func (a *GoaGRPCClientAdapter[C]) Adapt(addr string) (*Client[C], error) { + opts := []grpc.DialOption{ + grpc.WithTransportCredentials(insecure.NewCredentials()), + } + + conn, err := grpc.NewClient(addr, opts...) + if err != nil { + return nil, fmt.Errorf("create gRPC client: %w", err) + } + + return &Client[C]{ + close: conn.Close, + client: a.newFunc(conn), + }, nil +} + +type GoaHTTPClientAdapter[C any] struct { + newFunc GoaHTTPClientNewFunc[C] +} + +type GoaHTTPClientNewFunc[C any] func( + scheme string, + host string, + doer goahttp.Doer, + enc func(*http.Request) goahttp.Encoder, + dec func(*http.Response) goahttp.Decoder, + restoreBody bool, +) *C + +func GoaHTTPClient[C any](newFunc GoaHTTPClientNewFunc[C]) *GoaHTTPClientAdapter[C] { + return &GoaHTTPClientAdapter[C]{ + newFunc: newFunc, + } +} + +func (a *GoaHTTPClientAdapter[C]) Adapt(scheme, addr string) (*Client[C], error) { + doer := http.DefaultClient + enc := goahttp.RequestEncoder + dec := goahttp.ResponseDecoder + + return &Client[C]{ + client: a.newFunc(scheme, addr, doer, enc, dec, false), + }, nil +} diff --git a/app/internal/transport/goa.go b/app/internal/transport/goa.go new file mode 100644 index 0000000..f0d54ce --- /dev/null +++ b/app/internal/transport/goa.go @@ -0,0 +1,99 @@ +package transport + +import ( + "context" + "fmt" + "io/fs" + "net/http" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" + "goa.design/goa/v3/grpc" + goahttp "goa.design/goa/v3/http" + "goa.design/goa/v3/http/middleware" + goa "goa.design/goa/v3/pkg" + + "github.com/jace-ys/countup/internal/ctxlog" + "github.com/jace-ys/countup/internal/endpoint" +) + +type GoaGRPCAdapter[E endpoint.GoaEndpoints, S any] struct { + newFunc GoaGRPCNewFunc[E, S] +} + +type GoaGRPCNewFunc[E endpoint.GoaEndpoints, S any] func(e E, uh grpc.UnaryHandler) *S + +func GoaGRPC[E endpoint.GoaEndpoints, S any](newFunc GoaGRPCNewFunc[E, S]) *GoaGRPCAdapter[E, S] { + return &GoaGRPCAdapter[E, S]{ + newFunc: newFunc, + } +} + +func (a *GoaGRPCAdapter[E, S]) Adapt(ctx context.Context, ep E) *S { + srv := a.newFunc(ep, nil) + return srv +} + +type goaHTTPServer interface { + MethodNames() []string + Mount(mux goahttp.Muxer) + Service() string + Use(m func(http.Handler) http.Handler) +} + +type GoaHTTPAdapter[E endpoint.GoaEndpoints, S goaHTTPServer] struct { + newFunc GoaHTTPNewFunc[E, S] + mountFunc GoaHTTPMountFunc[S] +} + +type GoaHTTPNewFunc[E endpoint.GoaEndpoints, S goaHTTPServer] func( + e E, + mux goahttp.Muxer, + decoder func(*http.Request) goahttp.Decoder, + encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, + errhandler func(context.Context, http.ResponseWriter, error), + formatter func(ctx context.Context, err error) goahttp.Statuser, + files http.FileSystem, +) S + +type GoaHTTPMountFunc[S goaHTTPServer] func( + mux goahttp.Muxer, + srv S, +) + +func GoaHTTP[E endpoint.GoaEndpoints, S goaHTTPServer](newFunc GoaHTTPNewFunc[E, S], mountFunc GoaHTTPMountFunc[S]) *GoaHTTPAdapter[E, S] { + return &GoaHTTPAdapter[E, S]{ + newFunc: newFunc, + mountFunc: mountFunc, + } +} + +func (a *GoaHTTPAdapter[E, S]) Adapt(ctx context.Context, ep E, files fs.FS) goahttp.ResolverMuxer { + dec := goahttp.RequestDecoder + enc := goahttp.ResponseEncoder + formatter := goahttp.NewErrorResponse + + eh := func(ctx context.Context, w http.ResponseWriter, err error) { + ctxlog.Error(ctx, "failed to encode response", err, + ctxlog.KV("http.method", ctx.Value(middleware.RequestMethodKey)), + ctxlog.KV("http.path", ctx.Value(middleware.RequestPathKey)), + ) + + gerr := goa.Fault("failed to encode response") + + span := trace.SpanFromContext(ctx) + span.SetStatus(codes.Error, gerr.GoaErrorName()) + span.SetAttributes(attribute.String("error", fmt.Sprintf("failed to encode response: %v", err))) + + if err := goahttp.ErrorEncoder(enc, formatter)(ctx, w, gerr); err != nil { + panic(err) + } + } + + mux := goahttp.NewMuxer() + srv := a.newFunc(ep, mux, dec, enc, eh, formatter, http.FS(files)) + a.mountFunc(mux, srv) + + return mux +} diff --git a/app/internal/transport/middleware/recovery/recovery.go b/app/internal/transport/middleware/recovery/recovery.go new file mode 100644 index 0000000..593f66c --- /dev/null +++ b/app/internal/transport/middleware/recovery/recovery.go @@ -0,0 +1,52 @@ +package recovery + +import ( + "context" + "errors" + "net/http" + + "goa.design/clue/log" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/jace-ys/countup/internal/instrument" +) + +func UnaryServerInterceptor(logCtx context.Context) grpc.UnaryServerInterceptor { + return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, next grpc.UnaryHandler) (_ any, err error) { + defer func() { + if rvr := recover(); rvr != nil { + ctx := log.WithContext(ctx, logCtx) + instrument.EmitRecoveredPanicTelemetry(ctx, rvr, info.FullMethod[1:]) + err = status.Error(codes.Internal, "internal server error") + } + }() + + return next(ctx, req) + } +} + +func HTTP(logCtx context.Context) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer func() { + if rvr := recover(); rvr != nil { + if err, ok := rvr.(error); ok && errors.Is(err, http.ErrAbortHandler) { + panic(rvr) + } + + ctx := log.WithContext(r.Context(), logCtx) + source := r.Method + " " + r.URL.Path + instrument.EmitRecoveredPanicTelemetry(ctx, rvr, source) + + if r.Header.Get("Connection") != "Upgrade" { + w.WriteHeader(http.StatusInternalServerError) + } + } + }() + + next.ServeHTTP(w, r) + }) + } +} diff --git a/app/internal/transport/middleware/reqid/reqid.go b/app/internal/transport/middleware/reqid/reqid.go new file mode 100644 index 0000000..fb76cda --- /dev/null +++ b/app/internal/transport/middleware/reqid/reqid.go @@ -0,0 +1,49 @@ +package reqid + +import ( + "context" + "net/http" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + "goa.design/clue/log" + "google.golang.org/grpc" + + "github.com/jace-ys/countup/internal/idgen" +) + +func UnaryServerInterceptor() grpc.UnaryServerInterceptor { + return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, next grpc.UnaryHandler) (_ any, err error) { + ctx = newRequestID(ctx) + return next(ctx, req) + } +} + +func HTTP() func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := newRequestID(r.Context()) + next.ServeHTTP(w, r.WithContext(ctx)) + }) + } +} + +type ctxKeyRequestID struct{} + +func newRequestID(ctx context.Context) context.Context { + requestID := idgen.NewID("req") + ctx = context.WithValue(ctx, ctxKeyRequestID{}, requestID) + + span := trace.SpanFromContext(ctx) + span.SetAttributes(attribute.String(log.RequestIDKey, requestID)) + + return ctx +} + +func RequestIDFromContext(ctx context.Context) string { + requestID, ok := ctx.Value(ctxKeyRequestID{}).(string) + if !ok { + return "" + } + return requestID +} diff --git a/app/internal/transport/middleware/telemetry/http.go b/app/internal/transport/middleware/telemetry/http.go new file mode 100644 index 0000000..a3104a4 --- /dev/null +++ b/app/internal/transport/middleware/telemetry/http.go @@ -0,0 +1,24 @@ +package telemetry + +import ( + "net/http" + + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +func HTTP(attrs ...attribute.KeyValue) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + operation := r.Method + " " + r.URL.Path + + opts := []otelhttp.Option{ + otelhttp.WithSpanOptions(trace.WithAttributes(attrs...)), + } + + handler := otelhttp.NewHandler(otelhttp.WithRouteTag(r.URL.Path, next), operation, opts...) + handler.ServeHTTP(w, r) + }) + } +} diff --git a/app/internal/versioninfo/version.go b/app/internal/versioninfo/version.go new file mode 100644 index 0000000..2ed0475 --- /dev/null +++ b/app/internal/versioninfo/version.go @@ -0,0 +1,6 @@ +package versioninfo + +var ( + Version = "dev" + CommitSHA = "none" +) diff --git a/app/internal/worker/instrumented.go b/app/internal/worker/instrumented.go new file mode 100644 index 0000000..f0f95f0 --- /dev/null +++ b/app/internal/worker/instrumented.go @@ -0,0 +1,160 @@ +package worker + +import ( + "context" + "errors" + "fmt" + + "github.com/riverqueue/river" + "github.com/riverqueue/river/rivertype" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/trace" + "goa.design/clue/log" + + "github.com/jace-ys/countup/internal/ctxlog" + "github.com/jace-ys/countup/internal/instrument" +) + +type instrumentedWorkerMiddleware struct { + river.WorkerMiddlewareDefaults +} + +var _ rivertype.WorkerMiddleware = (*instrumentedWorkerMiddleware)(nil) + +func (w *instrumentedWorkerMiddleware) Work(ctx context.Context, job *rivertype.JobRow, doInner func(context.Context) error) error { + kvs := []log.Fielder{ + ctxlog.KV("job.worker", "river"), + ctxlog.KV("job.kind", job.Kind), + ctxlog.KV("job.id", job.ID), + ctxlog.KV("job.attempt", job.Attempt), + } + + attrs := []attribute.KeyValue{ + attribute.String("job.worker", "river"), + attribute.String("job.kind", job.Kind), + attribute.Int64("job.id", job.ID), + attribute.Int("job.attempt", job.Attempt), + attribute.String("job.queue", job.Queue), + attribute.Int("job.priority", job.Priority), + attribute.String("job.scheduled_at", job.ScheduledAt.String()), + attribute.String("job.attempted_at", job.AttemptedAt.String()), + } + + md, err := parseMetadata(job.Metadata) + if err != nil { + ctxlog.Error(ctx, "failed to extract metadata from job", err) + } + + for k, v := range md { + kvs = append(kvs, ctxlog.KV(k, v)) + attrs = append(attrs, attribute.String(k, v)) + } + + source := fmt.Sprintf("river.worker/%s", job.Kind) + ctx, span := instrument.OTel.Tracer().Start(ctx, source) + span.SetAttributes(attrs...) + defer span.End() + + ctx = ctxlog.With(ctx, kvs...) + ctxlog.Print(ctx, "job started", + ctxlog.KV("job.queue", job.Queue), + ctxlog.KV("job.priority", job.Priority), + ctxlog.KV("job.scheduled_at", job.ScheduledAt), + ctxlog.KV("job.attempted_at", job.AttemptedAt), + ) + + var jobErr error + func() { + defer func() { + if rvr := recover(); rvr != nil { + instrument.EmitRecoveredPanicTelemetry(ctx, rvr, source) + jobErr = fmt.Errorf("%v", rvr) + } + }() + jobErr = doInner(ctx) + }() + + if jobErr != nil { + var jobErrReason string + switch { + case errors.Is(err, &river.JobSnoozeError{}): + ctxlog.Print(ctx, "job snoozed") + span.SetAttributes(attribute.Bool("job.snoozed", true)) + return jobErr + + case errors.Is(err, &river.JobCancelError{}): + jobErrReason = "job cancelled" + span.SetAttributes(attribute.Bool("job.cancelled", true)) + + case job.Attempt == job.MaxAttempts: + jobErrReason = "job failed, discarded due to max attempts exceeded" + span.SetAttributes(attribute.Bool("job.discarded", true)) + + default: + jobErrReason = "job failed" + span.SetAttributes(attribute.Bool("job.failed", true)) + } + + ctxlog.Error(ctx, jobErrReason, jobErr) + span.SetStatus(codes.Error, jobErrReason) + span.SetAttributes(attribute.String("error", jobErr.Error())) + + return jobErr + } + + ctxlog.Print(ctx, "job completed") + return nil +} + +type instrumentedJobInsertMiddleware struct { + river.JobInsertMiddlewareDefaults + metrics *metrics +} + +var _ rivertype.JobInsertMiddleware = (*instrumentedJobInsertMiddleware)(nil) + +func (m *instrumentedJobInsertMiddleware) InsertMany(ctx context.Context, manyParams []*rivertype.JobInsertParams, doInner func(ctx context.Context) ([]*rivertype.JobInsertResult, error)) ([]*rivertype.JobInsertResult, error) { + results, err := doInner(ctx) + for _, enqueued := range results { + m.emitEnqueuedTelemetry(ctx, enqueued) + } + return results, err +} + +func (m *instrumentedJobInsertMiddleware) emitEnqueuedTelemetry(ctx context.Context, enqueued *rivertype.JobInsertResult) { + kvs := []log.Fielder{ + ctxlog.KV("job.worker", "river"), + ctxlog.KV("job.kind", enqueued.Job.Kind), + ctxlog.KV("job.id", enqueued.Job.ID), + ctxlog.KV("job.queue", enqueued.Job.Queue), + ctxlog.KV("job.priority", enqueued.Job.Priority), + ctxlog.KV("job.scheduled_at", enqueued.Job.ScheduledAt), + ctxlog.KV("job.max_attempts", enqueued.Job.MaxAttempts), + } + + attrs := []attribute.KeyValue{ + attribute.String("job.worker", "river"), + attribute.String("job.kind", enqueued.Job.Kind), + attribute.String("job.queue", enqueued.Job.Queue), + attribute.Int("job.priority", enqueued.Job.Priority), + } + + ctxlog.Print(ctx, "job enqueued", kvs...) + + span := trace.SpanFromContext(ctx) + span.AddEvent("job.enqueued", + trace.WithTimestamp(enqueued.Job.CreatedAt), + trace.WithAttributes(attrs...), + trace.WithAttributes( + attribute.Int64("job.id", enqueued.Job.ID), + attribute.String("job.scheduled_at", enqueued.Job.ScheduledAt.String()), + attribute.Int("job.max_attempts", enqueued.Job.MaxAttempts), + ), + ) + + metricAttrs := attribute.NewSet(attrs...) + m.metrics.jobsEnqueuedTotal.Add(ctx, 1, metric.WithAttributeSet(metricAttrs)) + m.metrics.jobsAvailableCount.Add(ctx, 1, metric.WithAttributeSet(metricAttrs)) +} diff --git a/app/internal/worker/metadata.go b/app/internal/worker/metadata.go new file mode 100644 index 0000000..8928448 --- /dev/null +++ b/app/internal/worker/metadata.go @@ -0,0 +1,31 @@ +package worker + +import ( + "context" + "encoding/json" + + "go.opentelemetry.io/otel/propagation" + "goa.design/clue/log" + + "github.com/jace-ys/countup/internal/instrument" + "github.com/jace-ys/countup/internal/transport/middleware/reqid" +) + +var _ propagation.TextMapCarrier = (*JobMetadata)(nil) + +type JobMetadata = propagation.MapCarrier + +func withContextMetadata(ctx context.Context) EnqueueOption { + md := make(JobMetadata) + + md[log.RequestIDKey] = reqid.RequestIDFromContext(ctx) + instrument.OTel.Propagators().Inject(ctx, md) + + return WithMetadata(md) +} + +func parseMetadata(metadata []byte) (JobMetadata, error) { + md := make(JobMetadata) + err := json.Unmarshal(metadata, &md) + return md, err //nolint:wrapcheck +} diff --git a/app/internal/worker/metrics.go b/app/internal/worker/metrics.go new file mode 100644 index 0000000..0645854 --- /dev/null +++ b/app/internal/worker/metrics.go @@ -0,0 +1,124 @@ +package worker + +import ( + "context" + "fmt" + + "github.com/riverqueue/river" + "github.com/riverqueue/river/rivertype" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric" + + "github.com/jace-ys/countup/internal/instrument" +) + +type metrics struct { + jobsEnqueuedTotal metric.Int64Counter + jobsCompletedTotal metric.Int64Counter + jobsFailedTotal metric.Int64Counter + jobsDiscardedTotal metric.Int64Counter + jobsCancelledTotal metric.Int64Counter + jobsAvailableCount metric.Int64UpDownCounter + jobsRunDurationSeconds metric.Float64Histogram + jobsQueueWaitMilliseconds metric.Int64Histogram +} + +func newMetrics() (*metrics, error) { + meter := instrument.OTel.Meter() + + metrics := &metrics{} + var err error + + metrics.jobsEnqueuedTotal, err = meter.Int64Counter("worker.jobs.enqueued.total") + if err != nil { + return nil, fmt.Errorf("worker.jobs.enqueued.total: %w", err) + } + + metrics.jobsCompletedTotal, err = meter.Int64Counter("worker.jobs.completed.total") + if err != nil { + return nil, fmt.Errorf("worker.jobs.completed.total: %w", err) + } + + metrics.jobsFailedTotal, err = meter.Int64Counter("worker.jobs.failed.total") + if err != nil { + return nil, fmt.Errorf("worker.jobs.failed.total: %w", err) + } + + metrics.jobsDiscardedTotal, err = meter.Int64Counter("worker.jobs.discarded.total") + if err != nil { + return nil, fmt.Errorf("worker.jobs.discarded.total: %w", err) + } + + metrics.jobsCancelledTotal, err = meter.Int64Counter("worker.jobs.cancelled.total") + if err != nil { + return nil, fmt.Errorf("worker.jobs.cancelled.total: %w", err) + } + + metrics.jobsAvailableCount, err = meter.Int64UpDownCounter("worker.jobs.available.count") + if err != nil { + return nil, fmt.Errorf("worker.jobs.available.count: %w", err) + } + + metrics.jobsRunDurationSeconds, err = meter.Float64Histogram("worker.jobs.run_duration.seconds") + if err != nil { + return nil, fmt.Errorf("worker.jobs.run.duration.seconds: %w", err) + } + + metrics.jobsQueueWaitMilliseconds, err = meter.Int64Histogram("worker.jobs.queue_wait.milliseconds") + if err != nil { + return nil, fmt.Errorf("worker.jobs.queue.wait.milliseconds: %w", err) + } + + return metrics, nil +} + +func (p *Pool) runMetricsExporter(ctx context.Context) { + subscriptions := []river.EventKind{ + river.EventKindJobCompleted, + river.EventKindJobCancelled, + river.EventKindJobFailed, + } + + events, cancel := p.pool.Subscribe(subscriptions...) + defer cancel() + + for { + select { + case <-ctx.Done(): + return + case event, ok := <-events: + if !ok { + return + } + + attrs := attribute.NewSet( + attribute.String("job.worker", "river"), + attribute.String("job.kind", event.Job.Kind), + attribute.String("job.queue", event.Job.Queue), + attribute.Int("job.priority", event.Job.Priority), + ) + + p.metrics.jobsQueueWaitMilliseconds.Record(ctx, event.JobStats.QueueWaitDuration.Milliseconds(), metric.WithAttributeSet(attrs)) + + switch event.Kind { //nolint:exhaustive + case river.EventKindJobCompleted: + p.metrics.jobsCompletedTotal.Add(ctx, 1, metric.WithAttributeSet(attrs)) + p.metrics.jobsAvailableCount.Add(ctx, -1, metric.WithAttributeSet(attrs)) + p.metrics.jobsRunDurationSeconds.Record(ctx, event.JobStats.RunDuration.Seconds(), metric.WithAttributeSet(attrs)) + + case river.EventKindJobCancelled: + p.metrics.jobsCancelledTotal.Add(ctx, 1, metric.WithAttributeSet(attrs)) + p.metrics.jobsAvailableCount.Add(ctx, -1, metric.WithAttributeSet(attrs)) + + case river.EventKindJobFailed: + p.metrics.jobsFailedTotal.Add(ctx, 1, metric.WithAttributeSet(attrs)) + + switch event.Job.State { //nolint:exhaustive + case rivertype.JobStateDiscarded: + p.metrics.jobsDiscardedTotal.Add(ctx, 1, metric.WithAttributeSet(attrs)) + p.metrics.jobsAvailableCount.Add(ctx, -1, metric.WithAttributeSet(attrs)) + } + } + } + } +} diff --git a/app/internal/worker/options.go b/app/internal/worker/options.go new file mode 100644 index 0000000..29af7c0 --- /dev/null +++ b/app/internal/worker/options.go @@ -0,0 +1,46 @@ +package worker + +import ( + "encoding/json" + "maps" + "time" + + "github.com/riverqueue/river" +) + +type EnqueueOption func(*river.InsertOpts) + +func WithMetadata(md JobMetadata) EnqueueOption { + return func(o *river.InsertOpts) { + existingMD, err := parseMetadata(o.Metadata) + if err != nil { + existingMD = make(JobMetadata) + } + + maps.Copy(existingMD, md) + metadata, err := json.Marshal(existingMD) + if err != nil { + return + } + + o.Metadata = metadata + } +} + +func WithSchedule(schedule time.Time) EnqueueOption { + return func(opts *river.InsertOpts) { + opts.ScheduledAt = schedule + } +} + +func WithMaxAttempts(attempts int) EnqueueOption { + return func(opts *river.InsertOpts) { + opts.MaxAttempts = attempts + } +} + +func WithPriority(priority int) EnqueueOption { + return func(opts *river.InsertOpts) { + opts.Priority = priority + } +} diff --git a/app/internal/worker/pool.go b/app/internal/worker/pool.go new file mode 100644 index 0000000..8bcee9a --- /dev/null +++ b/app/internal/worker/pool.go @@ -0,0 +1,131 @@ +package worker + +import ( + "context" + "errors" + "fmt" + "log/slog" + + "github.com/alexliesenfeld/health" + "github.com/jackc/pgx/v5" + "github.com/riverqueue/river" + "github.com/riverqueue/river/riverdriver/riverpgxv5" + "github.com/riverqueue/river/rivertype" + + "github.com/jace-ys/countup/internal/app" + "github.com/jace-ys/countup/internal/ctxlog" + "github.com/jace-ys/countup/internal/healthz" + "github.com/jace-ys/countup/internal/postgres" +) + +func Register[T river.JobArgs](pool *Pool, worker river.Worker[T]) { + river.AddWorker(pool.workers, worker) +} + +type Pool struct { + name string + pool *river.Client[pgx.Tx] + workers *river.Workers + metrics *metrics +} + +func NewPool(ctx context.Context, name string, db *postgres.Pool, concurrency int) (*Pool, error) { + metrics, err := newMetrics() + if err != nil { + return nil, fmt.Errorf("init metrics: %w", err) + } + + workers := river.NewWorkers() + + client, err := river.NewClient(riverpgxv5.New(db.Pool), &river.Config{ + Workers: workers, + Queues: map[string]river.QueueConfig{ + river.QueueDefault: {MaxWorkers: concurrency}, + }, + JobInsertMiddleware: []rivertype.JobInsertMiddleware{ + &instrumentedJobInsertMiddleware{metrics: metrics}, + }, + WorkerMiddleware: []rivertype.WorkerMiddleware{ + &instrumentedWorkerMiddleware{}, + }, + Logger: slog.New(ctxlog.AsNopHandler()), + }) + if err != nil { + return nil, fmt.Errorf("init river client: %w", err) + } + + return &Pool{ + name: name, + pool: client, + workers: workers, + metrics: metrics, + }, nil +} + +func (p *Pool) Enqueue(ctx context.Context, job river.JobArgs, opts ...EnqueueOption) error { + opts = append(opts, withContextMetadata(ctx)) + + insertOpts := &river.InsertOpts{} + for _, opt := range opts { + opt(insertOpts) + } + + _, err := p.pool.Insert(ctx, job, insertOpts) + return err //nolint:wrapcheck +} + +func (p *Pool) EnqueueTx(ctx context.Context, tx pgx.Tx, job river.JobArgs, opts ...EnqueueOption) error { + opts = append(opts, withContextMetadata(ctx)) + + insertOpts := &river.InsertOpts{} + for _, opt := range opts { + opt(insertOpts) + } + + _, err := p.pool.InsertTx(ctx, tx, job, insertOpts) + return err //nolint:wrapcheck +} + +var _ app.Server = (*Pool)(nil) + +func (p *Pool) Name() string { + return p.name +} + +func (p *Pool) Kind() string { + return "worker" +} + +func (p *Pool) Addr() string { + return "" +} + +func (p *Pool) Serve(ctx context.Context) error { + go p.runMetricsExporter(ctx) + if err := p.pool.Start(ctx); err != nil { + return fmt.Errorf("starting worker pool: %w", err) + } + return nil +} + +func (p *Pool) Shutdown(ctx context.Context) error { + return p.pool.StopAndCancel(ctx) //nolint:wrapcheck +} + +var _ healthz.Target = (*Pool)(nil) + +func (p *Pool) HealthChecks() []health.Check { + return []health.Check{ + { + Name: fmt.Sprintf("%s:%s", p.Kind(), p.Name()), + Check: func(ctx context.Context) error { + select { + case <-p.pool.Stopped(): + return errors.New("river client reported as not running") + default: + return nil + } + }, + }, + } +} diff --git a/app/schema/counter.sql b/app/schema/counter.sql new file mode 100644 index 0000000..a09700f --- /dev/null +++ b/app/schema/counter.sql @@ -0,0 +1,44 @@ +-- name: GetCounter :one +SELECT * +FROM counter +WHERE id = 1; + +-- name: IncrementCounter :exec +UPDATE counter +SET + count = count + 1, + last_increment_by = $1, + last_increment_at = $2, + next_finalize_at = NULL +WHERE id = 1; + +-- name: ResetCounter :exec +UPDATE counter +SET + count = 0, + last_increment_by = NULL, + last_increment_at = NULL, + next_finalize_at = NULL +WHERE id = 1; + +-- name: UpdateCounterFinalizeTime :exec +UPDATE counter +SET + next_finalize_at = $1 +WHERE id = 1; + +-- name: ListIncrementRequests :many +SELECT * +FROM increment_requests; + +-- name: InsertIncrementRequest :one +WITH inserted AS ( + INSERT INTO increment_requests (requested_by, requested_at) + VALUES ($1, $2) + RETURNING * +) +SELECT COUNT(*) AS num_requests +FROM increment_requests; + +-- name: DeleteIncrementRequests :exec +TRUNCATE TABLE increment_requests; \ No newline at end of file diff --git a/app/schema/migrations/20241003194615_rivermigrate002.go b/app/schema/migrations/20241003194615_rivermigrate002.go new file mode 100644 index 0000000..889f6f9 --- /dev/null +++ b/app/schema/migrations/20241003194615_rivermigrate002.go @@ -0,0 +1,47 @@ +package migrations + +import ( + "context" + "database/sql" + "fmt" + + "github.com/pressly/goose/v3" + "github.com/riverqueue/river/riverdriver/riverdatabasesql" + "github.com/riverqueue/river/rivermigrate" +) + +func init() { + goose.AddMigrationNoTxContext(upRiverMigrate002, downRiverMigrate002) +} + +func upRiverMigrate002(ctx context.Context, db *sql.DB) error { + migrator, err := rivermigrate.New(riverdatabasesql.New(db), &rivermigrate.Config{}) + if err != nil { + return fmt.Errorf("init river migrator: %w", err) + } + + _, err = migrator.Migrate(ctx, rivermigrate.DirectionUp, &rivermigrate.MigrateOpts{ + TargetVersion: 2, + }) + if err != nil { + return fmt.Errorf("apply river migration: %w", err) + } + + return nil +} + +func downRiverMigrate002(ctx context.Context, db *sql.DB) error { + migrator, err := rivermigrate.New(riverdatabasesql.New(db), &rivermigrate.Config{}) + if err != nil { + return fmt.Errorf("init river migrator: %w", err) + } + + _, err = migrator.Migrate(ctx, rivermigrate.DirectionDown, &rivermigrate.MigrateOpts{ + TargetVersion: -1, + }) + if err != nil { + return fmt.Errorf("apply river migration: %w", err) + } + + return nil +} diff --git a/app/schema/migrations/20241003194729_rivermigrate003.go b/app/schema/migrations/20241003194729_rivermigrate003.go new file mode 100644 index 0000000..0100cb1 --- /dev/null +++ b/app/schema/migrations/20241003194729_rivermigrate003.go @@ -0,0 +1,47 @@ +package migrations + +import ( + "context" + "database/sql" + "fmt" + + "github.com/pressly/goose/v3" + "github.com/riverqueue/river/riverdriver/riverdatabasesql" + "github.com/riverqueue/river/rivermigrate" +) + +func init() { + goose.AddMigrationNoTxContext(upRiverMigrate003, downRiverMigrate003) +} + +func upRiverMigrate003(ctx context.Context, db *sql.DB) error { + migrator, err := rivermigrate.New(riverdatabasesql.New(db), &rivermigrate.Config{}) + if err != nil { + return fmt.Errorf("init river migrator: %w", err) + } + + _, err = migrator.Migrate(ctx, rivermigrate.DirectionUp, &rivermigrate.MigrateOpts{ + TargetVersion: 3, + }) + if err != nil { + return fmt.Errorf("apply river migration: %w", err) + } + + return nil +} + +func downRiverMigrate003(ctx context.Context, db *sql.DB) error { + migrator, err := rivermigrate.New(riverdatabasesql.New(db), &rivermigrate.Config{}) + if err != nil { + return fmt.Errorf("init river migrator: %w", err) + } + + _, err = migrator.Migrate(ctx, rivermigrate.DirectionDown, &rivermigrate.MigrateOpts{ + TargetVersion: 2, + }) + if err != nil { + return fmt.Errorf("apply river migration: %w", err) + } + + return nil +} diff --git a/app/schema/migrations/20241003195032_rivermigrate004.go b/app/schema/migrations/20241003195032_rivermigrate004.go new file mode 100644 index 0000000..5b2f597 --- /dev/null +++ b/app/schema/migrations/20241003195032_rivermigrate004.go @@ -0,0 +1,47 @@ +package migrations + +import ( + "context" + "database/sql" + "fmt" + + "github.com/pressly/goose/v3" + "github.com/riverqueue/river/riverdriver/riverdatabasesql" + "github.com/riverqueue/river/rivermigrate" +) + +func init() { + goose.AddMigrationNoTxContext(upRiverMigrate004, downRiverMigrate004) +} + +func upRiverMigrate004(ctx context.Context, db *sql.DB) error { + migrator, err := rivermigrate.New(riverdatabasesql.New(db), &rivermigrate.Config{}) + if err != nil { + return fmt.Errorf("init river migrator: %w", err) + } + + _, err = migrator.Migrate(ctx, rivermigrate.DirectionUp, &rivermigrate.MigrateOpts{ + TargetVersion: 4, + }) + if err != nil { + return fmt.Errorf("apply river migration: %w", err) + } + + return nil +} + +func downRiverMigrate004(ctx context.Context, db *sql.DB) error { + migrator, err := rivermigrate.New(riverdatabasesql.New(db), &rivermigrate.Config{}) + if err != nil { + return fmt.Errorf("init river migrator: %w", err) + } + + _, err = migrator.Migrate(ctx, rivermigrate.DirectionDown, &rivermigrate.MigrateOpts{ + TargetVersion: 3, + }) + if err != nil { + return fmt.Errorf("apply river migration: %w", err) + } + + return nil +} diff --git a/app/schema/migrations/20241003195147_rivermigrate005.go b/app/schema/migrations/20241003195147_rivermigrate005.go new file mode 100644 index 0000000..ff333d9 --- /dev/null +++ b/app/schema/migrations/20241003195147_rivermigrate005.go @@ -0,0 +1,47 @@ +package migrations + +import ( + "context" + "database/sql" + "fmt" + + "github.com/pressly/goose/v3" + "github.com/riverqueue/river/riverdriver/riverdatabasesql" + "github.com/riverqueue/river/rivermigrate" +) + +func init() { + goose.AddMigrationNoTxContext(upRiverMigrate005, downRiverMigrate005) +} + +func upRiverMigrate005(ctx context.Context, db *sql.DB) error { + migrator, err := rivermigrate.New(riverdatabasesql.New(db), &rivermigrate.Config{}) + if err != nil { + return fmt.Errorf("init river migrator: %w", err) + } + + _, err = migrator.Migrate(ctx, rivermigrate.DirectionUp, &rivermigrate.MigrateOpts{ + TargetVersion: 5, + }) + if err != nil { + return fmt.Errorf("apply river migration: %w", err) + } + + return nil +} + +func downRiverMigrate005(ctx context.Context, db *sql.DB) error { + migrator, err := rivermigrate.New(riverdatabasesql.New(db), &rivermigrate.Config{}) + if err != nil { + return fmt.Errorf("init river migrator: %w", err) + } + + _, err = migrator.Migrate(ctx, rivermigrate.DirectionDown, &rivermigrate.MigrateOpts{ + TargetVersion: 4, + }) + if err != nil { + return fmt.Errorf("apply river migration: %w", err) + } + + return nil +} diff --git a/app/schema/migrations/20241003195218_rivermigrate006.go b/app/schema/migrations/20241003195218_rivermigrate006.go new file mode 100644 index 0000000..7056144 --- /dev/null +++ b/app/schema/migrations/20241003195218_rivermigrate006.go @@ -0,0 +1,47 @@ +package migrations + +import ( + "context" + "database/sql" + "fmt" + + "github.com/pressly/goose/v3" + "github.com/riverqueue/river/riverdriver/riverdatabasesql" + "github.com/riverqueue/river/rivermigrate" +) + +func init() { + goose.AddMigrationNoTxContext(upRiverMigrate006, downRiverMigrate006) +} + +func upRiverMigrate006(ctx context.Context, db *sql.DB) error { + migrator, err := rivermigrate.New(riverdatabasesql.New(db), &rivermigrate.Config{}) + if err != nil { + return fmt.Errorf("init river migrator: %w", err) + } + + _, err = migrator.Migrate(ctx, rivermigrate.DirectionUp, &rivermigrate.MigrateOpts{ + TargetVersion: 6, + }) + if err != nil { + return fmt.Errorf("apply river migration: %w", err) + } + + return nil +} + +func downRiverMigrate006(ctx context.Context, db *sql.DB) error { + migrator, err := rivermigrate.New(riverdatabasesql.New(db), &rivermigrate.Config{}) + if err != nil { + return fmt.Errorf("init river migrator: %w", err) + } + + _, err = migrator.Migrate(ctx, rivermigrate.DirectionDown, &rivermigrate.MigrateOpts{ + TargetVersion: 5, + }) + if err != nil { + return fmt.Errorf("apply river migration: %w", err) + } + + return nil +} diff --git a/app/schema/migrations/20241004101658_create_table_users.sql b/app/schema/migrations/20241004101658_create_table_users.sql new file mode 100644 index 0000000..a9e8601 --- /dev/null +++ b/app/schema/migrations/20241004101658_create_table_users.sql @@ -0,0 +1,7 @@ +-- +goose Up +-- create "users" table +CREATE TABLE "public"."users" ("id" character varying(24) NOT NULL, "email" text NOT NULL, PRIMARY KEY ("id"), CONSTRAINT "users_email_key" UNIQUE ("email")); + +-- +goose Down +-- reverse: create "users" table +DROP TABLE "public"."users"; diff --git a/app/schema/migrations/20241005193820_create_table_counter.sql b/app/schema/migrations/20241005193820_create_table_counter.sql new file mode 100644 index 0000000..5306492 --- /dev/null +++ b/app/schema/migrations/20241005193820_create_table_counter.sql @@ -0,0 +1,7 @@ +-- +goose Up +-- create "counter" table +CREATE TABLE "public"."counter" ("id" serial NOT NULL, "count" integer NOT NULL, "last_increment_by" text NULL, "last_increment_at" timestamptz NULL, "next_finalize_at" timestamptz NULL, PRIMARY KEY ("id"), CONSTRAINT "counter_id_check" CHECK (id = 1)); + +-- +goose Down +-- reverse: create "counter" table +DROP TABLE "public"."counter"; diff --git a/app/schema/migrations/20241005201313_init_table_counter.sql b/app/schema/migrations/20241005201313_init_table_counter.sql new file mode 100644 index 0000000..b00a7e6 --- /dev/null +++ b/app/schema/migrations/20241005201313_init_table_counter.sql @@ -0,0 +1,5 @@ +-- +goose Up +INSERT INTO counter (id, count) VALUES (1, 0); + +-- +goose Down +DELETE FROM counter WHERE id = 1; diff --git a/app/schema/migrations/20241006142909_create_table_increment_requests.sql b/app/schema/migrations/20241006142909_create_table_increment_requests.sql new file mode 100644 index 0000000..c3c2bdc --- /dev/null +++ b/app/schema/migrations/20241006142909_create_table_increment_requests.sql @@ -0,0 +1,7 @@ +-- +goose Up +-- create "increment_requests" table +CREATE TABLE "public"."increment_requests" ("requested_by" text NOT NULL, "requested_at" timestamptz NOT NULL); + +-- +goose Down +-- reverse: create "increment_requests" table +DROP TABLE "public"."increment_requests"; diff --git a/app/schema/migrations/20241008214418_increment_requests_requested_by_unique.sql b/app/schema/migrations/20241008214418_increment_requests_requested_by_unique.sql new file mode 100644 index 0000000..c02a736 --- /dev/null +++ b/app/schema/migrations/20241008214418_increment_requests_requested_by_unique.sql @@ -0,0 +1,7 @@ +-- +goose Up +-- modify "increment_requests" table +ALTER TABLE "public"."increment_requests" ADD CONSTRAINT "increment_requests_requested_by_key" UNIQUE ("requested_by"); + +-- +goose Down +-- reverse: modify "increment_requests" table +ALTER TABLE "public"."increment_requests" DROP CONSTRAINT "increment_requests_requested_by_key"; diff --git a/app/schema/migrations/20241208075149_fkey_reference_users_email.sql b/app/schema/migrations/20241208075149_fkey_reference_users_email.sql new file mode 100644 index 0000000..65e462f --- /dev/null +++ b/app/schema/migrations/20241208075149_fkey_reference_users_email.sql @@ -0,0 +1,11 @@ +-- +goose Up +-- modify "counter" table +ALTER TABLE "public"."counter" ADD CONSTRAINT "counter_last_increment_by_fkey" FOREIGN KEY ("last_increment_by") REFERENCES "public"."users" ("email") ON UPDATE NO ACTION ON DELETE NO ACTION; +-- modify "increment_requests" table +ALTER TABLE "public"."increment_requests" ADD CONSTRAINT "increment_requests_requested_by_fkey" FOREIGN KEY ("requested_by") REFERENCES "public"."users" ("email") ON UPDATE NO ACTION ON DELETE NO ACTION; + +-- +goose Down +-- reverse: modify "increment_requests" table +ALTER TABLE "public"."increment_requests" DROP CONSTRAINT "increment_requests_requested_by_fkey"; +-- reverse: modify "counter" table +ALTER TABLE "public"."counter" DROP CONSTRAINT "counter_last_increment_by_fkey"; diff --git a/app/schema/migrations/atlas.sum b/app/schema/migrations/atlas.sum new file mode 100644 index 0000000..441c6c0 --- /dev/null +++ b/app/schema/migrations/atlas.sum @@ -0,0 +1,7 @@ +h1:pymmzmCzY8LyjEGfqT1AKV+tZiCZ3NPCJMvwVH8ULlY= +20241004101658_create_table_users.sql h1:W2hAVuQOYQz6TOUa/FT36sL2fM+EfXMgRoReo7PNt3k= +20241005193820_create_table_counter.sql h1:UYiw7tLwzkaDE5+5QT5Ag0UuljO4/R4XUrn7p+/p8lg= +20241005201313_init_table_counter.sql h1:zbDMco9T+ioKug4wekDAjPMi12iGsCNJBM+u3QtQiKg= +20241006142909_create_table_increment_requests.sql h1:+vxyq8bLqN3IXRKupCWHqRvEssS9xoaV+wq8gdktqc8= +20241008214418_increment_requests_requested_by_unique.sql h1:s64Iew7HQ+JTwQcryYcD6CZu1xmneWeSlvyl15NHzYo= +20241208075149_fkey_reference_users_email.sql h1:FbsA99P9/zqLu3zp7G7hnshpCnJiICyQRAmaMZIImzs= diff --git a/app/schema/migrations/embed.go b/app/schema/migrations/embed.go new file mode 100644 index 0000000..a46c69c --- /dev/null +++ b/app/schema/migrations/embed.go @@ -0,0 +1,8 @@ +package migrations + +import ( + "embed" +) + +//go:embed *.sql +var FSDir embed.FS diff --git a/app/schema/schema.sql b/app/schema/schema.sql new file mode 100644 index 0000000..d5b9d95 --- /dev/null +++ b/app/schema/schema.sql @@ -0,0 +1,17 @@ +CREATE TABLE users ( + id VARCHAR(24) PRIMARY KEY, + email TEXT NOT NULL UNIQUE +); + +CREATE TABLE counter ( + id SERIAL PRIMARY KEY CHECK (id = 1), + count INTEGER NOT NULL, + last_increment_by TEXT REFERENCES users(email), + last_increment_at TIMESTAMPTZ, + next_finalize_at TIMESTAMPTZ +); + +CREATE UNLOGGED TABLE increment_requests ( + requested_by TEXT UNIQUE NOT NULL REFERENCES users(email), + requested_at TIMESTAMPTZ NOT NULL +); \ No newline at end of file diff --git a/app/schema/users.sql b/app/schema/users.sql new file mode 100644 index 0000000..1c49f40 --- /dev/null +++ b/app/schema/users.sql @@ -0,0 +1,14 @@ +-- name: GetUser :one +SELECT * +FROM users +WHERE id = $1; + +-- name: GetUserByEmail :one +SELECT * +FROM users +WHERE email = $1; + +-- name: InsertUserIfNotExists :exec +INSERT INTO users (id, email) +VALUES ($1, $2) +ON CONFLICT(email) DO NOTHING; \ No newline at end of file diff --git a/app/sqlc.yaml b/app/sqlc.yaml new file mode 100644 index 0000000..3a3705b --- /dev/null +++ b/app/sqlc.yaml @@ -0,0 +1,27 @@ +--- +version: "2" + +sql: +- name: userstore + engine: postgresql + queries: schema/users.sql + schema: schema/schema.sql + gen: + go: + package: userstore + out: internal/service/user/store + sql_package: pgx/v5 + emit_interface: true + emit_methods_with_db_argument: true + +- name: counterstore + engine: postgresql + queries: schema/counter.sql + schema: schema/schema.sql + gen: + go: + package: counterstore + out: internal/service/counter/store + sql_package: pgx/v5 + emit_interface: true + emit_methods_with_db_argument: true diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..dc5833c --- /dev/null +++ b/compose.yaml @@ -0,0 +1,207 @@ +name: countup + +services: + countup: + image: jace-ys/countup:0.0.0 + build: + context: ./app + profiles: [ apps ] + labels: + service: countup + tier: app + environment: local + traefik.enable: true + traefik.http.routers.countup.entrypoints: localhttps + traefik.http.routers.countup.rule: Host(`localhost`) + traefik.http.routers.countup.tls: true + ports: + - 8080:8080 + - 8081:8081 + - 9090:9090 + command: + - server + depends_on: + postgres: + condition: service_healthy + postgres-init: + condition: service_completed_successfully + otel-collector: + condition: service_started + environment: + OTEL_GO_X_EXEMPLAR: true + OTEL_RESOURCE_ATTRIBUTES: tier=app,environment=local + OTLP_METRICS_ENDPOINT: otel-collector:4317 + OTLP_TRACES_ENDPOINT: otel-collector:4317 + DATABASE_CONNECTION_URI: postgresql://countup:countup@postgres:5432/countup + OAUTH_CLIENT_ID: ${OAUTH_CLIENT_ID} + OAUTH_CLIENT_SECRET: ${OAUTH_CLIENT_SECRET} + OAUTH_REDIRECT_URL: https://localhost:4043/login/google/callback + + traefik: + image: traefik:v3.2.1 + labels: + service: traefik + tier: ingress + environment: local + ports: + - 4043:4043 + - 4040:8080 + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ./infra/environments/local/compose/traefik:/etc/traefik + + postgres: + image: postgres:15.8-alpine + labels: + service: postgres + component: primary + tier: database + environment: local + ports: + - 5432:5432 + environment: + POSTGRES_USER: countup + POSTGRES_PASSWORD: countup + POSTGRES_DB: countup + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB" ] + interval: 5s + retries: 3 + start_period: 10s + timeout: 5s + + postgres-init: + image: jace-ys/countup:0.0.0 + build: + context: ./app + labels: + service: postgres + component: init + tier: database + environment: local + command: + - migrate + - up + depends_on: + postgres: + condition: service_healthy + environment: + DATABASE_CONNECTION_URI: postgresql://countup:countup@postgres:5432/countup + MIGRATIONS_DIR: /app/migrations + MIGRATIONS_LOCALFS: true + volumes: + - ./app/schema/migrations:/app/migrations + + postgres-exporter: + image: quay.io/prometheuscommunity/postgres-exporter:v0.15.0 + labels: + service: postgres + component: exporter + tier: database + environment: local + ports: + - 9187:9187 + depends_on: + postgres: + condition: service_healthy + environment: + DATA_SOURCE_URI: postgres:5432/countup?sslmode=disable + DATA_SOURCE_USER: countup + DATA_SOURCE_PASS: countup + + grafana: + image: grafana/grafana:11.1.4 + labels: + service: grafana + tier: observability + environment: local + ports: + - 3000:3000 + command: + - --config=/etc/grafana/config.ini + volumes: + - ./infra/environments/local/compose/grafana/config.ini:/etc/grafana/config.ini + - ./infra/environments/local/compose/grafana/provisioning:/etc/grafana/provisioning + - ./infra/environments/local/compose/grafana/definitions:/var/lib/grafana/dashboards + environment: + - GF_INSTALL_PLUGINS=https://storage.googleapis.com/integration-artifacts/grafana-lokiexplore-app/grafana-lokiexplore-app-latest.zip;grafana-lokiexplore-app + + otel-collector: + image: otel/opentelemetry-collector-contrib:0.107.0 + labels: + service: otel-collector + tier: observability + environment: local + user: '0' + ports: + - 4317:4317 + - 8888:8888 + command: + - --config=/etc/otel-collector/config.yaml + depends_on: + mimir: + condition: service_started + tempo: + condition: service_started + loki: + condition: service_started + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - ./infra/environments/local/compose/otel-collector/config.yaml:/etc/otel-collector/config.yaml + + promtail: + image: grafana/promtail:3.1.1 + labels: + service: promtail + tier: observability + environment: local + ports: + - 3080:3080 + command: + - -config.file=/etc/promtail/config.yaml + depends_on: + loki: + condition: service_started + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - ./infra/environments/local/compose/promtail/config.yaml:/etc/promtail/config.yaml + + loki: + image: grafana/loki:3.1.1 + labels: + service: loki + tier: observability + environment: local + command: + - -config.file=/etc/loki/config.yaml + ports: + - 3100:3100 + volumes: + - ./infra/environments/local/compose/loki/config.yaml:/etc/loki/config.yaml + + tempo: + image: grafana/tempo:2.5.0 + labels: + service: tempo + tier: observability + environment: local + ports: + - 3200:3200 + command: + - -config.file=/etc/tempo/config.yaml + volumes: + - ./infra/environments/local/compose/tempo/config.yaml:/etc/tempo/config.yaml + + mimir: + image: grafana/mimir:2.13.0 + labels: + service: mimir + tier: observability + environment: local + command: + - -ingester.native-histograms-ingestion-enabled=true + - -config.file=/etc/mimir/config.yaml + ports: + - 3300:3300 + volumes: + - ./infra/environments/local/compose/mimir/config.yaml:/etc/mimir/config.yaml diff --git a/infra/environments/dev/.gitkeep b/infra/environments/dev/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/infra/environments/local/compose/grafana/config.ini b/infra/environments/local/compose/grafana/config.ini new file mode 100644 index 0000000..011c225 --- /dev/null +++ b/infra/environments/local/compose/grafana/config.ini @@ -0,0 +1,14 @@ +http_port = 3000 + +[log] +level = warn + +[log.console] +format = json + +[auth] +disable_login_form = true + +[auth.anonymous] +enabled = true +org_role = Admin \ No newline at end of file diff --git a/infra/environments/local/compose/grafana/definitions/otel-collector.json b/infra/environments/local/compose/grafana/definitions/otel-collector.json new file mode 100644 index 0000000..376ab9e --- /dev/null +++ b/infra/environments/local/compose/grafana/definitions/otel-collector.json @@ -0,0 +1,4006 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "description": "Visualize OpenTelemetry (OTEL) collector metrics (tested with OTEL contrib v0.101.0)", + "editable": true, + "fiscalYearStartMonth": 0, + "gnetId": 15983, + "graphTooltip": 1, + "id": 2, + "links": [], + "liveNow": false, + "panels": [ + { + "collapsed": false, + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 23, + "panels": [], + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "refId": "A" + } + ], + "title": "Receivers", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "Accepted: count/rate of spans successfully pushed into the pipeline.\nRefused: count/rate of spans that could not be pushed into the pipeline.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/Refused.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + }, + { + "id": "custom.axisPlacement", + "value": "right" + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 1 + }, + "id": 28, + "interval": "$minstep", + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(${metric:value}(otelcol_receiver_accepted_spans${suffix}{receiver=~\"$receiver\",job=\"$job\"}[$__rate_interval])) by (receiver $grouping)", + "format": "time_series", + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Accepted: {{receiver}} {{transport}} {{service_instance_id}}", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(${metric:value}(otelcol_receiver_refused_spans${suffix}{receiver=~\"$receiver\",job=\"$job\"}[$__rate_interval])) by (receiver $grouping)", + "format": "time_series", + "hide": false, + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Refused: {{receiver}} {{transport}} {{service_instance_id}}", + "range": true, + "refId": "B" + } + ], + "title": "Spans ${metric:text}", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "Accepted: count/rate of metric points successfully pushed into the pipeline.\nRefused: count/rate of metric points that could not be pushed into the pipeline.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/Refused.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + }, + { + "id": "custom.axisPlacement", + "value": "right" + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 1 + }, + "id": 32, + "interval": "$minstep", + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(${metric:value}(otelcol_receiver_accepted_metric_points${suffix}{receiver=~\"$receiver\",job=\"$job\"}[$__rate_interval])) by (receiver $grouping)", + "format": "time_series", + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Accepted: {{receiver}} {{transport}} {{service_instance_id}}", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(${metric:value}(otelcol_receiver_refused_metric_points${suffix}{receiver=~\"$receiver\",job=\"$job\"}[$__rate_interval])) by (receiver $grouping)", + "format": "time_series", + "hide": false, + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Refused: {{receiver}} {{transport}} {{service_instance_id}}", + "range": true, + "refId": "B" + } + ], + "title": "Metric Points ${metric:text}", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "Accepted: count/rate of log records successfully pushed into the pipeline.\nRefused: count/rate of log records that could not be pushed into the pipeline.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/Refused.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + }, + { + "id": "custom.axisPlacement", + "value": "right" + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 1 + }, + "id": 47, + "interval": "$minstep", + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(${metric:value}(otelcol_receiver_accepted_log_records${suffix}{receiver=~\"$receiver\",job=\"$job\"}[$__rate_interval])) by (receiver $grouping)", + "format": "time_series", + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Accepted: {{receiver}} {{transport}} {{service_instance_id}}", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(${metric:value}(otelcol_receiver_refused_log_records${suffix}{receiver=~\"$receiver\",job=\"$job\"}[$__rate_interval])) by (receiver $grouping)", + "format": "time_series", + "hide": false, + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Refused: {{receiver}} {{transport}} {{service_instance_id}}", + "range": true, + "refId": "B" + } + ], + "title": "Log Records ${metric:text}", + "type": "timeseries" + }, + { + "collapsed": false, + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 9 + }, + "id": 34, + "panels": [], + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "refId": "A" + } + ], + "title": "Processors", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "Accepted: count/rate of spans successfully pushed into the next component in the pipeline.\nRefused: count/rate of spans that were rejected by the next component in the pipeline.\nDropped: count/rate of spans that were dropped", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/Refused.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + }, + { + "id": "custom.axisPlacement", + "value": "right" + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/Dropped.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "purple", + "mode": "fixed" + } + }, + { + "id": "custom.axisPlacement", + "value": "right" + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 10 + }, + "id": 35, + "interval": "$minstep", + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(${metric:value}(otelcol_processor_accepted_spans${suffix}{processor=~\"$processor\",job=\"$job\"}[$__rate_interval])) by (processor $grouping)", + "format": "time_series", + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Accepted: {{processor}} {{service_instance_id}}", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(${metric:value}(otelcol_processor_refused_spans${suffix}{processor=~\"$processor\",job=\"$job\"}[$__rate_interval])) by (processor $grouping)", + "format": "time_series", + "hide": false, + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Refused: {{processor}} {{service_instance_id}}", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(${metric:value}(otelcol_processor_dropped_spans${suffix}{processor=~\"$processor\",job=\"$job\"}[$__rate_interval])) by (processor $grouping)", + "format": "time_series", + "hide": false, + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Dropped: {{processor}} {{service_instance_id}}", + "range": true, + "refId": "C" + } + ], + "title": "Spans ${metric:text}", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "Accepted: count/rate of metric points successfully pushed into the next component in the pipeline.\nRefused: count/rate of metric points that were rejected by the next component in the pipeline.\nDropped: count/rate of metric points that were dropped", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/Refused.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + }, + { + "id": "custom.axisPlacement", + "value": "right" + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/Dropped.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "purple", + "mode": "fixed" + } + }, + { + "id": "custom.axisPlacement", + "value": "right" + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 10 + }, + "id": 50, + "interval": "$minstep", + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(${metric:value}(otelcol_processor_accepted_metric_points${suffix}{processor=~\"$processor\",job=\"$job\"}[$__rate_interval])) by (processor $grouping)", + "format": "time_series", + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Accepted: {{processor}} {{service_instance_id}}", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(${metric:value}(otelcol_processor_refused_metric_points${suffix}{processor=~\"$processor\",job=\"$job\"}[$__rate_interval])) by (processor $grouping)", + "format": "time_series", + "hide": false, + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Refused: {{processor}} {{service_instance_id}}", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(${metric:value}(otelcol_processor_dropped_metric_points${suffix}{processor=~\"$processor\",job=\"$job\"}[$__rate_interval])) by (processor)", + "format": "time_series", + "hide": false, + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Dropped: {{processor}} {{service_instance_id}}", + "range": true, + "refId": "C" + } + ], + "title": "Metric Points ${metric:text}", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "Accepted: count/rate of log records successfully pushed into the next component in the pipeline.\nRefused: count/rate of log records that were rejected by the next component in the pipeline.\nDropped: count/rate of log records that were dropped", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/Refused.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + }, + { + "id": "custom.axisPlacement", + "value": "right" + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/Dropped.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "purple", + "mode": "fixed" + } + }, + { + "id": "custom.axisPlacement", + "value": "right" + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 10 + }, + "id": 51, + "interval": "$minstep", + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(${metric:value}(otelcol_processor_accepted_log_records${suffix}{processor=~\"$processor\",job=\"$job\"}[$__rate_interval])) by (processor $grouping)", + "format": "time_series", + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Accepted: {{processor}} {{service_instance_id}}", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(${metric:value}(otelcol_processor_refused_log_records${suffix}{processor=~\"$processor\",job=\"$job\"}[$__rate_interval])) by (processor $grouping)", + "format": "time_series", + "hide": false, + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Refused: {{processor}} {{service_instance_id}}", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(${metric:value}(otelcol_processor_dropped_log_records${suffix}{processor=~\"$processor\",job=\"$job\"}[$__rate_interval])) by (processor)", + "format": "time_series", + "hide": false, + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Dropped: {{processor}} {{service_instance_id}}", + "range": true, + "refId": "C" + } + ], + "title": "Log Records ${metric:text}", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "Number of units in the batch", + "fieldConfig": { + "defaults": { + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "scaleDistribution": { + "type": "linear" + } + }, + "links": [] + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 18 + }, + "id": 49, + "interval": "$minstep", + "maxDataPoints": 50, + "options": { + "calculate": false, + "cellGap": 1, + "color": { + "exponent": 0.5, + "fill": "dark-orange", + "mode": "scheme", + "reverse": true, + "scale": "exponential", + "scheme": "Reds", + "steps": 57 + }, + "exemplars": { + "color": "rgba(255,0,255,0.7)" + }, + "filterValues": { + "le": 1e-9 + }, + "legend": { + "show": true + }, + "rowsFrame": { + "layout": "auto" + }, + "tooltip": { + "mode": "single", + "showColorScale": false, + "yHistogram": false + }, + "yAxis": { + "axisPlacement": "left", + "reverse": false + } + }, + "pluginVersion": "11.1.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(increase(otelcol_processor_batch_batch_send_size_bucket{processor=~\"$processor\",job=\"$job\"}[$__rate_interval])) by (le)", + "format": "heatmap", + "hide": false, + "instant": false, + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "{{le}}", + "refId": "B" + } + ], + "title": "Batch Send Size Heatmap", + "type": "heatmap" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Refused.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*Dropped.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "purple", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 18 + }, + "id": 36, + "interval": "$minstep", + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(${metric:value}(otelcol_processor_batch_batch_send_size_count{processor=~\"$processor\",job=\"$job\"}[$__rate_interval])) by (processor $grouping)", + "format": "time_series", + "hide": false, + "instant": false, + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Batch send size count: {{processor}} {{service_instance_id}}", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(${metric:value}(otelcol_processor_batch_batch_send_size_sum{processor=~\"$processor\",job=\"$job\"}[$__rate_interval])) by (processor $grouping)", + "format": "time_series", + "hide": false, + "instant": false, + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Batch send size sum: {{processor}} {{service_instance_id}}", + "refId": "A" + } + ], + "title": "Batch Metrics 1", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "Number of times the batch was sent due to a size trigger. Number of times the batch was sent due to a timeout trigger.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Refused.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*Dropped.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "purple", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 18 + }, + "id": 56, + "interval": "$minstep", + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(${metric:value}(otelcol_processor_batch_batch_size_trigger_send${suffix}{processor=~\"$processor\",job=\"$job\"}[$__rate_interval])) by (processor $grouping)", + "format": "time_series", + "hide": false, + "instant": false, + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Batch sent due to a size trigger: {{processor}}", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(${metric:value}(otelcol_processor_batch_timeout_trigger_send${suffix}{processor=~\"$processor\",job=\"$job\"}[$__rate_interval])) by (processor $grouping)", + "format": "time_series", + "hide": false, + "instant": false, + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Batch sent due to a timeout trigger: {{processor}} {{service_instance_id}}", + "refId": "A" + } + ], + "title": "Batch Metrics 2", + "type": "timeseries" + }, + { + "collapsed": false, + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 26 + }, + "id": 25, + "panels": [], + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "refId": "A" + } + ], + "title": "Exporters", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "Sent: count/rate of spans successfully sent to destination.\nEnqueue: count/rate of spans failed to be added to the sending queue.\nFailed: count/rate of spans in failed attempts to send to destination.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/Failed:.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + }, + { + "id": "custom.axisPlacement", + "value": "right" + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 8, + "x": 0, + "y": 27 + }, + "id": 37, + "interval": "$minstep", + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(${metric:value}(otelcol_exporter_sent_spans${suffix}{exporter=~\"$exporter\",job=\"$job\"}[$__rate_interval])) by (exporter $grouping)", + "format": "time_series", + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Sent: {{exporter}} {{service_instance_id}}", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(${metric:value}(otelcol_exporter_enqueue_failed_spans${suffix}{exporter=~\"$exporter\",job=\"$job\"}[$__rate_interval])) by (exporter $grouping)", + "format": "time_series", + "hide": false, + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Enqueue: {{exporter}} {{service_instance_id}}", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(${metric:value}(otelcol_exporter_send_failed_spans${suffix}{exporter=~\"$exporter\",job=\"$job\"}[$__rate_interval])) by (exporter $grouping)", + "format": "time_series", + "hide": false, + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Failed: {{exporter}} {{service_instance_id}}", + "range": true, + "refId": "C" + } + ], + "title": "Spans ${metric:text}", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "Sent: count/rate of metric points successfully sent to destination.\nEnqueue: count/rate of metric points failed to be added to the sending queue.\nFailed: count/rate of metric points in failed attempts to send to destination.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/Failed:.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + }, + { + "id": "custom.axisPlacement", + "value": "right" + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 8, + "x": 8, + "y": 27 + }, + "id": 38, + "interval": "$minstep", + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(${metric:value}(otelcol_exporter_sent_metric_points${suffix}{exporter=~\"$exporter\",job=\"$job\"}[$__rate_interval])) by (exporter $grouping)", + "format": "time_series", + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Sent: {{exporter}} {{service_instance_id}}", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(${metric:value}(otelcol_exporter_enqueue_failed_metric_points${suffix}{exporter=~\"$exporter\",job=\"$job\"}[$__rate_interval])) by (exporter $grouping)", + "format": "time_series", + "hide": false, + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Enqueue: {{exporter}} {{service_instance_id}}", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(${metric:value}(otelcol_exporter_send_failed_metric_points${suffix}{exporter=~\"$exporter\",job=\"$job\"}[$__rate_interval])) by (exporter $grouping)", + "format": "time_series", + "hide": false, + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Failed: {{exporter}} {{service_instance_id}}", + "range": true, + "refId": "C" + } + ], + "title": "Metric Points ${metric:text}", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "Sent: count/rate of log records successfully sent to destination.\nEnqueue: count/rate of log records failed to be added to the sending queue.\nFailed: count/rate of log records in failed attempts to send to destination.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/Failed:.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + }, + { + "id": "custom.axisPlacement", + "value": "right" + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 8, + "x": 16, + "y": 27 + }, + "id": 48, + "interval": "$minstep", + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(${metric:value}(otelcol_exporter_sent_log_records${suffix}{exporter=~\"$exporter\",job=\"$job\"}[$__rate_interval])) by (exporter $grouping)", + "format": "time_series", + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Sent: {{exporter}} {{service_instance_id}}", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(${metric:value}(otelcol_exporter_enqueue_failed_log_records${suffix}{exporter=~\"$exporter\",job=\"$job\"}[$__rate_interval])) by (exporter $grouping)", + "format": "time_series", + "hide": false, + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Enqueue: {{exporter}} {{service_instance_id}}", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(${metric:value}(otelcol_exporter_send_failed_log_records${suffix}{exporter=~\"$exporter\",job=\"$job\"}[$__rate_interval])) by (exporter $grouping)", + "format": "time_series", + "hide": false, + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Failed: {{exporter}} {{service_instance_id}}", + "range": true, + "refId": "C" + } + ], + "title": "Log Records ${metric:text}", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "Current size of the retry queue (in batches)", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 8, + "x": 0, + "y": 36 + }, + "id": 10, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "max(otelcol_exporter_queue_size{exporter=~\"$exporter\",job=\"$job\"}) by (exporter $grouping)", + "format": "time_series", + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Max queue size: {{exporter}} {{service_instance_id}}", + "range": true, + "refId": "A" + } + ], + "title": "Exporter Queue Size", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "Fixed capacity of the retry queue (in batches)", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 8, + "x": 8, + "y": 36 + }, + "id": 55, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "min(otelcol_exporter_queue_capacity{exporter=~\"$exporter\",job=\"$job\"}) by (exporter $grouping)", + "format": "time_series", + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Queue capacity: {{exporter}} {{service_instance_id}}", + "range": true, + "refId": "A" + } + ], + "title": "Exporter Queue Capacity", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "max": 1, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 8, + "x": 16, + "y": 36 + }, + "id": 67, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "max(\r\n otelcol_exporter_queue_size{\r\n exporter=~\"$exporter\", job=\"$job\"\r\n }\r\n) by (exporter $grouping)\r\n/\r\nmin(\r\n otelcol_exporter_queue_capacity{\r\n exporter=~\"$exporter\", job=\"$job\"\r\n }\r\n) by (exporter $grouping)", + "format": "time_series", + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Queue capacity usage: {{exporter}} {{service_instance_id}}", + "range": true, + "refId": "A" + } + ], + "title": "Exporter Queue Usage", + "type": "timeseries" + }, + { + "collapsed": false, + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 45 + }, + "id": 21, + "panels": [], + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "refId": "A" + } + ], + "title": "Collector", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "Total physical memory (resident set size)", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Max Memory RSS " + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + }, + { + "id": "custom.fillBelowTo", + "value": "Avg Memory RSS " + }, + { + "id": "custom.lineWidth", + "value": 0 + }, + { + "id": "custom.fillOpacity", + "value": 20 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Min Memory RSS " + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "yellow", + "mode": "fixed" + } + }, + { + "id": "custom.lineWidth", + "value": 0 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Avg Memory RSS " + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "orange", + "mode": "fixed" + } + }, + { + "id": "custom.fillBelowTo", + "value": "Min Memory RSS " + }, + { + "id": "custom.fillOpacity", + "value": 20 + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 8, + "x": 0, + "y": 46 + }, + "id": 40, + "interval": "$minstep", + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "max(otelcol_process_memory_rss{job=\"$job\"}) by (job $grouping)", + "format": "time_series", + "hide": false, + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Max Memory RSS {{service_instance_id}}", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "avg(otelcol_process_memory_rss{job=\"$job\"}) by (job $grouping)", + "format": "time_series", + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Avg Memory RSS {{service_instance_id}}", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "min(otelcol_process_memory_rss{job=\"$job\"}) by (job $grouping)", + "format": "time_series", + "hide": false, + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Min Memory RSS {{service_instance_id}}", + "range": true, + "refId": "B" + } + ], + "title": "Total RSS Memory", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "Total bytes of memory obtained from the OS (see 'go doc runtime.MemStats.Sys')", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Max Memory RSS " + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + }, + { + "id": "custom.fillBelowTo", + "value": "Avg Memory RSS " + }, + { + "id": "custom.lineWidth", + "value": 0 + }, + { + "id": "custom.fillOpacity", + "value": 20 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Min Memory RSS " + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "yellow", + "mode": "fixed" + } + }, + { + "id": "custom.lineWidth", + "value": 0 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Avg Memory RSS " + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "orange", + "mode": "fixed" + } + }, + { + "id": "custom.fillBelowTo", + "value": "Min Memory RSS " + }, + { + "id": "custom.fillOpacity", + "value": 20 + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 8, + "x": 8, + "y": 46 + }, + "id": 52, + "interval": "$minstep", + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "max(otelcol_process_runtime_total_sys_memory_bytes{job=\"$job\"}) by (job $grouping)", + "format": "time_series", + "hide": false, + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Max Memory RSS {{service_instance_id}}", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "avg(otelcol_process_runtime_total_sys_memory_bytes{job=\"$job\"}) by (job $grouping)", + "format": "time_series", + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Avg Memory RSS {{service_instance_id}}", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "min(otelcol_process_runtime_total_sys_memory_bytes{job=\"$job\"}) by (job $grouping)", + "format": "time_series", + "hide": false, + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Min Memory RSS {{service_instance_id}}", + "range": true, + "refId": "B" + } + ], + "title": "Total Runtime Sys Memory", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "Bytes of allocated heap objects (see 'go doc runtime.MemStats.HeapAlloc')", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Max Memory RSS " + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + }, + { + "id": "custom.fillBelowTo", + "value": "Avg Memory RSS " + }, + { + "id": "custom.lineWidth", + "value": 0 + }, + { + "id": "custom.fillOpacity", + "value": 20 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Min Memory RSS " + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "yellow", + "mode": "fixed" + } + }, + { + "id": "custom.lineWidth", + "value": 0 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Avg Memory RSS " + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "orange", + "mode": "fixed" + } + }, + { + "id": "custom.fillBelowTo", + "value": "Min Memory RSS " + }, + { + "id": "custom.fillOpacity", + "value": 20 + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 8, + "x": 16, + "y": 46 + }, + "id": 53, + "interval": "$minstep", + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "max(otelcol_process_runtime_heap_alloc_bytes{job=\"$job\"}) by (job $grouping)", + "format": "time_series", + "hide": false, + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Max Memory RSS {{service_instance_id}}", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "avg(otelcol_process_runtime_heap_alloc_bytes{job=\"$job\"}) by (job $grouping)", + "format": "time_series", + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Avg Memory RSS {{service_instance_id}}", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "min(otelcol_process_runtime_heap_alloc_bytes{job=\"$job\"}) by (job $grouping)", + "format": "time_series", + "hide": false, + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Min Memory RSS {{service_instance_id}}", + "range": true, + "refId": "B" + } + ], + "title": "Total Runtime Heap Memory", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "Total CPU user and system time in percentage", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percent" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Max CPU usage " + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + }, + { + "id": "custom.fillBelowTo", + "value": "Avg CPU usage " + }, + { + "id": "custom.lineWidth", + "value": 0 + }, + { + "id": "custom.fillOpacity", + "value": 20 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Avg CPU usage " + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "orange", + "mode": "fixed" + } + }, + { + "id": "custom.fillBelowTo", + "value": "Min CPU usage " + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Min CPU usage " + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "yellow", + "mode": "fixed" + } + }, + { + "id": "custom.lineWidth", + "value": 0 + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 8, + "x": 0, + "y": 55 + }, + "id": 39, + "interval": "$minstep", + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "max(rate(otelcol_process_cpu_seconds${suffix}{job=\"$job\"}[$__rate_interval])*100) by (job $grouping)", + "format": "time_series", + "hide": false, + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Max CPU usage {{service_instance_id}}", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "avg(rate(otelcol_process_cpu_seconds${suffix}{job=\"$job\"}[$__rate_interval])*100) by (job $grouping)", + "format": "time_series", + "hide": false, + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Avg CPU usage {{service_instance_id}}", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "min(rate(otelcol_process_cpu_seconds${suffix}{job=\"$job\"}[$__rate_interval])*100) by (job $grouping)", + "format": "time_series", + "hide": false, + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Min CPU usage {{service_instance_id}}", + "range": true, + "refId": "C" + } + ], + "title": "CPU Usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "Number of service instances, which are reporting metrics", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 8, + "x": 8, + "y": 55 + }, + "id": 41, + "interval": "$minstep", + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "count(count(otelcol_process_cpu_seconds${suffix}{service_instance_id=~\".*\",job=\"$job\"}) by (service_instance_id))", + "format": "time_series", + "hide": false, + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Service instance count", + "range": true, + "refId": "B" + } + ], + "title": "Service Instance Count", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 8, + "x": 16, + "y": 55 + }, + "id": 54, + "interval": "$minstep", + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": true, + "expr": "max(otelcol_process_uptime${suffix}{service_instance_id=~\".*\",job=\"$job\"}) by (service_instance_id)", + "format": "time_series", + "hide": false, + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "Service instance uptime: {{service_instance_id}}", + "range": true, + "refId": "B" + } + ], + "title": "Uptime by Service Instance", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 24, + "x": 0, + "y": 64 + }, + "id": 57, + "interval": "$minstep", + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "11.1.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": false, + "expr": "max(otelcol_process_uptime${suffix}{service_instance_id=~\".*\",job=\"$job\"}) by (service_instance_id,service_name,service_version)", + "format": "table", + "hide": false, + "instant": true, + "interval": "$minstep", + "intervalFactor": 1, + "legendFormat": "__auto", + "range": false, + "refId": "B" + } + ], + "title": "Service Instance Details", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true, + "Value": true + }, + "indexByName": {}, + "renameByName": {} + } + } + ], + "type": "table" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 69 + }, + "id": 59, + "panels": [], + "title": "Signal Flows", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "Receivers -> Processor(s) -> Exporters (Node Graph panel is beta, so this panel may not show data correctly).", + "gridPos": { + "h": 9, + "w": 8, + "x": 0, + "y": 70 + }, + "id": 58, + "options": { + "edges": {}, + "nodes": { + "mainStatUnit": "flops" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": false, + "expr": "# receivers\nlabel_replace(\n label_join(\n label_join(\n sum(${metric:value}(\n otelcol_receiver_accepted_spans${suffix}{job=\"$job\"}[$__rate_interval])\n ) by (receiver)\n , \"id\", \"-rcv-\", \"transport\", \"receiver\"\n )\n , \"title\", \"\", \"transport\", \"receiver\"\n )\n , \"icon\", \"arrow-to-right\", \"\", \"\"\n)\n\n# dummy processor\nor\nlabel_replace(\n label_replace(\n label_replace(\n (sum(rate(otelcol_process_uptime${suffix}{job=\"$job\"}[$__rate_interval])))\n , \"id\", \"processor\", \"\", \"\"\n )\n , \"title\", \"Processor(s)\", \"\", \"\"\n )\n , \"icon\", \"arrow-random\", \"\", \"\"\n)\n\n# exporters\nor\nlabel_replace(\n label_join(\n label_join(\n sum(${metric:value}(\n otelcol_exporter_sent_spans${suffix}{job=\"$job\"}[$__rate_interval])\n ) by (exporter)\n , \"id\", \"-exp-\", \"transport\", \"exporter\"\n )\n , \"title\", \"\", \"transport\", \"exporter\"\n )\n , \"icon\", \"arrow-from-right\", \"\", \"\"\n)", + "format": "table", + "hide": false, + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "nodes" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": false, + "expr": "# receivers -> processor\r\nlabel_join(\r\n label_replace(\r\n label_join(\r\n (sum(rate(otelcol_receiver_accepted_spans${suffix}{job=\"$job\"}[$__rate_interval])) by (receiver))\r\n ,\"source\", \"-rcv-\", \"transport\", \"receiver\"\r\n )\r\n ,\"target\", \"processor\", \"\", \"\"\r\n )\r\n , \"id\", \"-\", \"source\", \"target\"\r\n)\r\n\r\n# processor -> exporters\r\nor\r\nlabel_join(\r\n label_replace(\r\n label_join(\r\n (sum(rate(otelcol_exporter_sent_spans${suffix}{job=\"$job\"}[$__rate_interval])) by (exporter))\r\n , \"target\", \"-exp-\", \"transport\", \"exporter\"\r\n )\r\n , \"source\", \"processor\", \"\", \"\"\r\n )\r\n , \"id\", \"-\", \"source\", \"target\"\r\n)", + "format": "table", + "hide": false, + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "edges" + } + ], + "title": "Spans Flow", + "transformations": [ + { + "id": "renameByRegex", + "options": { + "regex": "Value", + "renamePattern": "mainstat" + } + }, + { + "disabled": true, + "id": "calculateField", + "options": { + "alias": "secondarystat", + "mode": "reduceRow", + "reduce": { + "include": [ + "mainstat" + ], + "reducer": "sum" + } + } + } + ], + "type": "nodeGraph" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "Receivers -> Processor(s) -> Exporters (Node Graph panel is beta, so this panel may not show data correctly).", + "gridPos": { + "h": 9, + "w": 8, + "x": 8, + "y": 70 + }, + "id": 60, + "options": { + "edges": {}, + "nodes": { + "mainStatUnit": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": false, + "expr": "# receivers\nlabel_replace(\n label_join(\n label_join(\n (sum(\n ${metric:value}(otelcol_receiver_accepted_metric_points${suffix}{job=\"$job\"}[$__rate_interval])\n ) by (receiver))\n , \"id\", \"-rcv-\", \"transport\", \"receiver\"\n )\n , \"title\", \"\", \"transport\", \"receiver\"\n )\n , \"icon\", \"arrow-to-right\", \"\", \"\"\n)\n\n# dummy processor\nor\nlabel_replace(\n label_replace(\n label_replace(\n (sum(rate(otelcol_process_uptime${suffix}{job=\"$job\"}[$__rate_interval])))\n , \"id\", \"processor\", \"\", \"\"\n )\n , \"title\", \"Processor(s)\", \"\", \"\"\n )\n , \"icon\", \"arrow-random\", \"\", \"\"\n)\n\n# exporters\nor\nlabel_replace(\n label_join(\n label_join(\n (sum(\n ${metric:value}(otelcol_exporter_sent_metric_points${suffix}{job=\"$job\"}[$__rate_interval])\n ) by (exporter))\n , \"id\", \"-exp-\", \"transport\", \"exporter\"\n )\n , \"title\", \"\", \"transport\", \"exporter\"\n )\n , \"icon\", \"arrow-from-right\", \"\", \"\"\n)", + "format": "table", + "hide": false, + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "nodes" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": false, + "expr": "# receivers -> processor\r\nlabel_join(\r\n label_replace(\r\n label_join(\r\n (sum(rate(otelcol_receiver_accepted_metric_points${suffix}{job=\"$job\"}[$__rate_interval])) by (receiver))\r\n , \"source\", \"-rcv-\", \"transport\", \"receiver\"\r\n )\r\n , \"target\", \"processor\", \"\", \"\"\r\n )\r\n , \"id\", \"-\", \"source\", \"target\"\r\n)\r\n\r\n# processor -> exporters\r\nor \r\nlabel_join(\r\n label_replace(\r\n label_join(\r\n (sum(rate(otelcol_exporter_sent_metric_points${suffix}{job=\"$job\"}[$__rate_interval])) by (exporter))\r\n , \"target\", \"-exp-\", \"transport\", \"exporter\"\r\n )\r\n , \"source\", \"processor\", \"\", \"\"\r\n )\r\n , \"id\", \"-\", \"source\", \"target\"\r\n)", + "format": "table", + "hide": false, + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "edges" + } + ], + "title": "Metric Points Flow", + "transformations": [ + { + "id": "renameByRegex", + "options": { + "regex": "Value", + "renamePattern": "mainstat" + } + }, + { + "disabled": true, + "id": "calculateField", + "options": { + "alias": "secondarystat", + "mode": "reduceRow", + "reduce": { + "include": [ + "Value #nodes" + ], + "reducer": "sum" + } + } + } + ], + "type": "nodeGraph" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "Receivers -> Processor(s) -> Exporters (Node Graph panel is beta, so this panel may not show data correctly).", + "gridPos": { + "h": 9, + "w": 8, + "x": 16, + "y": 70 + }, + "id": 61, + "options": { + "edges": {}, + "nodes": { + "mainStatUnit": "flops" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": false, + "expr": "# receivers\nlabel_replace(\n label_join(\n label_join(\n sum(${metric:value}(\n otelcol_receiver_accepted_log_records${suffix}{job=\"$job\"}[$__rate_interval])\n ) by (receiver)\n , \"id\", \"-rcv-\", \"transport\", \"receiver\"\n )\n , \"title\", \"\", \"transport\", \"receiver\"\n )\n , \"icon\", \"arrow-to-right\", \"\", \"\"\n)\n\n# dummy processor\nor\nlabel_replace(\n label_replace(\n label_replace(\n (sum(rate(otelcol_process_uptime${suffix}{job=\"$job\"}[$__rate_interval])))\n , \"id\", \"processor\", \"\", \"\"\n )\n , \"title\", \"Processor(s)\", \"\", \"\"\n )\n , \"icon\", \"arrow-random\", \"\", \"\"\n)\n\n# exporters\nor\nlabel_replace(\n label_join(\n label_join(\n sum(${metric:value}(\n otelcol_exporter_sent_log_records${suffix}{job=\"$job\"}[$__rate_interval])\n ) by (exporter)\n , \"id\", \"-exp-\", \"transport\", \"exporter\"\n )\n , \"title\", \"\", \"transport\", \"exporter\"\n )\n , \"icon\", \"arrow-from-right\", \"\", \"\"\n)", + "format": "table", + "hide": false, + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "nodes" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": false, + "expr": "# receivers -> processor\r\nlabel_join(\r\n label_replace(\r\n label_join(\r\n (sum(rate(otelcol_receiver_accepted_log_records${suffix}{job=\"$job\"}[$__rate_interval])) by (receiver))\r\n , \"source\", \"-rcv-\", \"transport\", \"receiver\"\r\n )\r\n , \"target\", \"processor\", \"\", \"\"\r\n )\r\n , \"id\", \"-edg-\", \"source\", \"target\"\r\n)\r\n\r\n# processor -> exporters\r\nor \r\nlabel_join(\r\n label_replace(\r\n label_join(\r\n (sum(rate(otelcol_exporter_sent_log_records${suffix}{job=\"$job\"}[$__rate_interval])) by (exporter))\r\n ,\"target\",\"-exp-\",\"transport\",\"exporter\"\r\n )\r\n ,\"source\",\"processor\",\"\",\"\"\r\n )\r\n ,\"id\",\"-edg-\",\"source\",\"target\"\r\n)", + "format": "table", + "hide": false, + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "edges" + } + ], + "title": "Log Records Flow", + "transformations": [ + { + "id": "renameByRegex", + "options": { + "regex": "Value", + "renamePattern": "mainstat" + } + }, + { + "disabled": true, + "id": "calculateField", + "options": { + "alias": "secondarystat", + "mode": "reduceRow", + "reduce": { + "include": [ + "mainstat" + ], + "reducer": "sum" + } + } + } + ], + "type": "nodeGraph" + } + ], + "refresh": "30s", + "schemaVersion": 39, + "tags": [], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "Mimir", + "value": "mimir" + }, + "hide": 0, + "includeAll": false, + "label": "Datasource", + "multi": false, + "name": "datasource", + "options": [], + "query": "prometheus", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + }, + { + "current": { + "selected": false, + "text": "otel-collector", + "value": "otel-collector" + }, + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "definition": "label_values(otelcol_process_memory_rss,job)", + "hide": 0, + "includeAll": false, + "label": "Job", + "multi": false, + "name": "job", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(otelcol_process_memory_rss,job)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + }, + { + "auto": true, + "auto_count": 300, + "auto_min": "30s", + "current": { + "selected": true, + "text": "auto", + "value": "$__auto_interval_minstep" + }, + "hide": 0, + "label": "Min step", + "name": "minstep", + "options": [ + { + "selected": true, + "text": "auto", + "value": "$__auto_interval_minstep" + }, + { + "selected": false, + "text": "10s", + "value": "10s" + }, + { + "selected": false, + "text": "30s", + "value": "30s" + }, + { + "selected": false, + "text": "1m", + "value": "1m" + }, + { + "selected": false, + "text": "5m", + "value": "5m" + } + ], + "query": "10s,30s,1m,5m", + "queryValue": "", + "refresh": 2, + "skipUrlSync": false, + "type": "interval" + }, + { + "current": { + "selected": true, + "text": "Rate", + "value": "rate" + }, + "hide": 0, + "includeAll": false, + "label": "Base metric", + "multi": false, + "name": "metric", + "options": [ + { + "selected": true, + "text": "Rate", + "value": "rate" + }, + { + "selected": false, + "text": "Count", + "value": "increase" + } + ], + "query": "Rate : rate, Count : increase", + "queryValue": "", + "skipUrlSync": false, + "type": "custom" + }, + { + "allValue": ".*", + "current": { + "selected": false, + "text": "All", + "value": "$__all" + }, + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "definition": "query_result(avg by (receiver) ({__name__=~\"otelcol_receiver_.+\",job=\"$job\"}))", + "hide": 0, + "includeAll": true, + "label": "Receiver", + "multi": false, + "name": "receiver", + "options": [], + "query": { + "qryType": 3, + "query": "query_result(avg by (receiver) ({__name__=~\"otelcol_receiver_.+\",job=\"$job\"}))", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 2, + "regex": "/.*receiver=\"(.*)\".*/", + "skipUrlSync": false, + "sort": 1, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": ".*", + "current": { + "selected": false, + "text": "All", + "value": "$__all" + }, + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "definition": "query_result(avg by (processor) ({__name__=~\"otelcol_processor_.+\",job=\"$job\"}))", + "hide": 0, + "includeAll": true, + "label": "Processor", + "multi": false, + "name": "processor", + "options": [], + "query": { + "qryType": 3, + "query": "query_result(avg by (processor) ({__name__=~\"otelcol_processor_.+\",job=\"$job\"}))", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 2, + "regex": "/.*processor=\"(.*)\".*/", + "skipUrlSync": false, + "sort": 1, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": ".*", + "current": { + "selected": false, + "text": "All", + "value": "$__all" + }, + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "definition": "query_result(avg by (exporter) ({__name__=~\"otelcol_exporter_.+\",job=\"$job\"}))", + "hide": 0, + "includeAll": true, + "label": "Exporter", + "multi": false, + "name": "exporter", + "options": [], + "query": { + "qryType": 3, + "query": "query_result(avg by (exporter) ({__name__=~\"otelcol_exporter_.+\",job=\"$job\"}))", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 2, + "regex": "/.*exporter=\"(.*)\".*/", + "skipUrlSync": false, + "sort": 1, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "current": { + "selected": true, + "text": "None (basic metrics)", + "value": "" + }, + "description": "Detailed metrics must be configured in the collector configuration. They add grouping by transport protocol (http/grpc) for receivers. ", + "hide": 0, + "includeAll": false, + "label": "Additional groupping", + "multi": false, + "name": "grouping", + "options": [ + { + "selected": true, + "text": "None (basic metrics)", + "value": "" + }, + { + "selected": false, + "text": "By transport (detailed metrics)", + "value": ",transport" + }, + { + "selected": false, + "text": "By service instance id", + "value": ",service_instance_id" + } + ], + "query": "None (basic metrics) : , By transport (detailed metrics) : \\,transport, By service instance id : \\,service_instance_id", + "queryValue": "", + "skipUrlSync": false, + "type": "custom" + }, + { + "current": { + "selected": false, + "text": "_total", + "value": "_total" + }, + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "definition": "query_result({__name__=~\"otelcol_process_uptime.+\",job=\"$job\"})", + "description": "Some newer prometheusremotewrite exporter versions/configurations add _total suffix, so this hidden variable detects if _total suffix should be used or not", + "hide": 2, + "includeAll": false, + "label": "Suffix", + "multi": false, + "name": "suffix", + "options": [], + "query": { + "qryType": 3, + "query": "query_result({__name__=~\"otelcol_process_uptime.+\",job=\"$job\"})", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "/otelcol_process_uptime(.*){.*/", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "filters": [], + "hide": 0, + "label": "Ad Hoc", + "name": "adhoc", + "skipUrlSync": false, + "type": "adhoc" + } + ] + }, + "time": { + "from": "now-15m", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "OpenTelemetry Collector", + "uid": "BKf2sowmj", + "version": 1, + "weekStart": "" +} \ No newline at end of file diff --git a/infra/environments/local/compose/grafana/definitions/service.json b/infra/environments/local/compose/grafana/definitions/service.json new file mode 100644 index 0000000..3fd5fb6 --- /dev/null +++ b/infra/environments/local/compose/grafana/definitions/service.json @@ -0,0 +1,4322 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 1, + "links": [], + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 26, + "panels": [], + "title": "Go Runtime", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "description": "", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 1 + }, + "id": 27, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.1.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "editorMode": "code", + "exemplar": false, + "expr": "avg(process_runtime_go_goroutines{service=\"$service\", environment=\"$environment\"})", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Goroutines", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "bars", + "fillOpacity": 100, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ns" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 1 + }, + "id": 28, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.1.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "editorMode": "code", + "exemplar": false, + "expr": "histogram_quantile(0.5, avg(rate(process_runtime_go_gc_pause_ns_bucket{service=\"$service\", environment=\"$environment\"}[$__rate_interval])) by (le))", + "legendFormat": "P50", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "editorMode": "code", + "exemplar": false, + "expr": "histogram_quantile(0.99, avg(rate(process_runtime_go_gc_pause_ns_bucket{service=\"$service\", environment=\"$environment\"}[$__rate_interval])) by (le))", + "hide": false, + "legendFormat": "P99", + "range": true, + "refId": "B" + } + ], + "title": "GC Duration", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "bars", + "fillOpacity": 100, + "gradientMode": "hue", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "percent" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "decbytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 9 + }, + "id": 29, + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.1.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "editorMode": "code", + "exemplar": false, + "expr": "avg(process_runtime_go_mem_heap_alloc_bytes{service=\"$service\", environment=\"$environment\"})", + "legendFormat": "alloc", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "editorMode": "code", + "exemplar": false, + "expr": "avg(process_runtime_go_mem_heap_inuse_bytes{service=\"$service\", environment=\"$environment\"})", + "hide": false, + "legendFormat": "in use", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "editorMode": "code", + "exemplar": false, + "expr": "avg(process_runtime_go_mem_heap_idle_bytes{service=\"$service\", environment=\"$environment\"})", + "hide": false, + "legendFormat": "idle", + "range": true, + "refId": "C" + } + ], + "title": "Heap Bytes", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-GrYlRd" + }, + "custom": { + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1 + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 9 + }, + "id": 41, + "options": { + "colWidth": 0.9, + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "rowHeight": 0.9, + "showValue": "never", + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.1.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(rate(go_panics_recovered_total{service=\"$service\", environment=\"$environment\"}[$__rate_interval]))", + "hide": false, + "legendFormat": "Rate", + "range": true, + "refId": "A" + } + ], + "title": "Recovered Panics", + "type": "status-history" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 17 + }, + "id": 8, + "panels": [], + "title": "Endpoints", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "green", + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "bars", + "fillOpacity": 100, + "gradientMode": "hue", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 18 + }, + "id": 17, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.1.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(rate(traces_spanmetrics_calls_total{service=\"$service\", environment=\"$environment\", span_kind=\"SPAN_KIND_INTERNAL\", span_name=~\"goa.endpoint/.+\", goa_service=~\"$endpoint_service\", goa_method=~\"$endpoint_method\"}[$__rate_interval])) by (span_name)", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Request Rates", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "red", + "mode": "shades" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "bars", + "fillOpacity": 100, + "gradientMode": "hue", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 18 + }, + "id": 16, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(traces_spanmetrics_calls_total{service=\"$service\", environment=\"$environment\", span_kind=\"SPAN_KIND_INTERNAL\", span_name=~\"goa.endpoint/.+\", goa_service=~\"$endpoint_service\", goa_method=~\"$endpoint_method\", status_code=\"STATUS_CODE_ERROR\"}[$__rate_interval])) by (span_name)", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Request Error Rates", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 26 + }, + "id": 20, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.1.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "editorMode": "code", + "exemplar": true, + "expr": "histogram_quantile(.95, sum(rate(traces_spanmetrics_latency_bucket{service=\"$service\", environment=\"$environment\", span_kind=\"SPAN_KIND_INTERNAL\", span_name=~\"goa.endpoint/.+\", goa_service=~\"$endpoint_service\", goa_method=~\"$endpoint_method\"}[$__rate_interval])) by (span_name, status_code, le))", + "legendFormat": "{{span_name}}: {{status_code}}", + "range": true, + "refId": "A" + } + ], + "title": "Request P95 Latency", + "type": "timeseries" + }, + { + "datasource": { + "type": "tempo", + "uid": "tempo" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 26 + }, + "id": 21, + "options": { + "edges": {}, + "nodes": {} + }, + "targets": [ + { + "datasource": { + "type": "tempo", + "uid": "tempo" + }, + "limit": 20, + "query": "", + "queryType": "serviceMap", + "refId": "A", + "serviceMapQuery": "{}", + "tableType": "traces" + } + ], + "title": "Service Mesh", + "type": "nodeGraph" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 34 + }, + "id": 4, + "panels": [], + "title": "HTTP", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "right", + "cellOptions": { + "type": "color-text" + }, + "filterable": true, + "inspect": false + }, + "decimals": 0, + "fieldMinMax": false, + "mappings": [], + "max": 10, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 500 + }, + { + "color": "orange", + "value": 3000 + }, + { + "color": "red", + "value": 5000 + } + ] + }, + "unit": "ms" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "RPS" + }, + "properties": [ + { + "id": "unit", + "value": "reqps" + }, + { + "id": "custom.cellOptions", + "value": { + "type": "auto", + "wrapText": false + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Requests" + }, + "properties": [ + { + "id": "unit", + "value": "short" + }, + { + "id": "custom.cellOptions", + "value": { + "type": "auto", + "wrapText": false + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Success %" + }, + "properties": [ + { + "id": "unit", + "value": "percent" + }, + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "orange", + "value": 10 + }, + { + "color": "#EAB839", + "value": 60 + }, + { + "color": "green", + "value": 90 + } + ] + } + }, + { + "id": "max", + "value": 100 + }, + { + "id": "custom.cellOptions", + "value": { + "applyToRow": false, + "mode": "gradient", + "type": "color-background", + "wrapText": false + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "HTTP Route" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "type": "auto", + "wrapText": false + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "HTTP Method" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "type": "auto" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 35 + }, + "id": 22, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "enablePagination": true, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "11.1.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum by (http_method, http_route) (rate(http_server_duration_milliseconds_count{service=\"$service\", environment=\"$environment\", http_method=~\"$http_method\", http_route=~\"$http_route\"}[$__range]))", + "format": "table", + "hide": false, + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "rps" + }, + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum by (http_method, http_route) (increase(http_server_duration_milliseconds_sum{service=\"$service\", environment=\"$environment\", http_method=~\"$http_method\", http_route=~\"$http_route\"}[$__range])) / sum by (http_method, http_route) (increase(http_server_duration_milliseconds_count{service=\"$service\", environment=\"$environment\", http_method=~\"$http_method\", http_route=~\"$http_route\"}[$__range]))", + "format": "table", + "hide": false, + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "avg-response-time-seconds" + }, + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "editorMode": "code", + "exemplar": false, + "expr": "histogram_quantile(.95, sum(rate(http_server_duration_milliseconds_bucket{service=\"$service\", environment=\"$environment\", http_method=~\"$http_method\", http_route=~\"$http_route\"}[$__range])) by (http_method, http_route, le))", + "format": "table", + "hide": false, + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "p95" + }, + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum by (http_method, http_route) (increase(http_server_duration_milliseconds_count{service=\"$service\", environment=\"$environment\", http_method=~\"$http_method\", http_route=~\"$http_route\"}[$__range]))", + "format": "table", + "hide": false, + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "requests" + }, + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum by (http_method, http_route) (increase(http_server_duration_milliseconds_count{service=\"$service\", environment=\"$environment\", http_method=~\"$http_method\", http_route=~\"$http_route\", http_status_code=~\"2.*\"}[$__range])) / sum by (http_method, http_route) (increase(http_server_duration_milliseconds_count{service=\"$service\", environment=\"$environment\", http_method=~\"$http_method\", http_route=~\"$http_route\"}[$__range])) * 100", + "format": "table", + "hide": false, + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "success-rate" + } + ], + "title": "Overview", + "transformations": [ + { + "id": "joinByField", + "options": { + "byField": "http_route", + "mode": "outer" + } + }, + { + "id": "organize", + "options": { + "excludeByName": { + "Time 1": true, + "Time 2": true, + "Time 3": true, + "Time 4": true, + "Time 5": true, + "http_method 2": true, + "http_method 3": true, + "http_method 4": true, + "http_method 5": true + }, + "includeByName": {}, + "indexByName": { + "Time 1": 2, + "Time 2": 4, + "Time 3": 7, + "Time 4": 10, + "Time 5": 13, + "Value #avg-response-time-seconds": 6, + "Value #p95": 9, + "Value #requests": 12, + "Value #rps": 3, + "Value #success-rate": 15, + "http_method 1": 0, + "http_method 2": 5, + "http_method 3": 8, + "http_method 4": 11, + "http_method 5": 14, + "http_route": 1 + }, + "renameByName": { + "Value #avg-response-time-seconds": "Average", + "Value #p95": "P95", + "Value #requests": "Requests", + "Value #rps": "RPS", + "Value #success-rate": "Success %", + "http_method 1": "HTTP Method", + "http_route": "HTTP Route" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "green", + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 43 + }, + "id": 24, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(http_server_duration_milliseconds_count{service=\"$service\", environment=\"$environment\", http_method=~\"$http_method\", http_route=~\"$http_route\"}[$__rate_interval])) by (http_method, http_route)", + "legendFormat": "{{http_method}} {{http_route}}", + "range": true, + "refId": "A" + } + ], + "title": "Request Rate", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "bars", + "fillOpacity": 100, + "gradientMode": "hue", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "percent" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "reqps" + }, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "A" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "green", + "mode": "shades" + } + } + ] + }, + { + "matcher": { + "id": "byFrameRefID", + "options": "B" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "yellow", + "mode": "shades" + } + } + ] + }, + { + "matcher": { + "id": "byFrameRefID", + "options": "C" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "shades" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 43 + }, + "id": 25, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(rate(http_server_duration_milliseconds_count{service=\"$service\", environment=\"$environment\", http_method=~\"$http_method\", http_route=~\"$http_route\", http_status_code=~\"2..\"}[$__rate_interval])) by (http_method, http_route)", + "hide": false, + "legendFormat": "{{http_method}} {{http_route}} - 2XX", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(rate(http_server_duration_milliseconds_count{service=\"$service\", environment=\"$environment\", http_method=~\"$http_method\", http_route=~\"$http_route\", http_status_code=~\"4..\"}[$__rate_interval])) by (http_method, http_route)", + "hide": false, + "legendFormat": "{{http_method}} {{http_route}} - 4XX", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(rate(http_server_duration_milliseconds_count{service=\"$service\", environment=\"$environment\", http_method=~\"$http_method\", http_route=~\"$http_route\", http_status_code=~\"5..\"}[$__rate_interval])) by (http_method, http_route)", + "hide": false, + "legendFormat": "{{http_method}} {{http_route}} - 5XX", + "range": true, + "refId": "C" + } + ], + "title": "Response Status", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic", + "seriesBy": "last" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "bars", + "fillOpacity": 100, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 51 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "editorMode": "code", + "exemplar": true, + "expr": "histogram_quantile(.95, sum(rate(http_server_duration_milliseconds_bucket{service=\"$service\", environment=\"$environment\", http_method=~\"$http_method\", http_route=~\"$http_route\"}[$__rate_interval])) by (http_method, http_route, le))", + "legendFormat": "{{http_method}} {{http_route}}", + "range": true, + "refId": "A" + } + ], + "title": "P95 Latency", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "description": "", + "fieldConfig": { + "defaults": { + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "scaleDistribution": { + "type": "linear" + } + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 51 + }, + "id": 39, + "maxDataPoints": 20, + "options": { + "calculate": false, + "cellGap": 1, + "color": { + "exponent": 0.5, + "fill": "green", + "mode": "scheme", + "reverse": true, + "scale": "exponential", + "scheme": "Blues", + "steps": 64 + }, + "exemplars": { + "color": "rgba(255,0,255,0.7)" + }, + "filterValues": { + "le": 1e-9 + }, + "legend": { + "show": false, + "showLegend": true + }, + "rowsFrame": { + "layout": "unknown" + }, + "tooltip": { + "mode": "single", + "showColorScale": false, + "yHistogram": false + }, + "yAxis": { + "axisPlacement": "left", + "reverse": false, + "unit": "ms" + } + }, + "pluginVersion": "11.1.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum by (le) (increase(http_server_duration_milliseconds_bucket{service=\"$service\", environment=\"$environment\", http_method=~\"$http_method\", http_route=~\"$http_route\"}[$__interval]))", + "format": "heatmap", + "legendFormat": "{{le}}", + "range": true, + "refId": "A" + } + ], + "title": "Response Times", + "type": "heatmap" + }, + { + "datasource": { + "type": "tempo", + "uid": "tempo" + }, + "description": "", + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 24, + "x": 0, + "y": 59 + }, + "id": 11, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "11.1.4", + "targets": [ + { + "datasource": { + "type": "tempo", + "uid": "tempo" + }, + "hide": false, + "key": "Q-f807f3aa-fde3-4f3d-96e1-ebff01a76cb9-0", + "limit": 20, + "query": "{ resource.service.name = \"$service\" && resource.environment = \"$environment\" && span.http.method=~\"$http_method\" && span.http.route=~\"$http_route\" && status = error } || { resource.service.name = \"$service\" && resource.environment = \"$environment\" && span.http.method=~\"$http_method\" && span.http.route=~\"$http_route\" } >> { status = error }", + "queryType": "traceql", + "refId": "A", + "tableType": "traces" + } + ], + "title": "Error Traces", + "type": "table" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 70 + }, + "id": 31, + "panels": [], + "title": "gRPC", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "right", + "cellOptions": { + "type": "color-text" + }, + "filterable": true, + "inspect": false + }, + "decimals": 0, + "fieldMinMax": false, + "mappings": [], + "max": 10, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 500 + }, + { + "color": "orange", + "value": 3000 + }, + { + "color": "red", + "value": 5000 + } + ] + }, + "unit": "ms" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "RPS" + }, + "properties": [ + { + "id": "unit", + "value": "reqps" + }, + { + "id": "custom.cellOptions", + "value": { + "type": "auto", + "wrapText": false + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Requests" + }, + "properties": [ + { + "id": "unit", + "value": "short" + }, + { + "id": "custom.cellOptions", + "value": { + "type": "auto", + "wrapText": false + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Success %" + }, + "properties": [ + { + "id": "unit", + "value": "percent" + }, + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "orange", + "value": 10 + }, + { + "color": "#EAB839", + "value": 60 + }, + { + "color": "green", + "value": 90 + } + ] + } + }, + { + "id": "max", + "value": 100 + }, + { + "id": "custom.cellOptions", + "value": { + "applyToRow": false, + "mode": "gradient", + "type": "color-background", + "wrapText": false + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "gRPC Method" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "type": "auto", + "wrapText": false + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "gRPC Service" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "type": "auto" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 71 + }, + "id": 36, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "enablePagination": true, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "11.1.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum by (rpc_service, rpc_method) (rate(rpc_server_duration_milliseconds_count{service=\"$service\", environment=\"$environment\", rpc_service=~\"$grpc_service\", rpc_method=~\"$grpc_method\"}[$__range]))", + "format": "table", + "hide": false, + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "rps" + }, + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum by (rpc_service, rpc_method) (increase(rpc_server_duration_milliseconds_sum{service=\"$service\", environment=\"$environment\", rpc_service=~\"$grpc_service\", rpc_method=~\"$grpc_method\"}[$__range])) / sum by (rpc_service, rpc_method) (increase(rpc_server_duration_milliseconds_count{service=\"$service\", environment=\"$environment\", rpc_service=~\"$grpc_service\", rpc_method=~\"$grpc_method\"}[$__range]))", + "format": "table", + "hide": false, + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "avg-response-time-seconds" + }, + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "editorMode": "code", + "exemplar": false, + "expr": "histogram_quantile(.95, sum(rate(rpc_server_duration_milliseconds_bucket{service=\"$service\", environment=\"$environment\", rpc_service=~\"$grpc_service\", rpc_method=~\"$grpc_method\"}[$__range])) by (rpc_service, rpc_method, le))", + "format": "table", + "hide": false, + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "p95" + }, + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum by (rpc_service, rpc_method) (increase(rpc_server_duration_milliseconds_count{service=\"$service\", environment=\"$environment\", rpc_service=~\"$grpc_service\", rpc_method=~\"$grpc_method\"}[$__range]))", + "format": "table", + "hide": false, + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "requests" + }, + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum by (rpc_service, rpc_method) (increase(rpc_server_duration_milliseconds_count{service=\"$service\", environment=\"$environment\", rpc_service=~\"$grpc_service\", rpc_method=~\"$grpc_method\", rpc_grpc_status_code=\"0\"}[$__range])) / sum by (rpc_service, rpc_method) (increase(rpc_server_duration_milliseconds_count{service=\"$service\", environment=\"$environment\", rpc_service=~\"$grpc_service\", rpc_method=~\"$grpc_method\"}[$__range])) * 100", + "format": "table", + "hide": false, + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "success-rate" + } + ], + "title": "Overview", + "transformations": [ + { + "id": "joinByField", + "options": { + "byField": "rpc_method", + "mode": "outer" + } + }, + { + "id": "organize", + "options": { + "excludeByName": { + "Time 1": true, + "Time 2": true, + "Time 3": true, + "Time 4": true, + "Time 5": true, + "rpc_service 2": true, + "rpc_service 3": true, + "rpc_service 4": true, + "rpc_service 5": true + }, + "includeByName": {}, + "indexByName": { + "Time 1": 2, + "Time 2": 4, + "Time 3": 7, + "Time 4": 10, + "Time 5": 13, + "Value #avg-response-time-seconds": 6, + "Value #p95": 9, + "Value #requests": 12, + "Value #rps": 3, + "Value #success-rate": 15, + "rpc_method": 1, + "rpc_service 1": 0, + "rpc_service 2": 5, + "rpc_service 3": 8, + "rpc_service 4": 11, + "rpc_service 5": 14 + }, + "renameByName": { + "Value #avg-response-time-seconds": "Average", + "Value #p95": "P95", + "Value #requests": "Requests", + "Value #rps": "RPS", + "Value #success-rate": "Success %", + "rpc_method": "gRPC Method", + "rpc_service 1": "gRPC Service" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "green", + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 79 + }, + "id": 32, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(rpc_server_duration_milliseconds_count{service=\"$service\", environment=\"$environment\", rpc_service=~\"$grpc_service\", rpc_method=~\"$grpc_method\"}[$__rate_interval])) by (rpc_service, rpc_method)", + "legendFormat": "{{rpc_service}} {{rpc_method}}", + "range": true, + "refId": "A" + } + ], + "title": "Request Rate", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "bars", + "fillOpacity": 100, + "gradientMode": "hue", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "percent" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "reqps" + }, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "A" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "green", + "mode": "shades" + } + } + ] + }, + { + "matcher": { + "id": "byFrameRefID", + "options": "B" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "shades" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 79 + }, + "id": 33, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(rate(rpc_server_duration_milliseconds_count{service=\"$service\", environment=\"$environment\", rpc_service=~\"$grpc_service\", rpc_method=~\"$grpc_method\", rpc_grpc_status_code=\"0\"}[$__rate_interval])) by (rpc_service, rpc_method)", + "hide": false, + "legendFormat": "{{rpc_service}} {{rpc_method}} - Ok", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(rate(rpc_server_duration_milliseconds_count{service=\"$service\", environment=\"$environment\", rpc_service=~\"$grpc_service\", rpc_method=~\"$grpc_method\", rpc_grpc_status_code!=\"0\"}[$__rate_interval])) by (rpc_service, rpc_method, rpc_grpc_status_code)", + "hide": false, + "legendFormat": "{{rpc_service}} {{rpc_method}} - {{rpc_grpc_status_code}}", + "range": true, + "refId": "B" + } + ], + "title": "Response Status", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic", + "seriesBy": "last" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "bars", + "fillOpacity": 100, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 87 + }, + "id": 34, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "editorMode": "code", + "exemplar": true, + "expr": "histogram_quantile(.95, sum(rate(rpc_server_duration_milliseconds_bucket{service=\"$service\", environment=\"$environment\", rpc_service=~\"$grpc_service\", rpc_method=~\"$grpc_method\"}[$__rate_interval])) by (rpc_service, rpc_method, le))", + "legendFormat": "{{rpc_service}} {{rpc_method}}", + "range": true, + "refId": "A" + } + ], + "title": "P95 Latency", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "description": "", + "fieldConfig": { + "defaults": { + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "scaleDistribution": { + "type": "linear" + } + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 87 + }, + "id": 52, + "maxDataPoints": 20, + "options": { + "calculate": false, + "cellGap": 1, + "color": { + "exponent": 0.5, + "fill": "green", + "mode": "scheme", + "reverse": true, + "scale": "exponential", + "scheme": "Blues", + "steps": 64 + }, + "exemplars": { + "color": "rgba(255,0,255,0.7)" + }, + "filterValues": { + "le": 1e-9 + }, + "legend": { + "show": false, + "showLegend": true + }, + "rowsFrame": { + "layout": "unknown" + }, + "tooltip": { + "mode": "single", + "showColorScale": false, + "yHistogram": false + }, + "yAxis": { + "axisPlacement": "left", + "reverse": false, + "unit": "ms" + } + }, + "pluginVersion": "11.1.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum by (le) (increase(rpc_server_duration_milliseconds_bucket{service=\"$service\", environment=\"$environment\", rpc_service=~\"$grpc_service\", rpc_method=~\"$grpc_method\"}[$__interval]))", + "format": "heatmap", + "legendFormat": "{{le}}", + "range": true, + "refId": "A" + } + ], + "title": "Response Times", + "type": "heatmap" + }, + { + "datasource": { + "type": "tempo", + "uid": "tempo" + }, + "description": "", + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 24, + "x": 0, + "y": 95 + }, + "id": 37, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "11.1.4", + "targets": [ + { + "datasource": { + "type": "tempo", + "uid": "tempo" + }, + "hide": false, + "key": "Q-f807f3aa-fde3-4f3d-96e1-ebff01a76cb9-0", + "limit": 20, + "query": "{ resource.service.name = \"$service\" && resource.environment = \"$environment\" && kind = server && span.rpc.service=~\"$grpc_service\" && span.rpc.method=~\"$grpc_method\" && status = error } || { resource.service.name = \"$service\" && resource.environment = \"$environment\" && kind = server && span.rpc.service=~\"$grpc_service\" && span.rpc.method=~\"$grpc_method\" } >> { status = error }", + "queryType": "traceql", + "refId": "A", + "tableType": "traces" + } + ], + "title": "Error Traces", + "type": "table" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 106 + }, + "id": 42, + "panels": [], + "title": "Worker", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 107 + }, + "id": 40, + "maxDataPoints": 20, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.1.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum by (job_kind, job_queue, job_priority) (worker_jobs_available_count{service=\"$service\", environment=\"$environment\", job_kind=~\"$job_kind\"})", + "format": "heatmap", + "legendFormat": "Available: {{job_kind}} - {{job_queue}}[{{job_priority}}]", + "range": true, + "refId": "A" + } + ], + "title": "Available Jobs", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "green", + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 107 + }, + "id": 44, + "maxDataPoints": 20, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.1.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum by (job_kind, job_queue, job_priority) (increase(worker_jobs_enqueued_total{service=\"$service\", environment=\"$environment\", job_kind=~\"$job_kind\"}[$__interval]))", + "format": "heatmap", + "legendFormat": "Enqueued: {{job_kind}} - {{job_queue}}[{{job_priority}}]", + "range": true, + "refId": "A" + } + ], + "title": "Jobs Enqueued", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "bars", + "fillOpacity": 100, + "gradientMode": "hue", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "percent" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "B" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "green", + "mode": "shades" + } + } + ] + }, + { + "matcher": { + "id": "byFrameRefID", + "options": "C" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "shades" + } + } + ] + }, + { + "matcher": { + "id": "byFrameRefID", + "options": "D" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "orange", + "mode": "shades" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 115 + }, + "id": 45, + "maxDataPoints": 20, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.1.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum by (job_kind, job_queue, job_priority) (increase(worker_jobs_completed_total{service=\"$service\", environment=\"$environment\", job_kind=~\"$job_kind\"}[$__interval]))", + "format": "heatmap", + "hide": false, + "instant": false, + "legendFormat": "Completed: {{job_kind}} - {{job_queue}}[{{job_priority}}]", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum by (job_kind, job_queue, job_priority) (increase(worker_jobs_discarded_total{service=\"$service\", environment=\"$environment\", job_kind=~\"$job_kind\"}[$__interval]))", + "format": "heatmap", + "hide": false, + "legendFormat": "Discarded: {{job_kind}} - {{job_queue}}[{{job_priority}}]", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum by (job_kind, job_queue, job_priority) (increase(worker_jobs_cancelled_total{service=\"$service\", environment=\"$environment\", job_kind=~\"$job_kind\"}[$__interval]))", + "format": "heatmap", + "hide": false, + "legendFormat": "Cancelled: {{job_kind}} - {{job_queue}}[{{job_priority}}]", + "range": true, + "refId": "D" + } + ], + "title": "Jobs Completed", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "red", + "mode": "shades" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 115 + }, + "id": 46, + "maxDataPoints": 20, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.1.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum by (job_kind, job_queue, job_priority) (increase(worker_jobs_failed_total{service=\"$service\", environment=\"$environment\", job_kind=~\"$job_kind\"}[$__interval]))", + "format": "heatmap", + "legendFormat": "Failed: {{job_kind}} - {{job_queue}}[{{job_priority}}]", + "range": true, + "refId": "A" + } + ], + "title": "Jobs Failed", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "green", + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "bars", + "fillOpacity": 100, + "gradientMode": "hue", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 123 + }, + "id": 54, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.1.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(rate(traces_spanmetrics_calls_total{service=\"$service\", environment=\"$environment\", span_kind=\"SPAN_KIND_INTERNAL\", span_name=~\"river.worker/.*\", job_kind=~\"$job_kind\"}[$__rate_interval])) by (span_name)", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Job Rates", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "red", + "mode": "shades" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "bars", + "fillOpacity": 100, + "gradientMode": "hue", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "reqps" + }, + "overrides": [ + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "river.worker/echo" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": false, + "viz": true + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 123 + }, + "id": 55, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(traces_spanmetrics_calls_total{service=\"$service\", environment=\"$environment\", span_kind=\"SPAN_KIND_INTERNAL\", span_name=~\"river.worker/.*\", job_kind=~\"$job_kind\"}[$__rate_interval])) by (span_name)", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Error Rates", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 131 + }, + "id": 53, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.1.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "editorMode": "code", + "exemplar": true, + "expr": "histogram_quantile(.95, sum(rate(traces_spanmetrics_latency_bucket{service=\"$service\", environment=\"$environment\", span_kind=\"SPAN_KIND_INTERNAL\", span_name=~\"river.worker/.*\", job_kind=~\"$job_kind\"}[$__rate_interval])) by (span_name, status_code, le))", + "legendFormat": "{{span_name}}: {{status_code}}", + "range": true, + "refId": "A" + } + ], + "title": "P95 Job Latency", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "description": "", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "orange", + "value": 70 + }, + { + "color": "red", + "value": 85 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 131 + }, + "id": 50, + "maxDataPoints": 20, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "mean" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "11.1.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "mimir" + }, + "editorMode": "code", + "exemplar": false, + "expr": "histogram_quantile(0.95, sum by (le) (increase(worker_jobs_queue_wait_milliseconds_bucket{service=\"$service\", environment=\"$environment\", job_kind=~\"$job_kind\"}[$__interval])))", + "format": "heatmap", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "P95 Queue Latency", + "type": "gauge" + }, + { + "datasource": { + "type": "tempo", + "uid": "tempo" + }, + "description": "", + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 24, + "x": 0, + "y": 139 + }, + "id": 51, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "11.1.4", + "targets": [ + { + "datasource": { + "type": "tempo", + "uid": "tempo" + }, + "hide": false, + "key": "Q-f807f3aa-fde3-4f3d-96e1-ebff01a76cb9-0", + "limit": 20, + "query": "{ resource.service.name = \"$service\" && resource.environment = \"$environment\" && span.job.kind =~ \"$job_kind\" && status = error } || { resource.service.name = \"$service\" && resource.environment = \"$environment\" && span.job.kind =~ \"$job_kind\" } >> { status = error }", + "queryType": "traceql", + "refId": "A", + "tableType": "traces" + } + ], + "title": "Error Traces", + "type": "table" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 150 + }, + "id": 6, + "panels": [], + "title": "Logs", + "type": "row" + }, + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 151 + }, + "id": 9, + "maxDataPoints": 100, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "sum" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.1.4", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "editorMode": "code", + "expr": "sum(count_over_time(({service_name=\"$service\", environment=\"$environment\"})[$__auto]))", + "queryType": "range", + "refId": "A" + } + ], + "title": "Log Count", + "type": "stat" + }, + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "fieldConfig": { + "defaults": { + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "scaleDistribution": { + "type": "linear" + } + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 18, + "x": 6, + "y": 151 + }, + "id": 12, + "interval": "1m", + "maxDataPoints": "", + "options": { + "calculate": false, + "cellGap": 1, + "color": { + "exponent": 0.5, + "fill": "dark-orange", + "mode": "scheme", + "reverse": true, + "scale": "exponential", + "scheme": "Oranges", + "steps": 64 + }, + "exemplars": { + "color": "rgba(255,0,255,0.7)" + }, + "filterValues": { + "le": 1e-9 + }, + "legend": { + "show": true + }, + "rowsFrame": { + "layout": "auto" + }, + "tooltip": { + "mode": "single", + "showColorScale": false, + "yHistogram": false + }, + "yAxis": { + "axisPlacement": "right", + "reverse": false + } + }, + "pluginVersion": "11.1.4", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "editorMode": "code", + "expr": "topk(5, sum(count_over_time(({service_name=\"$service\", environment=\"$environment\"} | json | __error__=\"\")[$__auto])) by (file))", + "legendFormat": "{{file}}", + "queryType": "range", + "refId": "A" + } + ], + "title": "Log Volume by File", + "type": "heatmap" + }, + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 155 + }, + "id": 10, + "maxDataPoints": 100, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "sum" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.1.4", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "editorMode": "code", + "expr": "sum(count_over_time(({service_name=\"$service\", environment=\"$environment\"} | json | detected_level = \"error\")[$__auto]))", + "queryType": "range", + "refId": "A" + } + ], + "title": "Error Log Count", + "type": "stat" + }, + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 1, + "drawStyle": "bars", + "fillOpacity": 100, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "stepBefore", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "info" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "green", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "error" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 159 + }, + "id": 1, + "interval": "1m", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "zXvfRSSVz" + }, + "editorMode": "code", + "expr": "sum(count_over_time(({service_name=\"$service\", environment=\"$environment\"} | json | __error__=\"\")[$__auto])) by (detected_level)", + "legendFormat": "{{level}}", + "queryType": "range", + "range": true, + "refId": "A" + } + ], + "title": "Log Volume", + "type": "timeseries" + }, + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [] + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "info" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "green", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "error" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 159 + }, + "id": 14, + "interval": "1m", + "maxDataPoints": "", + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "6.4.3", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "editorMode": "code", + "expr": "sum(count_over_time(({service_name=\"$service\", environment=\"$environment\"} | json | __error__=\"\")[$__range])) by (detected_level)", + "legendFormat": "{{detected_level}}", + "queryType": "range", + "refId": "A" + } + ], + "title": "Logs by Level", + "type": "piechart" + }, + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [] + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "stdout" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 159 + }, + "id": 13, + "maxDataPoints": 100, + "options": { + "legend": { + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "values": [ + "value" + ] + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "7.0.4", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "editorMode": "code", + "expr": "sum(count_over_time(({service_name=\"$service\", environment=\"$environment\"})[$__range])) by (logstream)", + "legendFormat": "{{logstream}}", + "queryType": "range", + "refId": "A" + } + ], + "title": "Logs by Stream", + "type": "piechart" + }, + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "description": "", + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 167 + }, + "id": 5, + "options": { + "dedupStrategy": "none", + "enableLogDetails": true, + "prettifyLogMessage": false, + "showCommonLabels": false, + "showLabels": false, + "showTime": true, + "sortOrder": "Descending", + "wrapLogMessage": false + }, + "pluginVersion": "11.1.4", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "editorMode": "code", + "expr": "{service_name=\"$service\", environment=\"$environment\"} | json | line_format `{{__line__}}` | detected_level=~\"$log_level\" |~ \"(?i).*($log_search).*\"", + "queryType": "range", + "refId": "A" + } + ], + "title": "Logs (Searchable)", + "type": "logs" + } + ], + "refresh": "30s", + "schemaVersion": 39, + "tags": [], + "templating": { + "list": [ + { + "current": { + "selected": true, + "text": [ + "countup" + ], + "value": [ + "countup" + ] + }, + "datasource": { + "type": "tempo", + "uid": "tempo" + }, + "definition": "", + "hide": 0, + "includeAll": false, + "label": "Service", + "multi": false, + "name": "service", + "options": [], + "query": { + "label": "service.name", + "refId": "TempoDatasourceVariableQueryEditor-VariableQuery", + "type": 1 + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "current": { + "selected": true, + "text": [ + "local" + ], + "value": [ + "local" + ] + }, + "datasource": { + "type": "tempo", + "uid": "tempo" + }, + "definition": "", + "hide": 0, + "includeAll": false, + "label": "Environment", + "multi": false, + "name": "environment", + "options": [], + "query": { + "label": "environment", + "refId": "TempoDatasourceVariableQueryEditor-VariableQuery", + "type": 1 + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "allValue": ".*", + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "tempo", + "uid": "tempo" + }, + "definition": "", + "hide": 0, + "includeAll": true, + "label": "Endpoint Service", + "multi": true, + "name": "endpoint_service", + "options": [], + "query": { + "label": "endpoint.service", + "refId": "TempoDatasourceVariableQueryEditor-VariableQuery", + "type": 1 + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "allValue": ".*", + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "tempo", + "uid": "tempo" + }, + "definition": "", + "hide": 0, + "includeAll": true, + "label": "Endpoint Method", + "multi": true, + "name": "endpoint_method", + "options": [], + "query": { + "label": "endpoint.method", + "refId": "TempoDatasourceVariableQueryEditor-VariableQuery", + "type": 1 + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "allValue": ".*", + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "tempo", + "uid": "tempo" + }, + "definition": "", + "hide": 0, + "includeAll": true, + "label": "HTTP Method", + "multi": true, + "name": "http_method", + "options": [], + "query": { + "label": "http.method", + "refId": "TempoDatasourceVariableQueryEditor-VariableQuery", + "type": 1 + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "allValue": ".*", + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "tempo", + "uid": "tempo" + }, + "definition": "", + "hide": 0, + "includeAll": true, + "label": "HTTP Route", + "multi": true, + "name": "http_route", + "options": [], + "query": { + "label": "http.route", + "refId": "TempoDatasourceVariableQueryEditor-VariableQuery", + "type": 1 + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "allValue": ".*", + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "tempo", + "uid": "tempo" + }, + "definition": "", + "hide": 0, + "includeAll": true, + "label": "gRPC Service", + "multi": true, + "name": "grpc_service", + "options": [], + "query": { + "label": "rpc.service", + "refId": "TempoDatasourceVariableQueryEditor-VariableQuery", + "type": 1 + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "allValue": ".*", + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "tempo", + "uid": "tempo" + }, + "definition": "", + "hide": 0, + "includeAll": true, + "label": "gRPC Method", + "multi": true, + "name": "grpc_method", + "options": [], + "query": { + "label": "rpc.method", + "refId": "TempoDatasourceVariableQueryEditor-VariableQuery", + "type": 1 + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "allValue": ".*", + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "tempo", + "uid": "tempo" + }, + "definition": "", + "hide": 0, + "includeAll": true, + "label": "Job Kind", + "multi": true, + "name": "job_kind", + "options": [], + "query": { + "label": "job.kind", + "refId": "TempoDatasourceVariableQueryEditor-VariableQuery", + "type": 1 + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "allValue": ".*", + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "loki", + "uid": "loki" + }, + "definition": "", + "hide": 0, + "includeAll": true, + "label": "Log Level", + "multi": true, + "name": "log_level", + "options": [], + "query": { + "label": "level", + "refId": "LokiVariableQueryEditor-VariableQuery", + "stream": "{service_name=\"$service\", environment=\"$environment\"}", + "type": 1 + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "current": { + "selected": true, + "text": "", + "value": "" + }, + "hide": 0, + "label": "Log Search", + "name": "log_search", + "options": [ + { + "selected": true, + "text": "", + "value": "" + } + ], + "query": "", + "skipUrlSync": false, + "type": "textbox" + } + ] + }, + "time": { + "from": "now-15m", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Service O11Y", + "version": 0, + "weekStart": "" +} \ No newline at end of file diff --git a/infra/environments/local/compose/grafana/provisioning/dashboards/dashboards.yaml b/infra/environments/local/compose/grafana/provisioning/dashboards/dashboards.yaml new file mode 100644 index 0000000..b39257b --- /dev/null +++ b/infra/environments/local/compose/grafana/provisioning/dashboards/dashboards.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: 1 + +providers: +- name: 'Grafana' + orgId: 1 + type: file + disableDeletion: false + updateIntervalSeconds: 10 + allowUiUpdates: true + options: + path: /var/lib/grafana/dashboards diff --git a/infra/environments/local/compose/grafana/provisioning/datasources/datasources.yaml b/infra/environments/local/compose/grafana/provisioning/datasources/datasources.yaml new file mode 100644 index 0000000..bfd8bd6 --- /dev/null +++ b/infra/environments/local/compose/grafana/provisioning/datasources/datasources.yaml @@ -0,0 +1,70 @@ +--- +apiVersion: 1 + +prune: true + +datasources: +- name: Mimir + type: prometheus + access: proxy + uid: mimir + url: http://mimir:3300/prometheus + jsonData: + httpMethod: POST + exemplarTraceIdDestinations: + - datasourceUid: tempo + name: trace_id + +- name: Tempo + type: tempo + access: proxy + uid: tempo + url: http://tempo:3200 + jsonData: + tracesToMetrics: + datasourceUid: mimir + spanStartTimeShift: '-5m' + spanEndTimeShift: '5m' + tags: + - {key: 'service.name', value: 'service'} + - {key: 'service.version', value: 'service_version'} + - {key: 'tier'} + - {key: 'environment'} + - {key: 'http.method', value: 'http_method'} + - {key: 'http.target', value: 'http_target'} + - {key: 'http.status_code', value: 'http_status_code'} + - {key: 'rpc.service', value: 'rpc_service'} + - {key: 'rpc.method', value: 'rpc_method'} + - {key: 'rpc.grpc.status_code', value: 'rpc_grpc_status_code'} + - {key: 'endpoint.service', value: 'endpoint_service'} + - {key: 'endpoint.method', value: endpointa_method'} + - {key: 'job.worker', value: 'job_worker'} + - {key: 'job.kind', value: 'job_kind'} + - {key: 'job.queue', value: 'job_queue'} + - {key: 'job.priority', value: 'job_priority'} + queries: + - name: 'Spanmetrics Latency' + query: 'sum(rate(traces_spanmetrics_latency_bucket{$$__tags}[1m]))' + tracesToLogsV2: + datasourceUid: loki + spanStartTimeShift: '-5m' + spanEndTimeShift: '5m' + customQuery: true + query: '{service_name=`$${__span.tags["service.name"]}`} | json | trace_id = `$${__span.traceId}` | span_id = `$${__span.spanId}`' + nodeGraph: + enabled: true + serviceMap: + datasourceUid: mimir + +- name: Loki + type: loki + access: proxy + uid: loki + url: http://loki:3100 + jsonData: + derivedFields: + - datasourceUid: tempo + matcherRegex: '"trace_id":"(\w+)"' + name: trace_id + url: '$${__value.raw}' + urlDisplayLabel: 'View Trace' diff --git a/infra/environments/local/compose/grafana/provisioning/plugins/loki-explorer-app.yaml b/infra/environments/local/compose/grafana/provisioning/plugins/loki-explorer-app.yaml new file mode 100644 index 0000000..b531d8f --- /dev/null +++ b/infra/environments/local/compose/grafana/provisioning/plugins/loki-explorer-app.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: 1 + +apps: +- type: grafana-lokiexplore-app + orgId: 1 + disabled: false diff --git a/infra/environments/local/compose/loki/config.yaml b/infra/environments/local/compose/loki/config.yaml new file mode 100644 index 0000000..e6e9348 --- /dev/null +++ b/infra/environments/local/compose/loki/config.yaml @@ -0,0 +1,33 @@ +--- +auth_enabled: false + +server: + http_listen_port: 3100 + log_level: warn + log_format: json + +common: + instance_addr: 127.0.0.1 + replication_factor: 1 + path_prefix: /tmp/loki + ring: + kvstore: + store: memberlist + +pattern_ingester: + enabled: true + +storage_config: + filesystem: + directory: /tmp/loki/chunks + +schema_config: + configs: + - from: 2024-08-28 + store: tsdb + object_store: filesystem + schema: v13 + index: + prefix: index_ + period: 24h + diff --git a/infra/environments/local/compose/mimir/config.yaml b/infra/environments/local/compose/mimir/config.yaml new file mode 100644 index 0000000..ad9d9b5 --- /dev/null +++ b/infra/environments/local/compose/mimir/config.yaml @@ -0,0 +1,41 @@ +--- +multitenancy_enabled: false + +server: + http_listen_port: 3300 + log_level: warn + log_format: json + +blocks_storage: + backend: filesystem + filesystem: + dir: /tmp/mimir/tsdb-data + bucket_store: + sync_dir: /tmp/mimir/tsdb-sync + tsdb: + dir: /tmp/mimir/tsdb + +distributor: + ring: + kvstore: + store: memberlist + +ingester: + ring: + replication_factor: 1 + kvstore: + store: memberlist + +compactor: + data_dir: /tmp/mimir/data-compactor + sharding_ring: + kvstore: + store: memberlist + +store_gateway: + sharding_ring: + replication_factor: 1 + +limits: + native_histograms_ingestion_enabled: true + max_global_exemplars_per_user: 100000 diff --git a/infra/environments/local/compose/otel-collector/config.yaml b/infra/environments/local/compose/otel-collector/config.yaml new file mode 100644 index 0000000..ba189ed --- /dev/null +++ b/infra/environments/local/compose/otel-collector/config.yaml @@ -0,0 +1,211 @@ +--- +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + + prometheus: + config: + scrape_configs: + - job_name: postgres + scrape_interval: 30s + docker_sd_configs: + - host: unix:///var/run/docker.sock + filters: + - name: network + values: [countup_default] + - name: label + values: [service=postgres] + relabel_configs: + - action: 'keep' + source_labels: ['__meta_docker_port_private'] + regex: 9187 + - source_labels: ['__meta_docker_container_name'] + regex: '/(.*)' + target_label: 'container' + - source_labels: ['__meta_docker_container_label_service'] + target_label: 'service' + - action: labelmap + regex: '__meta_docker_container_label_(component|tier|environment)' + + - job_name: grafana + scrape_interval: 30s + docker_sd_configs: + - host: unix:///var/run/docker.sock + filters: + - name: network + values: [countup_default] + - name: label + values: [service=grafana] + relabel_configs: + - action: 'keep' + source_labels: ['__meta_docker_port_private'] + regex: 3000 + - source_labels: ['__meta_docker_container_name'] + regex: '/(.*)' + target_label: 'container' + - source_labels: ['__meta_docker_container_label_service'] + target_label: 'service' + - action: labelmap + regex: '__meta_docker_container_label_(component|tier|environment)' + + - job_name: otel-collector + scrape_interval: 30s + docker_sd_configs: + - host: unix:///var/run/docker.sock + filters: + - name: network + values: [countup_default] + - name: label + values: [service=otel-collector] + relabel_configs: + - action: 'keep' + source_labels: ['__meta_docker_port_private'] + regex: 8888 + - source_labels: ['__meta_docker_container_name'] + regex: '/(.*)' + target_label: 'container' + - source_labels: ['__meta_docker_container_label_service'] + target_label: 'service' + - action: labelmap + regex: '__meta_docker_container_label_(component|tier|environment)' + + - job_name: promtail + scrape_interval: 30s + docker_sd_configs: + - host: unix:///var/run/docker.sock + filters: + - name: network + values: [countup_default] + - name: label + values: [service=promtail] + relabel_configs: + - action: 'keep' + source_labels: ['__meta_docker_port_private'] + regex: 3080 + - source_labels: ['__meta_docker_container_name'] + regex: '/(.*)' + target_label: 'container' + - source_labels: ['__meta_docker_container_label_service'] + target_label: 'service' + - action: labelmap + regex: '__meta_docker_container_label_(component|tier|environment)' + + - job_name: loki + scrape_interval: 30s + docker_sd_configs: + - host: unix:///var/run/docker.sock + filters: + - name: network + values: [countup_default] + - name: label + values: [service=loki] + relabel_configs: + - action: 'keep' + source_labels: ['__meta_docker_port_private'] + regex: 3100 + - source_labels: ['__meta_docker_container_name'] + regex: '/(.*)' + target_label: 'container' + - source_labels: ['__meta_docker_container_label_service'] + target_label: 'service' + - action: labelmap + regex: '__meta_docker_container_label_(component|tier|environment)' + + - job_name: tempo + scrape_interval: 30s + docker_sd_configs: + - host: unix:///var/run/docker.sock + filters: + - name: network + values: [countup_default] + - name: label + values: [service=tempo] + relabel_configs: + - action: 'keep' + source_labels: ['__meta_docker_port_private'] + regex: 3200 + - source_labels: ['__meta_docker_container_name'] + regex: '/(.*)' + target_label: 'container' + - source_labels: ['__meta_docker_container_label_service'] + target_label: 'service' + - action: labelmap + regex: '__meta_docker_container_label_(component|tier|environment)' + + - job_name: mimir + scrape_interval: 30s + docker_sd_configs: + - host: unix:///var/run/docker.sock + filters: + - name: network + values: [countup_default] + - name: label + values: [service=mimir] + relabel_configs: + - action: 'keep' + source_labels: ['__meta_docker_port_private'] + regex: 3300 + - source_labels: ['__meta_docker_container_name'] + regex: '/(.*)' + target_label: 'container' + - source_labels: ['__meta_docker_container_label_service'] + target_label: 'service' + - action: labelmap + regex: '__meta_docker_container_label_(component|tier|environment)' + +processors: + batch: + + tail_sampling: + decision_wait: 30s + policies: + - name: sample-error-traces + type: status_code + status_code: {status_codes: [ERROR]} + - name: sample-long-traces + type: latency + latency: {threshold_ms: 200} + + transform/otlp: + error_mode: ignore + metric_statements: + - context: datapoint + statements: + - set(attributes["service"], resource.attributes["service.name"]) + - set(attributes["service_version"], resource.attributes["service.version"]) + - set(attributes["tier"], resource.attributes["tier"]) + - set(attributes["environment"], resource.attributes["environment"]) + +exporters: + otlp/tempo: + endpoint: tempo:4317 + tls: + insecure: true + + prometheusremotewrite/mimir: + endpoint: http://mimir:3300/api/v1/push + tls: + insecure: true + +service: + telemetry: + logs: + encoding: json + + pipelines: + traces: + receivers: [otlp] + processors: [batch] + exporters: [otlp/tempo] + + metrics/otlp: + receivers: [otlp] + processors: [batch, transform/otlp] + exporters: [prometheusremotewrite/mimir] + + metrics/prometheus: + receivers: [prometheus] + processors: [batch] + exporters: [prometheusremotewrite/mimir] diff --git a/infra/environments/local/compose/promtail/config.yaml b/infra/environments/local/compose/promtail/config.yaml new file mode 100644 index 0000000..1e2231e --- /dev/null +++ b/infra/environments/local/compose/promtail/config.yaml @@ -0,0 +1,151 @@ +--- +server: + http_listen_port: 3080 + log_level: warn + +clients: +- url: http://loki:3100/loki/api/v1/push + +positions: + filename: /tmp/positions.yaml + +scrape_configs: +- job_name: countup + docker_sd_configs: + - host: unix:///var/run/docker.sock + filters: + - name: network + values: [countup_default] + - name: label + values: [service=countup] + relabel_configs: + - source_labels: ['__meta_docker_container_name'] + regex: '/(.*)' + target_label: 'container' + - source_labels: ['__meta_docker_container_log_stream'] + target_label: 'logstream' + - source_labels: ['__meta_docker_container_label_service'] + target_label: 'service_name' + - action: labelmap + regex: '__meta_docker_container_label_(component|tier|environment)' + pipeline_stages: + - docker: {} + - json: + expressions: + level: + - labels: + level: + +- job_name: grafana + docker_sd_configs: + - host: unix:///var/run/docker.sock + filters: + - name: network + values: [countup_default] + - name: label + values: [service=grafana] + relabel_configs: + - source_labels: ['__meta_docker_container_name'] + regex: '/(.*)' + target_label: 'container' + - source_labels: ['__meta_docker_container_log_stream'] + target_label: 'logstream' + - source_labels: ['__meta_docker_container_label_service'] + target_label: 'service_name' + - action: labelmap + regex: '__meta_docker_container_label_(component|tier|environment)' + +- job_name: otel-collector + docker_sd_configs: + - host: unix:///var/run/docker.sock + filters: + - name: network + values: [countup_default] + - name: label + values: [service=otel-collector] + relabel_configs: + - source_labels: ['__meta_docker_container_name'] + regex: '/(.*)' + target_label: 'container' + - source_labels: ['__meta_docker_container_log_stream'] + target_label: 'logstream' + - source_labels: ['__meta_docker_container_label_service'] + target_label: 'service_name' + - action: labelmap + regex: '__meta_docker_container_label_(component|tier|environment)' + +- job_name: promtail + docker_sd_configs: + - host: unix:///var/run/docker.sock + filters: + - name: network + values: [countup_default] + - name: label + values: [service=promtail] + relabel_configs: + - source_labels: ['__meta_docker_container_name'] + regex: '/(.*)' + target_label: 'container' + - source_labels: ['__meta_docker_container_log_stream'] + target_label: 'logstream' + - source_labels: ['__meta_docker_container_label_service'] + target_label: 'service_name' + - action: labelmap + regex: '__meta_docker_container_label_(component|tier|environment)' + +- job_name: loki + docker_sd_configs: + - host: unix:///var/run/docker.sock + filters: + - name: network + values: [countup_default] + - name: label + values: [service=loki] + relabel_configs: + - source_labels: ['__meta_docker_container_name'] + regex: '/(.*)' + target_label: 'container' + - source_labels: ['__meta_docker_container_log_stream'] + target_label: 'logstream' + - source_labels: ['__meta_docker_container_label_service'] + target_label: 'service_name' + - action: labelmap + regex: '__meta_docker_container_label_(component|tier|environment)' + +- job_name: tempo + docker_sd_configs: + - host: unix:///var/run/docker.sock + filters: + - name: network + values: [countup_default] + - name: label + values: [service=tempo] + relabel_configs: + - source_labels: ['__meta_docker_container_name'] + regex: '/(.*)' + target_label: 'container' + - source_labels: ['__meta_docker_container_log_stream'] + target_label: 'logstream' + - source_labels: ['__meta_docker_container_label_service'] + target_label: 'service_name' + - action: labelmap + regex: '__meta_docker_container_label_(component|tier|environment)' + +- job_name: mimir + docker_sd_configs: + - host: unix:///var/run/docker.sock + filters: + - name: network + values: [countup_default] + - name: label + values: [service=mimir] + relabel_configs: + - source_labels: ['__meta_docker_container_name'] + regex: '/(.*)' + target_label: 'container' + - source_labels: ['__meta_docker_container_log_stream'] + target_label: 'logstream' + - source_labels: ['__meta_docker_container_label_service'] + target_label: 'service_name' + - action: labelmap + regex: '__meta_docker_container_label_(component|tier|environment)' diff --git a/infra/environments/local/compose/tempo/config.yaml b/infra/environments/local/compose/tempo/config.yaml new file mode 100644 index 0000000..086f9ad --- /dev/null +++ b/infra/environments/local/compose/tempo/config.yaml @@ -0,0 +1,88 @@ +--- +stream_over_http_enabled: true + +server: + http_listen_port: 3200 + log_level: warn + log_format: json + +storage: + trace: + backend: local + local: + path: /tmp/tempo/blocks + wal: + path: /tmp/tempo/wal + pool: + max_workers: 50 + queue_depth: 2000 + +distributor: + receivers: + otlp: + protocols: + grpc: + +ingester: + lifecycler: + ring: + replication_factor: 1 + +compactor: + ring: + kvstore: + store: memberlist + compaction: + block_retention: 168h + +metrics_generator: + ring: + kvstore: + store: memberlist + processor: + span_metrics: + dimensions: + - service.version + - tier + - environment + - endpoint.service + - endpoint.method + - http.method + - http.target + - http.status_code + - rpc.service + - rpc.method + - rpc.grpc.status_code + - job.worker + - job.kind + - job.queue + - job.priority + service_graphs: + dimensions: + - service.version + - tier + - environment + - endpoint.service + - endpoint.method + - http.method + - http.target + - http.status_code + - rpc.service + - rpc.method + - rpc.grpc.status_code + registry: + external_labels: + source: tempo + storage: + path: /tmp/tempo/metrics-generator/wal + remote_write: + - url: http://mimir:3300/api/v1/push + send_exemplars: true + traces_storage: + path: /tmp/tempo/metrics-generator/traces + +overrides: + defaults: + metrics_generator: + processors: [service-graphs, span-metrics] + trace_id_label_name: trace_id diff --git a/infra/environments/local/compose/traefik/certs/traefik.cert b/infra/environments/local/compose/traefik/certs/traefik.cert new file mode 100644 index 0000000..9886888 --- /dev/null +++ b/infra/environments/local/compose/traefik/certs/traefik.cert @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIESzCCArOgAwIBAgIQCRZq7fEzCW6A3119L7+ZtzANBgkqhkiG9w0BAQsFADB7 +MR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExKDAmBgNVBAsMH2phY2VA +SmFjZXMtTUJQLkR1ZmZlbC5TZWMubG9jYWwxLzAtBgNVBAMMJm1rY2VydCBqYWNl +QEphY2VzLU1CUC5EdWZmZWwuU2VjLmxvY2FsMB4XDTI0MTExMTAzNDQzMloXDTI3 +MDIxMTAzNDQzMlowUDEnMCUGA1UEChMebWtjZXJ0IGRldmVsb3BtZW50IGNlcnRp +ZmljYXRlMSUwIwYDVQQLDBxqYWNlQEphY2VzLU1hY0Jvb2stUHJvLmxvY2FsMIIB +IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwSzuNcfz7R9rwd4B/DlYKSre +p+uakiRv2u+p9VgPhn8ol9VgcEYUe6QVTPibE6IWlyQRIL+RyxFoi7ZbwG1qTyv4 +uZAc/q13FVNJMj8Pp//G81+v8w+YI6yYujts2N/to/lBUzIUZ+RwO/C+5lImtRFh +pkVb1+VsaJM1FucI4Bh8XAC0slp2U62Jk1RL1uI/WD6dUIskzEnZ4q6SNqGvgR5E +grOBTReZzz10DOoRqpDSkCsHhZOev/IgAtO/0A/CiCMvDwW2/aGcqgobTZ/xHl0K +EVR8eotu5w5n+odDTez2/ThjuLeZmpPF210a5+jb2BAJZwFLzRHR602/MsFMkwID +AQABo3YwdDAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwHwYD +VR0jBBgwFoAUafP5j+CKJWlNYKCok064G2j0R1MwLAYDVR0RBCUwI4IJbG9jYWxo +b3N0hwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMA0GCSqGSIb3DQEBCwUAA4IBgQB4 +s4WbOCsD0MHLWyG46aKdQqL36yemrG8B8YN7l2OBgQSSUAwfBPFGqH2BxH+Vj6U1 +gOZlDnZiI5IKUDhSKag8MnABjzVDESEdljMf4DZLhgH8GKEEQ7uDXMQnXHsNf7Su +HdSmyStZGwn8UlXkGNf8QquGxz+HFOnCqcl0HUx/sp/3LFvlQ8VrYoMMJTfu+W9G +UTVIWIYJVzXUwu4B5UjoKKYAml+kWQBPT5GcXDlTPYDnYI6aqVXw80tufIw+iptr +FqMF9bULZ9MFiguPF7c50oMzVIih1oMLDONe5Y8z4anEonKcLwCBACAc+fSuNRI9 +b+2esC9F4h5oZF5OYpzAMtSKiyvv8m4S/T8twMnc7RtgHJmBEIHYjotjLSVEmCut +55HkuWTFH2M2XBByIYXwXKZ1A5nUYcCf71p20deIDdNXrWfVdl79Q40a4L8gVJAv +ISMgBig7ml5j82BaOcammJGSg+cOtcLnCkzIqQ+XNMysLyGGzXwmZVDVAaDy+1Y= +-----END CERTIFICATE----- diff --git a/infra/environments/local/compose/traefik/certs/traefik.key b/infra/environments/local/compose/traefik/certs/traefik.key new file mode 100644 index 0000000..d95414f --- /dev/null +++ b/infra/environments/local/compose/traefik/certs/traefik.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDBLO41x/PtH2vB +3gH8OVgpKt6n65qSJG/a76n1WA+GfyiX1WBwRhR7pBVM+JsTohaXJBEgv5HLEWiL +tlvAbWpPK/i5kBz+rXcVU0kyPw+n/8bzX6/zD5gjrJi6O2zY3+2j+UFTMhRn5HA7 +8L7mUia1EWGmRVvX5WxokzUW5wjgGHxcALSyWnZTrYmTVEvW4j9YPp1QiyTMSdni +rpI2oa+BHkSCs4FNF5nPPXQM6hGqkNKQKweFk56/8iAC07/QD8KIIy8PBbb9oZyq +ChtNn/EeXQoRVHx6i27nDmf6h0NN7Pb9OGO4t5mak8XbXRrn6NvYEAlnAUvNEdHr +Tb8ywUyTAgMBAAECggEAChMn0VI+XI2Y9yF3BQqQmIUN28Aj7Z9M3iRFvu/6z07u +dQzB3Nkq1E/4dG062UlI2FUfN5AGMIsV4sN+AYkzVDG85SCpAndVkJ0pYh971401 +eRfye0DC9IlZ5cyXnq//GuAzEf24prp5SAcETcrXDfZ8G0newmHKx6F10V33Toih +MEHfSBSj1Ui3crUroJN/yYKuAmcZOQbum+P1vBEalBGuRAVEHXd2FiN5HIEQXBxx +fCFKi5d6yK6dD+4rlnp6mgPbT7CoBj6MSEd5WPF3gNbkZa9SDUmrqbKttDk1HaPX +by5rxlgQTS7Nfz+WFisafNwjnVywi0vOjKD2ZhBpoQKBgQDZ2HeOe0vpflSfFzvN +f7TCcj/tpnj45jovOpnQaFWxOjXNZPtHOzMZtAgRsENSCMIucGvIEn8r6tamuFh7 +doYIIkvA5j/5N04s8Sl5NadH/vKce+j1wtUteWye8oLQuw2c4AlDplPMejCmZY+i +V/PQoA1OzhAYRTlXEOLDRFnaKQKBgQDjAlU0cvsVTXEgwPSEgEl47Zl16YBMi2xq +yD6T921/wxGiR4wtSEzKlZ/LhRU7kxEuMTcIjvCej39oBhUki2owsvOka0ivBv6c +5fvsgrbd+jyl3QsT7c8M5b3912pbl3HFcMfYQzw0/bOpau+sj0TV5Ponr4nZgTp1 +INuz8+LAWwKBgQDPiNTlbXr1d//gDD9R2B75u+RBYH4RCSxXQCm3DR7OF5mYEmL9 +Cl31V7j0OQr5hRSRL1LPKSf0S+awsCDDhjfMWff3TqOVpeWZFSsgqUezZCP3hmh6 +cWGrz+j2SCzt87XVRO4uf6+HtsTQUSMUU1wY3dGvyMo2hQRKePC/fEdpeQKBgFq/ +uP4hpPwsHDhiyp0Zh8We/kUj1lVDO1EowdN3C0AS5D7CaWhEyeYGkH3UsttA/JJB +vGVgdxJ7/QvBurwEO6xCLaIh/Uly+2APlHlE/AObIJmR1vbdj3LxeNU8Q1lgHmw2 +nL14i14HucXVaQDLuVHkmpg41VutDIh8XTgAHDqXAoGAdz+Bd2iOVH8NMiGgounW +OHd5up9c7teYoG/9cXGbqDm+5zAnEORWvc4ioKpyK7o0dKYj0As68XE1wAv0lafz +gva4Z56ZUA+lFkUquhXta2sQbAdryyXo0tAnql2u3VihTI7sH5NevF/HcqeJpQK9 +vYYjuqNpqx1tLszSS9d6z6M= +-----END PRIVATE KEY----- diff --git a/infra/environments/local/compose/traefik/dynamic.yaml b/infra/environments/local/compose/traefik/dynamic.yaml new file mode 100644 index 0000000..bb96283 --- /dev/null +++ b/infra/environments/local/compose/traefik/dynamic.yaml @@ -0,0 +1,21 @@ +tls: + stores: + default: + defaultCertificate: + certFile: /etc/traefik/certs/traefik.cert + keyFile: /etc/traefik/certs/traefik.key + +http: + routers: + countup: + entryPoints: + - localhttps + service: countup + rule: Host(`localhost`) + tls: {} + + services: + countup: + loadBalancer: + servers: + - url: http://host.docker.internal:8080 \ No newline at end of file diff --git a/infra/environments/local/compose/traefik/traefik.yaml b/infra/environments/local/compose/traefik/traefik.yaml new file mode 100644 index 0000000..af25b85 --- /dev/null +++ b/infra/environments/local/compose/traefik/traefik.yaml @@ -0,0 +1,16 @@ +log: + level: INFO + +api: + insecure: true + +providers: + docker: + exposedByDefault: false + + file: + filename: /etc/traefik/dynamic.yaml + +entryPoints: + localhttps: + address: :4043 \ No newline at end of file diff --git a/infra/environments/local/gcp/terraform.tf b/infra/environments/local/gcp/terraform.tf new file mode 100644 index 0000000..0f26555 --- /dev/null +++ b/infra/environments/local/gcp/terraform.tf @@ -0,0 +1,20 @@ +terraform { + required_version = "1.8.7" + + required_providers { + google = { + source = "hashicorp/google" + version = "6.13.0" + } + } +} + +provider "google" { + add_terraform_attribution_label = true +} + +data "google_project" "this" {} + +output "google_project" { + value = data.google_project.this.project_id +} diff --git a/infra/environments/prd/.gitkeep b/infra/environments/prd/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/infra/spacelift/init/.terraform.lock.hcl b/infra/spacelift/init/.terraform.lock.hcl new file mode 100644 index 0000000..a444097 --- /dev/null +++ b/infra/spacelift/init/.terraform.lock.hcl @@ -0,0 +1,34 @@ +# This file is maintained automatically by "tofu init". +# Manual edits may be lost in future updates. + +provider "registry.opentofu.org/hashicorp/google" { + version = "6.13.0" + constraints = "6.13.0" + hashes = [ + "h1:eA2yW7QdrvxBRbQoo1zkGYNe76DUU6BuMiLMmom4jmM=", + "zh:051e86a775ffb0140603d0a280cae5b622c16997197944e68b509cc89a702e12", + "zh:1db6571b0c83808ab1af038334c8dbed97e0103d18af74b982014ef4307692ed", + "zh:2745cb842daa0a5a60994060aa8b5cbfde7714732a525dc0715b07a392effd67", + "zh:38f77748a71a5969179472bda2d34a0ae1c6c3e189bb6c90f34a547939611586", + "zh:4badf4cce94b9e3817183a43494a9d783210822a99c8ec1723e67bc89cbd0a6c", + "zh:63c8b27db8fdb8cd9cb5f6152e63e81b9d368b0ba99d6a4a7b1c91f9c51ccf11", + "zh:80fa2e658f15fba19cbf84bdb0dd1ff541067ac90cacb2b2f6ba3eaadd7917b9", + "zh:ce365e3b6bd773da76710723efb6f5e2fcaf16f5f3e843b3174af87fc718835f", + "zh:d0695f59ce9f225510b55f327b629b031c4f190af9f81626d0a8ec8de76cec98", + "zh:d7baefcba8f748c6392e987dbb4ba56cc6bf711102ce6963d3d77532a70a2373", + ] +} + +provider "registry.opentofu.org/spacelift-io/spacelift" { + version = "1.19.0" + constraints = "1.19.0" + hashes = [ + "h1:44h+EkMz33Ux41VRtz1+xZQKTqV1+S3/NGSctEjpdCk=", + "zh:169ac3ca7d5f11c4c3e0bdfc9d7f1fec7e7e608bffe0072ae03728e36326438d", + "zh:37d6959786e8fb537d9d83f61f880631d8fd1af7ab53993d172011a420def9d3", + "zh:3ae27ca62eb998bb3440584f9b7662fc2bc9befdc3f81bf3cb856012708e573f", + "zh:3f81144ae4ed59209172d7eae26f1fdc06c9dacfe0c7ed4cdb4a83f6603aadef", + "zh:e1e1cdd889299b4b823a611c22b3becb2ee29138a870cad9c6fbf0bb6411984a", + "zh:e429e956ac328fdbd42279907fc0f7e3eb6080945225dd38b90577bc30f64d5b", + ] +} diff --git a/infra/spacelift/init/contexts.tf b/infra/spacelift/init/contexts.tf new file mode 100644 index 0000000..ba16594 --- /dev/null +++ b/infra/spacelift/init/contexts.tf @@ -0,0 +1,20 @@ +resource "spacelift_context" "terraform_provider_google" { + space_id = spacelift_stack.root.id + name = "terraform-provider-google" + description = "Configuration for terraform-provider-google" + labels = ["autoattach:terraform-provider-google"] +} + +resource "spacelift_environment_variable" "google_project" { + context_id = spacelift_context.terraform_provider_google.id + name = "GOOGLE_PROJECT" + value = var.google_project + write_only = false +} + +resource "spacelift_environment_variable" "google_region" { + context_id = spacelift_context.terraform_provider_google.id + name = "GOOGLE_REGION" + value = var.google_region + write_only = false +} diff --git a/infra/spacelift/init/main.tf b/infra/spacelift/init/main.tf new file mode 100644 index 0000000..eceb260 --- /dev/null +++ b/infra/spacelift/init/main.tf @@ -0,0 +1,45 @@ +resource "spacelift_stack" "root" { + space_id = "root" + name = "root" + description = "🚀 Root stack for managing other Spacelift stacks" + repository = "countup" + branch = "main" + project_root = "infra/spacelift/root" + terraform_workflow_tool = "OPEN_TOFU" + terraform_version = "1.8.7" + administrative = true + protect_from_deletion = true + terraform_smart_sanitization = true + enable_well_known_secret_masking = true + + labels = ["feature:add_plan_pr_comment", "terraform-provider-google"] +} + +resource "spacelift_gcp_service_account" "root" { + stack_id = spacelift_stack.root.id + token_scopes = [ + "https://www.googleapis.com/auth/compute", + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/ndev.clouddns.readwrite", + "https://www.googleapis.com/auth/devstorage.full_control", + "https://www.googleapis.com/auth/userinfo.email", + ] +} + +resource "google_project_iam_member" "spacelift_editor" { + project = var.google_project + role = "roles/editor" + member = "serviceAccount:${spacelift_gcp_service_account.root.service_account_email}" +} + +resource "google_project_iam_member" "spacelift_project_iam_admin" { + project = var.google_project + role = "roles/resourcemanager.projectIamAdmin" + member = "serviceAccount:${spacelift_gcp_service_account.root.service_account_email}" +} + +resource "google_project_iam_member" "spacelift_secret_accessor" { + project = var.google_project + role = "roles/secretmanager.secretAccessor" + member = "serviceAccount:${spacelift_gcp_service_account.root.service_account_email}" +} diff --git a/infra/spacelift/init/terraform.tf b/infra/spacelift/init/terraform.tf new file mode 100644 index 0000000..7b4e529 --- /dev/null +++ b/infra/spacelift/init/terraform.tf @@ -0,0 +1,27 @@ +terraform { + required_version = "1.8.7" + + required_providers { + google = { + source = "hashicorp/google" + version = "6.13.0" + } + spacelift = { + source = "spacelift-io/spacelift" + version = "1.19.0" + } + } +} + +provider "google" { + project = var.google_project + region = var.google_region + + add_terraform_attribution_label = true +} + +provider "spacelift" { + api_key_endpoint = var.spacelift_endpoint + api_key_id = var.spacelift_key_id + api_key_secret = var.spacelift_key_secret +} diff --git a/infra/spacelift/init/terraform.tfstate b/infra/spacelift/init/terraform.tfstate new file mode 100644 index 0000000..7620338 --- /dev/null +++ b/infra/spacelift/init/terraform.tfstate @@ -0,0 +1 @@ +{"version":4,"terraform_version":"1.8.7","serial":16,"lineage":"dd0e9105-bb8d-b143-544e-487cef17ab9d","outputs":{},"resources":[{"mode":"managed","type":"google_project_iam_member","name":"spacelift_editor","provider":"provider[\"registry.opentofu.org/hashicorp/google\"]","instances":[{"schema_version":0,"attributes":{"condition":[],"etag":"BwYoLRmpfGE=","id":"emp-jace-1fb5/roles/editor/serviceAccount:gcp-01je045ze85st5v7yny372s5z5@spacelift.iam.gserviceaccount.com","member":"serviceAccount:gcp-01je045ze85st5v7yny372s5z5@spacelift.iam.gserviceaccount.com","project":"emp-jace-1fb5","role":"roles/editor"},"sensitive_attributes":[],"private":"bnVsbA==","dependencies":["spacelift_gcp_service_account.root","spacelift_stack.root"]}]},{"mode":"managed","type":"google_project_iam_member","name":"spacelift_project_iam_admin","provider":"provider[\"registry.opentofu.org/hashicorp/google\"]","instances":[{"schema_version":0,"attributes":{"condition":[],"etag":"BwYoLRmpfGE=","id":"emp-jace-1fb5/roles/resourcemanager.projectIamAdmin/serviceAccount:gcp-01je045ze85st5v7yny372s5z5@spacelift.iam.gserviceaccount.com","member":"serviceAccount:gcp-01je045ze85st5v7yny372s5z5@spacelift.iam.gserviceaccount.com","project":"emp-jace-1fb5","role":"roles/resourcemanager.projectIamAdmin"},"sensitive_attributes":[],"private":"bnVsbA==","dependencies":["spacelift_gcp_service_account.root","spacelift_stack.root"]}]},{"mode":"managed","type":"google_project_iam_member","name":"spacelift_secret_accessor","provider":"provider[\"registry.opentofu.org/hashicorp/google\"]","instances":[{"schema_version":0,"attributes":{"condition":[],"etag":"BwYoLRmpfGE=","id":"emp-jace-1fb5/roles/secretmanager.secretAccessor/serviceAccount:gcp-01je045ze85st5v7yny372s5z5@spacelift.iam.gserviceaccount.com","member":"serviceAccount:gcp-01je045ze85st5v7yny372s5z5@spacelift.iam.gserviceaccount.com","project":"emp-jace-1fb5","role":"roles/secretmanager.secretAccessor"},"sensitive_attributes":[],"private":"bnVsbA==","dependencies":["spacelift_gcp_service_account.root","spacelift_stack.root"]}]},{"mode":"managed","type":"spacelift_context","name":"terraform_provider_google","provider":"provider[\"registry.opentofu.org/spacelift-io/spacelift\"]","instances":[{"schema_version":0,"attributes":{"after_apply":[],"after_destroy":[],"after_init":[],"after_perform":[],"after_plan":[],"after_run":[],"before_apply":[],"before_destroy":[],"before_init":[],"before_perform":[],"before_plan":[],"description":"Configuration for terraform-provider-google","id":"terraform-provider-google","labels":["autoattach:terraform-provider-google"],"name":"terraform-provider-google","space_id":"root"},"sensitive_attributes":[],"private":"eyJzY2hlbWFfdmVyc2lvbiI6IjAifQ==","dependencies":["spacelift_stack.root"]}]},{"mode":"managed","type":"spacelift_environment_variable","name":"google_project","provider":"provider[\"registry.opentofu.org/spacelift-io/spacelift\"]","instances":[{"schema_version":0,"attributes":{"checksum":"31071a89839ebcaa521fc9d00727e95c4837df8ce2a2da8867f793c9ec87f2f3","context_id":"terraform-provider-google","description":"","id":"context/terraform-provider-google/GOOGLE_PROJECT","module_id":null,"name":"GOOGLE_PROJECT","stack_id":null,"value":"emp-jace-1fb5","write_only":false},"sensitive_attributes":[[{"type":"get_attr","value":"value"}]],"private":"bnVsbA==","dependencies":["spacelift_context.terraform_provider_google","spacelift_stack.root"]}]},{"mode":"managed","type":"spacelift_environment_variable","name":"google_region","provider":"provider[\"registry.opentofu.org/spacelift-io/spacelift\"]","instances":[{"schema_version":0,"attributes":{"checksum":"14fa84cb92dd1f1af064f81fe785be7f8906e54f3c378e3f98e93238daeb78f8","context_id":"terraform-provider-google","description":"","id":"context/terraform-provider-google/GOOGLE_REGION","module_id":null,"name":"GOOGLE_REGION","stack_id":null,"value":"europe-west1","write_only":false},"sensitive_attributes":[[{"type":"get_attr","value":"value"}]],"private":"bnVsbA==","dependencies":["spacelift_context.terraform_provider_google","spacelift_stack.root"]}]},{"mode":"managed","type":"spacelift_gcp_service_account","name":"root","provider":"provider[\"registry.opentofu.org/spacelift-io/spacelift\"]","instances":[{"schema_version":0,"attributes":{"id":"root","module_id":null,"service_account_email":"gcp-01je045ze85st5v7yny372s5z5@spacelift.iam.gserviceaccount.com","stack_id":"root","token_scopes":["https://www.googleapis.com/auth/cloud-platform","https://www.googleapis.com/auth/compute","https://www.googleapis.com/auth/devstorage.full_control","https://www.googleapis.com/auth/ndev.clouddns.readwrite","https://www.googleapis.com/auth/userinfo.email"]},"sensitive_attributes":[],"private":"bnVsbA==","dependencies":["spacelift_stack.root"]}]},{"mode":"managed","type":"spacelift_stack","name":"root","provider":"provider[\"registry.opentofu.org/spacelift-io/spacelift\"]","instances":[{"schema_version":0,"attributes":{"additional_project_globs":[],"administrative":true,"after_apply":[],"after_destroy":[],"after_init":[],"after_perform":[],"after_plan":[],"after_run":[],"ansible":[],"autodeploy":false,"autoretry":false,"aws_assume_role_policy_statement":"{\"Action\":\"sts:AssumeRole\",\"Condition\":{\"StringEquals\":{\"sts:ExternalId\":\"jace-ys@root\"}},\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"324880187172\"}}","azure_devops":[],"before_apply":[],"before_destroy":[],"before_init":[],"before_perform":[],"before_plan":[],"bitbucket_cloud":[],"bitbucket_datacenter":[],"branch":"main","cloudformation":[],"description":"🚀 Root stack for managing other Spacelift stacks","enable_local_preview":false,"enable_well_known_secret_masking":true,"github_action_deploy":true,"github_enterprise":[],"gitlab":[],"id":"root","import_state":null,"import_state_file":null,"kubernetes":[],"labels":["feature:add_plan_pr_comment","terraform-provider-google"],"manage_state":true,"name":"root","project_root":"infra/spacelift/root","protect_from_deletion":true,"pulumi":[],"raw_git":[],"repository":"countup","runner_image":"","showcase":[],"slug":"root","space_id":"root","terraform_external_state_access":false,"terraform_smart_sanitization":true,"terraform_version":"1.8.7","terraform_workflow_tool":"OPEN_TOFU","terraform_workspace":"","terragrunt":[],"worker_pool_id":""},"sensitive_attributes":[[{"type":"get_attr","value":"import_state"}]],"private":"bnVsbA=="}]}],"check_results":null} diff --git a/infra/spacelift/init/terraform.tfstate.backup b/infra/spacelift/init/terraform.tfstate.backup new file mode 100644 index 0000000..75bbabe --- /dev/null +++ b/infra/spacelift/init/terraform.tfstate.backup @@ -0,0 +1 @@ +{"version":4,"terraform_version":"1.8.7","serial":15,"lineage":"dd0e9105-bb8d-b143-544e-487cef17ab9d","outputs":{},"resources":[{"mode":"managed","type":"google_project_iam_member","name":"spacelift_editor","provider":"provider[\"registry.terraform.io/hashicorp/google\"]","instances":[{"schema_version":0,"attributes":{"condition":[],"etag":"BwYoLRmpfGE=","id":"emp-jace-1fb5/roles/editor/serviceAccount:gcp-01je045ze85st5v7yny372s5z5@spacelift.iam.gserviceaccount.com","member":"serviceAccount:gcp-01je045ze85st5v7yny372s5z5@spacelift.iam.gserviceaccount.com","project":"emp-jace-1fb5","role":"roles/editor"},"sensitive_attributes":[],"private":"bnVsbA==","dependencies":["spacelift_gcp_service_account.root","spacelift_stack.root"]}]},{"mode":"managed","type":"google_project_iam_member","name":"spacelift_project_iam_admin","provider":"provider[\"registry.terraform.io/hashicorp/google\"]","instances":[{"schema_version":0,"attributes":{"condition":[],"etag":"BwYoLRmpfGE=","id":"emp-jace-1fb5/roles/resourcemanager.projectIamAdmin/serviceAccount:gcp-01je045ze85st5v7yny372s5z5@spacelift.iam.gserviceaccount.com","member":"serviceAccount:gcp-01je045ze85st5v7yny372s5z5@spacelift.iam.gserviceaccount.com","project":"emp-jace-1fb5","role":"roles/resourcemanager.projectIamAdmin"},"sensitive_attributes":[],"private":"bnVsbA==","dependencies":["spacelift_gcp_service_account.root","spacelift_stack.root"]}]},{"mode":"managed","type":"google_project_iam_member","name":"spacelift_secret_accessor","provider":"provider[\"registry.terraform.io/hashicorp/google\"]","instances":[{"schema_version":0,"attributes":{"condition":[],"etag":"BwYoLRmpfGE=","id":"emp-jace-1fb5/roles/secretmanager.secretAccessor/serviceAccount:gcp-01je045ze85st5v7yny372s5z5@spacelift.iam.gserviceaccount.com","member":"serviceAccount:gcp-01je045ze85st5v7yny372s5z5@spacelift.iam.gserviceaccount.com","project":"emp-jace-1fb5","role":"roles/secretmanager.secretAccessor"},"sensitive_attributes":[],"private":"bnVsbA==","dependencies":["spacelift_gcp_service_account.root","spacelift_stack.root"]}]},{"mode":"managed","type":"spacelift_context","name":"terraform_provider_google","provider":"provider[\"registry.terraform.io/spacelift-io/spacelift\"]","instances":[{"schema_version":0,"attributes":{"after_apply":[],"after_destroy":[],"after_init":[],"after_perform":[],"after_plan":[],"after_run":[],"before_apply":[],"before_destroy":[],"before_init":[],"before_perform":[],"before_plan":[],"description":"Configuration for terraform-provider-google","id":"terraform-provider-google","labels":["autoattach:terraform-provider-google"],"name":"terraform-provider-google","space_id":"root"},"sensitive_attributes":[],"private":"eyJzY2hlbWFfdmVyc2lvbiI6IjAifQ==","dependencies":["spacelift_stack.root"]}]},{"mode":"managed","type":"spacelift_environment_variable","name":"google_project","provider":"provider[\"registry.terraform.io/spacelift-io/spacelift\"]","instances":[{"schema_version":0,"attributes":{"checksum":"31071a89839ebcaa521fc9d00727e95c4837df8ce2a2da8867f793c9ec87f2f3","context_id":"terraform-provider-google","description":"","id":"context/terraform-provider-google/GOOGLE_PROJECT","module_id":null,"name":"GOOGLE_PROJECT","stack_id":null,"value":"emp-jace-1fb5","write_only":false},"sensitive_attributes":[],"private":"bnVsbA==","dependencies":["spacelift_context.terraform_provider_google","spacelift_stack.root"]}]},{"mode":"managed","type":"spacelift_environment_variable","name":"google_region","provider":"provider[\"registry.terraform.io/spacelift-io/spacelift\"]","instances":[{"schema_version":0,"attributes":{"checksum":"14fa84cb92dd1f1af064f81fe785be7f8906e54f3c378e3f98e93238daeb78f8","context_id":"terraform-provider-google","description":"","id":"context/terraform-provider-google/GOOGLE_REGION","module_id":null,"name":"GOOGLE_REGION","stack_id":null,"value":"europe-west1","write_only":false},"sensitive_attributes":[],"private":"bnVsbA==","dependencies":["spacelift_context.terraform_provider_google","spacelift_stack.root"]}]},{"mode":"managed","type":"spacelift_gcp_service_account","name":"root","provider":"provider[\"registry.terraform.io/spacelift-io/spacelift\"]","instances":[{"schema_version":0,"attributes":{"id":"root","module_id":null,"service_account_email":"gcp-01je045ze85st5v7yny372s5z5@spacelift.iam.gserviceaccount.com","stack_id":"root","token_scopes":["https://www.googleapis.com/auth/cloud-platform","https://www.googleapis.com/auth/compute","https://www.googleapis.com/auth/devstorage.full_control","https://www.googleapis.com/auth/ndev.clouddns.readwrite","https://www.googleapis.com/auth/userinfo.email"]},"sensitive_attributes":[],"private":"bnVsbA==","dependencies":["spacelift_stack.root"]}]},{"mode":"managed","type":"spacelift_stack","name":"root","provider":"provider[\"registry.terraform.io/spacelift-io/spacelift\"]","instances":[{"schema_version":0,"attributes":{"additional_project_globs":[],"administrative":true,"after_apply":[],"after_destroy":[],"after_init":[],"after_perform":[],"after_plan":[],"after_run":[],"ansible":[],"autodeploy":false,"autoretry":false,"aws_assume_role_policy_statement":"{\"Action\":\"sts:AssumeRole\",\"Condition\":{\"StringEquals\":{\"sts:ExternalId\":\"jace-ys@root\"}},\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"324880187172\"}}","azure_devops":[],"before_apply":[],"before_destroy":[],"before_init":[],"before_perform":[],"before_plan":[],"bitbucket_cloud":[],"bitbucket_datacenter":[],"branch":"main","cloudformation":[],"description":"🚀 Root stack for managing other Spacelift stacks","enable_local_preview":false,"enable_well_known_secret_masking":true,"github_action_deploy":true,"github_enterprise":[],"gitlab":[],"id":"root","import_state":null,"import_state_file":null,"kubernetes":[],"labels":["feature:add_plan_pr_comment","terraform-provider-google"],"manage_state":true,"name":"root","project_root":"infra/spacelift/root","protect_from_deletion":true,"pulumi":[],"raw_git":[],"repository":"countup","runner_image":"","showcase":[],"slug":"root","space_id":"root","terraform_external_state_access":false,"terraform_smart_sanitization":true,"terraform_version":"1.5.7","terraform_workflow_tool":"TERRAFORM_FOSS","terraform_workspace":"","terragrunt":[],"worker_pool_id":""},"sensitive_attributes":[],"private":"bnVsbA=="}]}],"check_results":null} diff --git a/infra/spacelift/init/variables.tf b/infra/spacelift/init/variables.tf new file mode 100644 index 0000000..557ff6a --- /dev/null +++ b/infra/spacelift/init/variables.tf @@ -0,0 +1,24 @@ +variable "google_project" { + type = string + default = "emp-jace-1fb5" +} + +variable "google_region" { + type = string + default = "europe-west1" +} + +variable "spacelift_endpoint" { + type = string + default = "https://jace-ys.app.spacelift.io" +} + +variable "spacelift_key_id" { + type = string + default = "01JAN9TH8H60PCN3N30V1ZES81" +} + +variable "spacelift_key_secret" { + type = string + sensitive = true +} diff --git a/infra/spacelift/root/main.tf b/infra/spacelift/root/main.tf new file mode 100644 index 0000000..44dd74d --- /dev/null +++ b/infra/spacelift/root/main.tf @@ -0,0 +1,58 @@ +locals { + environments = [for i in fileset("../../environments", "*/gcp/terraform.tf") : dirname(dirname(i))] +} + +resource "spacelift_stack" "environment" { + for_each = toset(local.environments) + + space_id = "root" + name = each.key + description = "🌳 Environment [${each.key}]" + repository = "countup" + branch = "main" + project_root = "infra/environments/${each.key}/gcp" + terraform_version = "1.8.7" + administrative = false + protect_from_deletion = true + terraform_smart_sanitization = true + enable_well_known_secret_masking = true + + labels = ["feature:add_plan_pr_comment", "terraform-provider-google"] +} + +resource "spacelift_gcp_service_account" "environment" { + for_each = toset(local.environments) + + stack_id = spacelift_stack.environment[each.key].id + token_scopes = [ + "https://www.googleapis.com/auth/compute", + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/ndev.clouddns.readwrite", + "https://www.googleapis.com/auth/devstorage.full_control", + "https://www.googleapis.com/auth/userinfo.email", + ] +} + +resource "google_project_iam_member" "environment_editor" { + for_each = toset(local.environments) + + project = data.google_project.this.project_id + role = "roles/editor" + member = "serviceAccount:${spacelift_gcp_service_account.environment[each.key].service_account_email}" +} + +resource "google_project_iam_member" "environment_project_iam_admin" { + for_each = toset(local.environments) + + project = data.google_project.this.project_id + role = "roles/resourcemanager.projectIamAdmin" + member = "serviceAccount:${spacelift_gcp_service_account.environment[each.key].service_account_email}" +} + +resource "google_project_iam_member" "environment_secret_accessor" { + for_each = toset(local.environments) + + project = data.google_project.this.project_id + role = "roles/secretmanager.secretAccessor" + member = "serviceAccount:${spacelift_gcp_service_account.environment[each.key].service_account_email}" +} diff --git a/infra/spacelift/root/terraform.tf b/infra/spacelift/root/terraform.tf new file mode 100644 index 0000000..f4aea6f --- /dev/null +++ b/infra/spacelift/root/terraform.tf @@ -0,0 +1,23 @@ +terraform { + required_version = "1.8.7" + + required_providers { + google = { + source = "hashicorp/google" + version = "6.13.0" + } + spacelift = { + source = "spacelift-io/spacelift" + version = "1.19.0" + } + } +} + +provider "google" { + add_terraform_attribution_label = true +} + +provider "spacelift" { +} + +data "google_project" "this" {}