diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..6ed914b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +--- +version: 2 +updates: +- package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" +- package-ecosystem: "gomod" + directory: "/app" + schedule: + interval: "weekly" +- package-ecosystem: "docker" + directory: "/app" + schedule: + interval: "weekly" diff --git a/.github/workflows/app.yaml b/.github/workflows/app.yaml new file mode 100644 index 0000000..68adb11 --- /dev/null +++ b/.github/workflows/app.yaml @@ -0,0 +1,96 @@ +--- +name: app + +on: [push, pull_request] + +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.2' + 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.2' + 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.2' + 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.2' + cache-dependency-path: 'app/go.sum' + - run: task build diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7bc9ab5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# 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/ + +# Task +.task/ \ No newline at end of file diff --git a/.mise.toml b/.mise.toml new file mode 100644 index 0000000..1c51d44 --- /dev/null +++ b/.mise.toml @@ -0,0 +1,11 @@ +[tools] +atlas = "0.28.0" +golang = "1.23.2" +golangci-lint = "1.61.0" +protoc = "28.2" +protoc-gen-go = "1.35.1" +protoc-gen-go-grpc = "1.5.1" +task = "3.39.2" + +"go:github.com/sqlc-dev/sqlc/cmd/sqlc" = "1.27.0" +"go:goa.design/goa/v3/cmd/goa" = "3.19.1" diff --git a/README.md b/README.md index 6a75e7d..a85f711 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,12 @@ # Count Up -A little web app game, consolidating the best practices from my years of experience building production-ready software and infrastructure. \ No newline at end of file +A little web app game, consolidating the best practices from my years of experience building production-ready software and infrastructure. + +Probably way overengineered for incrementing a counter, but thought this will be a good exercise to codify my learnings! + +## Features + +- Async, transactional worker system for processing background jobs using [River](https://riverqueue.com/). +- Compiled SQL queries into type-safe application code using [sqlc](https://sqlc.dev/). +- Declarative database schema and migrations using a combination of [Atlas](https://atlasgo.io/) + [Goose](https://pressly.github.io/goose/). +- DDD-lite, interface-driven approach to writing decoupled and testable business logic. \ No newline at end of file diff --git a/Taskfile.yaml b/Taskfile.yaml new file mode 100644 index 0000000..6d8d0a8 --- /dev/null +++ b/Taskfile.yaml @@ -0,0 +1,135 @@ +--- +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=dev + DATABASE_CONNECTION_URI: postgresql://countup:countup@localhost:5432/countup + + 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/**/*.go + + gen:sqlc: + internal: true + deps: ['migration:plan'] + dir: app + cmds: + - sqlc generate + sources: + - schema/*.sql + + migration:new: + dir: app + cmds: + - atlas migrate new --env dev {{ .NAME }} + requires: + vars: [NAME] + + migration:plan: + dir: app + cmds: + - atlas migrate diff --env dev {{ .NAME }} + sources: + - schema/schema.sql + - schema/migrations/*.sql + generates: + - schema/migrations/*.sql + + migration:hash: + dir: app + cmds: + - atlas migrate hash --env dev + + 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 }} . + + compose: + ignore_error: true + deps: ['gen'] + cmds: + - docker compose --profile apps up --build {{ .CLI_ARGS }} + - defer: {task: 'compose:down'} + + compose:infra: + ignore_error: true + deps: ['gen'] + cmds: + - docker compose up {{ .CLI_ARGS }} + - defer: {task: 'compose:down'} + + compose:down: + ignore_error: true + cmds: + - docker compose down -v --remove-orphans diff --git a/app/.golangci.yaml b/app/.golangci.yaml new file mode 100644 index 0000000..4bf917c --- /dev/null +++ b/app/.golangci.yaml @@ -0,0 +1,127 @@ +--- +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: 50 + 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 + +linters: + disable-all: true + enable: + - errcheck + - gosimple + - govet + - ineffassign + - staticcheck + - unused + + - asasalint + - asciicheck + - bidichk + - bodyclose + - canonicalheader + - copyloopvar + - cyclop + - decorder + - dupl + - 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: + max-same-issues: 50 + + exclude-dirs: + - ^api$ + - ^cmd/countup-cli$ + - ^schema/migrations$ + + exclude-files: + - ^internal/handler/api/echo.go$ diff --git a/app/Dockerfile b/app/Dockerfile new file mode 100644 index 0000000..44de05a --- /dev/null +++ b/app/Dockerfile @@ -0,0 +1,15 @@ +FROM golang:1.23.2 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 scratch +WORKDIR /app +COPY --from=builder /src/dist /app/bin +ENTRYPOINT ["/app/bin/countup"] \ No newline at end of file diff --git a/app/api/v1/countup.go b/app/api/v1/countup.go new file mode 100644 index 0000000..19d707f --- /dev/null +++ b/app/api/v1/countup.go @@ -0,0 +1,130 @@ +package apiv1 + +import ( + "embed" + + . "goa.design/goa/v3/dsl" +) + +var ( + //go:embed gen/http/*.json gen/http/*.yaml + OpenAPIFS embed.FS +) + +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") + Host("dev-http", func() { + URI("http://localhost:8080") + }) + Host("dev-grpc", func() { + URI("grpc://localhost:8081") + }) + }) +}) + +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") +}) + +var _ = Service("api", func() { + Error("unauthorized") + Error("existing_increment_request", func() { + Temporary() + }) + + HTTP(func() { + Response("unauthorized", StatusUnauthorized) + Response("existing_increment_request", StatusTooManyRequests) + }) + + GRPC(func() { + Response("unauthorized", CodePermissionDenied) + Response("existing_increment_request", CodeAlreadyExists) + }) + + Method("CounterGet", func() { + Result(CounterInfo) + + HTTP(func() { + GET("/counter") + Response(StatusOK) + }) + + GRPC(func() { + Response(CodeOK) + }) + }) + + Method("CounterIncrement", func() { + Payload(func() { + Field(1, "user", String) + Required("user") + }) + + Result(CounterInfo) + + HTTP(func() { + POST("/counter/inc") + Response(StatusAccepted) + }) + + GRPC(func() { + Response(CodeOK) + }) + }) + + 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") +}) + +var _ = Service("web", func() { + 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") + }) + }) + }) + + Files("/static/{*path}", "static/") +}) diff --git a/app/api/v1/gen/api/client.go b/app/api/v1/gen/api/client.go new file mode 100644 index 0000000..3e8a87f --- /dev/null +++ b/app/api/v1/gen/api/client.go @@ -0,0 +1,72 @@ +// 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 { + CounterGetEndpoint goa.Endpoint + CounterIncrementEndpoint goa.Endpoint + EchoEndpoint goa.Endpoint +} + +// NewClient initializes a "api" service client given the endpoints. +func NewClient(counterGet, counterIncrement, echo goa.Endpoint) *Client { + return &Client{ + CounterGetEndpoint: counterGet, + CounterIncrementEndpoint: counterIncrement, + EchoEndpoint: echo, + } +} + +// CounterGet calls the "CounterGet" endpoint of the "api" service. +// CounterGet may return the following errors: +// - "unauthorized" (type *goa.ServiceError) +// - "existing_increment_request" (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: +// - "unauthorized" (type *goa.ServiceError) +// - "existing_increment_request" (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 +} + +// Echo calls the "Echo" endpoint of the "api" service. +// Echo may return the following errors: +// - "unauthorized" (type *goa.ServiceError) +// - "existing_increment_request" (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/api/endpoints.go b/app/api/v1/gen/api/endpoints.go new file mode 100644 index 0000000..27587bd --- /dev/null +++ b/app/api/v1/gen/api/endpoints.go @@ -0,0 +1,73 @@ +// 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" +) + +// Endpoints wraps the "api" service endpoints. +type Endpoints struct { + CounterGet goa.Endpoint + CounterIncrement goa.Endpoint + Echo goa.Endpoint +} + +// NewEndpoints wraps the methods of the "api" service with endpoints. +func NewEndpoints(s Service) *Endpoints { + return &Endpoints{ + CounterGet: NewCounterGetEndpoint(s), + CounterIncrement: NewCounterIncrementEndpoint(s), + Echo: NewEchoEndpoint(s), + } +} + +// Use applies the given middleware to all the "api" service endpoints. +func (e *Endpoints) Use(m func(goa.Endpoint) goa.Endpoint) { + e.CounterGet = m(e.CounterGet) + e.CounterIncrement = m(e.CounterIncrement) + e.Echo = m(e.Echo) +} + +// 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) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + p := req.(*CounterIncrementPayload) + res, err := s.CounterIncrement(ctx, p) + if err != nil { + return nil, err + } + vres := NewViewedCounterInfo(res, "default") + return vres, nil + } +} + +// NewEchoEndpoint returns an endpoint function that calls the method "Echo" of +// service "api". +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/api/service.go b/app/api/v1/gen/api/service.go new file mode 100644 index 0000000..52c794d --- /dev/null +++ b/app/api/v1/gen/api/service.go @@ -0,0 +1,119 @@ +// 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" +) + +// Service is the api service interface. +type Service interface { + // CounterGet implements CounterGet. + CounterGet(context.Context) (res *CounterInfo, err error) + // CounterIncrement implements CounterIncrement. + CounterIncrement(context.Context, *CounterIncrementPayload) (res *CounterInfo, err error) + // 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 = "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{"CounterGet", "CounterIncrement", "Echo"} + +// CounterIncrementPayload is the payload type of the api service +// CounterIncrement method. +type CounterIncrementPayload struct { + User string +} + +// CounterInfo is the result type of the api service CounterGet method. +type CounterInfo struct { + Count int32 + LastIncrementBy string + LastIncrementAt string + NextFinalizeAt string +} + +// EchoPayload is the payload type of the api service Echo method. +type EchoPayload struct { + Text string +} + +// EchoResult is the result type of the api service Echo method. +type EchoResult struct { + Text string +} + +// MakeUnauthorized builds a goa.ServiceError from an error. +func MakeUnauthorized(err error) *goa.ServiceError { + return goa.NewServiceError(err, "unauthorized", 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..0e67ec9 --- /dev/null +++ b/app/api/v1/gen/grpc/api/client/cli.go @@ -0,0 +1,55 @@ +// 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" +) + +// BuildCounterIncrementPayload builds the payload for the api CounterIncrement +// endpoint from CLI flags. +func BuildCounterIncrementPayload(apiCounterIncrementMessage string) (*api.CounterIncrementPayload, error) { + var err error + var message apipb.CounterIncrementRequest + { + if apiCounterIncrementMessage != "" { + err = json.Unmarshal([]byte(apiCounterIncrementMessage), &message) + if err != nil { + return nil, fmt.Errorf("invalid JSON for message, \nerror: %s, \nexample of valid JSON:\n%s", err, "'{\n \"user\": \"Non accusantium eos culpa autem illum architecto.\"\n }'") + } + } + } + v := &api.CounterIncrementPayload{ + User: message.User, + } + + return v, nil +} + +// BuildEchoPayload builds the payload for the api Echo endpoint from CLI flags. +func BuildEchoPayload(apiEchoMessage string) (*api.EchoPayload, error) { + var err error + var message apipb.EchoRequest + { + if apiEchoMessage != "" { + err = json.Unmarshal([]byte(apiEchoMessage), &message) + if err != nil { + return nil, fmt.Errorf("invalid JSON for message, \nerror: %s, \nexample of valid JSON:\n%s", err, "'{\n \"text\": \"Cupiditate tempore harum error iste ipsam natus.\"\n }'") + } + } + } + v := &api.EchoPayload{ + Text: message.Text, + } + + 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..38b0cf8 --- /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, + } +} // 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 + } +} // Echo calls the "Echo" function in apipb.APIClient 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 { + 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..358b5b5 --- /dev/null +++ b/app/api/v1/gen/grpc/api/client/encode_decode.go @@ -0,0 +1,127 @@ +// 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" +) + +// 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) + } + return NewProtoCounterIncrementRequest(payload), 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 +} // BuildEchoFunc builds the remote method to invoke for "api" service "Echo" +// endpoint. +func BuildEchoFunc(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.Echo(ctx, reqpb.(*apipb.EchoRequest), opts...) + } + return grpccli.Echo(ctx, &apipb.EchoRequest{}, opts...) + } +} + +// EncodeEchoRequest encodes requests sent to api Echo endpoint. +func EncodeEchoRequest(ctx context.Context, v any, md *metadata.MD) (any, error) { + payload, ok := v.(*api.EchoPayload) + if !ok { + return nil, goagrpc.ErrInvalidType("api", "Echo", "*api.EchoPayload", v) + } + return NewProtoEchoRequest(payload), nil +} + +// DecodeEchoResponse decodes responses from the api Echo endpoint. +func DecodeEchoResponse(ctx context.Context, v any, hdr, trlr metadata.MD) (any, error) { + message, ok := v.(*apipb.EchoResponse) + if !ok { + return nil, goagrpc.ErrInvalidType("api", "Echo", "*apipb.EchoResponse", v) + } + res := NewEchoResult(message) + return res, 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..596e175 --- /dev/null +++ b/app/api/v1/gen/grpc/api/client/types.go @@ -0,0 +1,72 @@ +// 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" +) + +// 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(payload *api.CounterIncrementPayload) *apipb.CounterIncrementRequest { + message := &apipb.CounterIncrementRequest{ + User: payload.User, + } + 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 +} + +// NewProtoEchoRequest builds the gRPC request type from the payload of the +// "Echo" endpoint of the "api" service. +func NewProtoEchoRequest(payload *api.EchoPayload) *apipb.EchoRequest { + message := &apipb.EchoRequest{ + Text: payload.Text, + } + return message +} + +// NewEchoResult builds the result type of the "Echo" endpoint of the "api" +// service from the gRPC response type. +func NewEchoResult(message *apipb.EchoResponse) *api.EchoResult { + result := &api.EchoResult{ + Text: message.Text, + } + 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..ecc0f0a --- /dev/null +++ b/app/api/v1/gen/grpc/api/pb/goagen_v1_api.pb.go @@ -0,0 +1,448 @@ +// 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.1 +// protoc v5.28.2 +// 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 CounterGetRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *CounterGetRequest) Reset() { + *x = CounterGetRequest{} + mi := &file_goagen_v1_api_proto_msgTypes[0] + 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[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 CounterGetRequest.ProtoReflect.Descriptor instead. +func (*CounterGetRequest) Descriptor() ([]byte, []int) { + return file_goagen_v1_api_proto_rawDescGZIP(), []int{0} +} + +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[1] + 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[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 CounterGetResponse.ProtoReflect.Descriptor instead. +func (*CounterGetResponse) Descriptor() ([]byte, []int) { + return file_goagen_v1_api_proto_rawDescGZIP(), []int{1} +} + +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 + + User string `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` +} + +func (x *CounterIncrementRequest) Reset() { + *x = CounterIncrementRequest{} + mi := &file_goagen_v1_api_proto_msgTypes[2] + 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[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 CounterIncrementRequest.ProtoReflect.Descriptor instead. +func (*CounterIncrementRequest) Descriptor() ([]byte, []int) { + return file_goagen_v1_api_proto_rawDescGZIP(), []int{2} +} + +func (x *CounterIncrementRequest) GetUser() string { + if x != nil { + return x.User + } + return "" +} + +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[3] + 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[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 CounterIncrementResponse.ProtoReflect.Descriptor instead. +func (*CounterIncrementResponse) Descriptor() ([]byte, []int) { + return file_goagen_v1_api_proto_rawDescGZIP(), []int{3} +} + +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 "" +} + +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_api_proto_msgTypes[4] + 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_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 EchoRequest.ProtoReflect.Descriptor instead. +func (*EchoRequest) Descriptor() ([]byte, []int) { + return file_goagen_v1_api_proto_rawDescGZIP(), []int{4} +} + +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_api_proto_msgTypes[5] + 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_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 EchoResponse.ProtoReflect.Descriptor instead. +func (*EchoResponse) Descriptor() ([]byte, []int) { + return file_goagen_v1_api_proto_rawDescGZIP(), []int{5} +} + +func (x *EchoResponse) GetText() string { + if x != nil { + return x.Text + } + 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, 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, 0x2d, + 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, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, + 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 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, 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, 0xc2, 0x01, 0x0a, 0x03, 0x41, 0x50, + 0x49, 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, 0x12, 0x2b, 0x0a, 0x04, 0x45, 0x63, 0x68, 0x6f, 0x12, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x45, 0x63, 0x68, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x61, 0x70, + 0x69, 0x2e, 0x45, 0x63, 0x68, 0x6f, 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{ + (*CounterGetRequest)(nil), // 0: api.CounterGetRequest + (*CounterGetResponse)(nil), // 1: api.CounterGetResponse + (*CounterIncrementRequest)(nil), // 2: api.CounterIncrementRequest + (*CounterIncrementResponse)(nil), // 3: api.CounterIncrementResponse + (*EchoRequest)(nil), // 4: api.EchoRequest + (*EchoResponse)(nil), // 5: api.EchoResponse +} +var file_goagen_v1_api_proto_depIdxs = []int32{ + 0, // 0: api.API.CounterGet:input_type -> api.CounterGetRequest + 2, // 1: api.API.CounterIncrement:input_type -> api.CounterIncrementRequest + 4, // 2: api.API.Echo:input_type -> api.EchoRequest + 1, // 3: api.API.CounterGet:output_type -> api.CounterGetResponse + 3, // 4: api.API.CounterIncrement:output_type -> api.CounterIncrementResponse + 5, // 5: api.API.Echo:output_type -> api.EchoResponse + 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..ec2860b --- /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 { + // CounterGet implements CounterGet. + rpc CounterGet (CounterGetRequest) returns (CounterGetResponse); + // CounterIncrement implements CounterIncrement. + rpc CounterIncrement (CounterIncrementRequest) returns (CounterIncrementResponse); + // Echo implements Echo. + rpc Echo (EchoRequest) returns (EchoResponse); +} + +message CounterGetRequest { +} + +message CounterGetResponse { + sint32 count = 1; + string last_increment_by = 2; + string last_increment_at = 3; + string next_finalize_at = 4; +} + +message CounterIncrementRequest { + string user = 1; +} + +message CounterIncrementResponse { + sint32 count = 1; + string last_increment_by = 2; + string last_increment_at = 3; + string next_finalize_at = 4; +} + +message EchoRequest { + string text = 1; +} + +message EchoResponse { + string text = 1; +} 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..eed11d7 --- /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.28.2 +// 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_CounterGet_FullMethodName = "/api.API/CounterGet" + API_CounterIncrement_FullMethodName = "/api.API/CounterIncrement" + API_Echo_FullMethodName = "/api.API/Echo" +) + +// 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 { + // 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) + // Echo implements Echo. + Echo(ctx context.Context, in *EchoRequest, opts ...grpc.CallOption) (*EchoResponse, error) +} + +type aPIClient struct { + cc grpc.ClientConnInterface +} + +func NewAPIClient(cc grpc.ClientConnInterface) APIClient { + return &aPIClient{cc} +} + +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 +} + +func (c *aPIClient) 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, API_Echo_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 { + // CounterGet implements CounterGet. + CounterGet(context.Context, *CounterGetRequest) (*CounterGetResponse, error) + // CounterIncrement implements CounterIncrement. + CounterIncrement(context.Context, *CounterIncrementRequest) (*CounterIncrementResponse, error) + // Echo implements Echo. + Echo(context.Context, *EchoRequest) (*EchoResponse, 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) 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) Echo(context.Context, *EchoRequest) (*EchoResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Echo 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_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) +} + +func _API_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.(APIServer).Echo(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: API_Echo_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).Echo(ctx, req.(*EchoRequest)) + } + 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: "CounterGet", + Handler: _API_CounterGet_Handler, + }, + { + MethodName: "CounterIncrement", + Handler: _API_CounterIncrement_Handler, + }, + { + MethodName: "Echo", + Handler: _API_Echo_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..2c40680 --- /dev/null +++ b/app/api/v1/gen/grpc/api/server/encode_decode.go @@ -0,0 +1,91 @@ +// 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" + + 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" +) + +// 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 ( + message *apipb.CounterIncrementRequest + ok bool + ) + { + if message, ok = v.(*apipb.CounterIncrementRequest); !ok { + return nil, goagrpc.ErrInvalidType("api", "CounterIncrement", "*apipb.CounterIncrementRequest", v) + } + } + var payload *api.CounterIncrementPayload + { + payload = NewCounterIncrementPayload(message) + } + return payload, nil +} + +// EncodeEchoResponse encodes responses from the "api" service "Echo" endpoint. +func EncodeEchoResponse(ctx context.Context, v any, hdr, trlr *metadata.MD) (any, error) { + result, ok := v.(*api.EchoResult) + if !ok { + return nil, goagrpc.ErrInvalidType("api", "Echo", "*api.EchoResult", v) + } + resp := NewProtoEchoResponse(result) + return resp, nil +} + +// DecodeEchoRequest decodes requests sent to "api" service "Echo" endpoint. +func DecodeEchoRequest(ctx context.Context, v any, md metadata.MD) (any, error) { + var ( + message *apipb.EchoRequest + ok bool + ) + { + if message, ok = v.(*apipb.EchoRequest); !ok { + return nil, goagrpc.ErrInvalidType("api", "Echo", "*apipb.EchoRequest", v) + } + } + var payload *api.EchoPayload + { + payload = NewEchoPayload(message) + } + 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..dcbceb8 --- /dev/null +++ b/app/api/v1/gen/grpc/api/server/server.go @@ -0,0 +1,124 @@ +// 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 { + CounterGetH goagrpc.UnaryHandler + CounterIncrementH goagrpc.UnaryHandler + EchoH 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{ + CounterGetH: NewCounterGetHandler(e.CounterGet, uh), + CounterIncrementH: NewCounterIncrementHandler(e.CounterIncrement, uh), + EchoH: NewEchoHandler(e.Echo, uh), + } +} + +// 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.PermissionDenied, err, goagrpc.NewErrorResponse(err)) + case "existing_increment_request": + return nil, goagrpc.NewStatusError(codes.AlreadyExists, 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 "unauthorized": + return nil, goagrpc.NewStatusError(codes.PermissionDenied, err, goagrpc.NewErrorResponse(err)) + case "existing_increment_request": + return nil, goagrpc.NewStatusError(codes.AlreadyExists, err, goagrpc.NewErrorResponse(err)) + } + } + return nil, goagrpc.EncodeError(err) + } + return resp.(*apipb.CounterIncrementResponse), nil +} + +// NewEchoHandler creates a gRPC handler which serves the "api" 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 apipb.APIServer interface. +func (s *Server) Echo(ctx context.Context, message *apipb.EchoRequest) (*apipb.EchoResponse, error) { + ctx = context.WithValue(ctx, goa.MethodKey, "Echo") + ctx = context.WithValue(ctx, goa.ServiceKey, "api") + resp, err := s.EchoH.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.PermissionDenied, err, goagrpc.NewErrorResponse(err)) + case "existing_increment_request": + return nil, goagrpc.NewStatusError(codes.AlreadyExists, err, goagrpc.NewErrorResponse(err)) + } + } + return nil, goagrpc.EncodeError(err) + } + return resp.(*apipb.EchoResponse), 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..9b23b3a --- /dev/null +++ b/app/api/v1/gen/grpc/api/server/types.go @@ -0,0 +1,65 @@ +// 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" +) + +// 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(message *apipb.CounterIncrementRequest) *api.CounterIncrementPayload { + v := &api.CounterIncrementPayload{ + User: message.User, + } + 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 +} + +// NewEchoPayload builds the payload of the "Echo" endpoint of the "api" +// service from the gRPC request type. +func NewEchoPayload(message *apipb.EchoRequest) *api.EchoPayload { + v := &api.EchoPayload{ + Text: message.Text, + } + return v +} + +// NewProtoEchoResponse builds the gRPC response type from the result of the +// "Echo" endpoint of the "api" service. +func NewProtoEchoResponse(result *api.EchoResult) *apipb.EchoResponse { + message := &apipb.EchoResponse{ + Text: result.Text, + } + return message +} 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..232bfc0 --- /dev/null +++ b/app/api/v1/gen/grpc/cli/countup/cli.go @@ -0,0 +1,186 @@ +// 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" + 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 (counter-get|counter-increment|echo) +` +} + +// UsageExamples produces an example of a valid invocation of the CLI tool. +func UsageExamples() string { + return os.Args[0] + ` api counter-get` + "\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) + + apiCounterGetFlags = flag.NewFlagSet("counter-get", flag.ExitOnError) + + apiCounterIncrementFlags = flag.NewFlagSet("counter-increment", flag.ExitOnError) + apiCounterIncrementMessageFlag = apiCounterIncrementFlags.String("message", "", "") + + apiEchoFlags = flag.NewFlagSet("echo", flag.ExitOnError) + apiEchoMessageFlag = apiEchoFlags.String("message", "", "") + ) + apiFlags.Usage = apiUsage + apiCounterGetFlags.Usage = apiCounterGetUsage + apiCounterIncrementFlags.Usage = apiCounterIncrementUsage + apiEchoFlags.Usage = apiEchoUsage + + 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 + 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 "counter-get": + epf = apiCounterGetFlags + + case "counter-increment": + epf = apiCounterIncrementFlags + + case "echo": + epf = apiEchoFlags + + } + + } + } + 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 "counter-get": + endpoint = c.CounterGet() + case "counter-increment": + endpoint = c.CounterIncrement() + data, err = apic.BuildCounterIncrementPayload(*apiCounterIncrementMessageFlag) + case "echo": + endpoint = c.Echo() + data, err = apic.BuildEchoPayload(*apiEchoMessageFlag) + } + } + } + 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: + counter-get: CounterGet implements CounterGet. + counter-increment: CounterIncrement implements CounterIncrement. + echo: Echo implements Echo. + +Additional help: + %[1]s api COMMAND --help +`, 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 -message JSON + +CounterIncrement implements CounterIncrement. + -message JSON: + +Example: + %[1]s api counter-increment --message '{ + "user": "Non accusantium eos culpa autem illum architecto." + }' +`, os.Args[0]) +} + +func apiEchoUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] api echo -message JSON + +Echo implements Echo. + -message JSON: + +Example: + %[1]s api echo --message '{ + "text": "Cupiditate tempore harum error iste ipsam natus." + }' +`, os.Args[0]) +} 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..58489ba --- /dev/null +++ b/app/api/v1/gen/http/api/client/cli.go @@ -0,0 +1,50 @@ +// 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" +) + +// BuildCounterIncrementPayload builds the payload for the api CounterIncrement +// endpoint from CLI flags. +func BuildCounterIncrementPayload(apiCounterIncrementBody string) (*api.CounterIncrementPayload, error) { + var err error + var body CounterIncrementRequestBody + { + err = json.Unmarshal([]byte(apiCounterIncrementBody), &body) + if err != nil { + return nil, fmt.Errorf("invalid JSON for body, \nerror: %s, \nexample of valid JSON:\n%s", err, "'{\n \"user\": \"Nihil doloribus et sed sequi consequatur.\"\n }'") + } + } + v := &api.CounterIncrementPayload{ + User: body.User, + } + + return v, nil +} + +// BuildEchoPayload builds the payload for the api Echo endpoint from CLI flags. +func BuildEchoPayload(apiEchoBody string) (*api.EchoPayload, error) { + var err error + var body EchoRequestBody + { + err = json.Unmarshal([]byte(apiEchoBody), &body) + if err != nil { + return nil, fmt.Errorf("invalid JSON for body, \nerror: %s, \nexample of valid JSON:\n%s", err, "'{\n \"text\": \"Vel omnis quo sit.\"\n }'") + } + } + v := &api.EchoPayload{ + Text: body.Text, + } + + 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..6de9935 --- /dev/null +++ b/app/api/v1/gen/http/api/client/client.go @@ -0,0 +1,127 @@ +// 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 { + // 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 + + // 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 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{ + CounterGetDoer: doer, + CounterIncrementDoer: doer, + EchoDoer: doer, + RestoreResponseBody: restoreBody, + scheme: scheme, + host: host, + decoder: dec, + encoder: enc, + } +} + +// 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) + } +} + +// Echo returns an endpoint that makes HTTP requests to the api 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("api", "Echo", 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..dd511c9 --- /dev/null +++ b/app/api/v1/gen/http/api/client/encode_decode.go @@ -0,0 +1,317 @@ +// 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" + + 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" +) + +// 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 +// - "existing_increment_request" (type *goa.ServiceError): http.StatusTooManyRequests +// - 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.StatusTooManyRequests: + var ( + body CounterGetExistingIncrementRequestResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("api", "CounterGet", err) + } + err = ValidateCounterGetExistingIncrementRequestResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("api", "CounterGet", err) + } + return nil, NewCounterGetExistingIncrementRequest(&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) + } + body := NewCounterIncrementRequestBody(p) + if err := encoder(req).Encode(&body); err != nil { + return goahttp.ErrEncodingError("api", "CounterIncrement", err) + } + 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: +// - "unauthorized" (type *goa.ServiceError): http.StatusUnauthorized +// - "existing_increment_request" (type *goa.ServiceError): http.StatusTooManyRequests +// - 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.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.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) + default: + body, _ := io.ReadAll(resp.Body) + return nil, goahttp.ErrInvalidResponse("api", "CounterIncrement", resp.StatusCode, string(body)) + } + } +} + +// BuildEchoRequest instantiates a HTTP request object with method and path set +// to call the "api" 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: EchoAPIPath()} + req, err := http.NewRequest("POST", u.String(), nil) + if err != nil { + return nil, goahttp.ErrInvalidURL("api", "Echo", u.String(), err) + } + if ctx != nil { + req = req.WithContext(ctx) + } + + return req, nil +} + +// EncodeEchoRequest returns an encoder for requests sent to the api 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.(*api.EchoPayload) + if !ok { + return goahttp.ErrInvalidType("api", "Echo", "*api.EchoPayload", v) + } + body := NewEchoRequestBody(p) + if err := encoder(req).Encode(&body); err != nil { + return goahttp.ErrEncodingError("api", "Echo", err) + } + return nil + } +} + +// DecodeEchoResponse returns a decoder for responses returned by the api Echo +// endpoint. restoreBody controls whether the response body should be restored +// after having been read. +// DecodeEchoResponse may return the following errors: +// - "unauthorized" (type *goa.ServiceError): http.StatusUnauthorized +// - "existing_increment_request" (type *goa.ServiceError): http.StatusTooManyRequests +// - error: internal error +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("api", "Echo", err) + } + err = ValidateEchoResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("api", "Echo", err) + } + res := NewEchoResultOK(&body) + return res, nil + case http.StatusUnauthorized: + var ( + body EchoUnauthorizedResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("api", "Echo", err) + } + err = ValidateEchoUnauthorizedResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("api", "Echo", err) + } + return nil, NewEchoUnauthorized(&body) + case http.StatusTooManyRequests: + var ( + body EchoExistingIncrementRequestResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("api", "Echo", err) + } + err = ValidateEchoExistingIncrementRequestResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("api", "Echo", err) + } + return nil, NewEchoExistingIncrementRequest(&body) + default: + body, _ := io.ReadAll(resp.Body) + return nil, goahttp.ErrInvalidResponse("api", "Echo", 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..0f1045e --- /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 + +// CounterGetAPIPath returns the URL path to the api service CounterGet HTTP endpoint. +func CounterGetAPIPath() string { + return "/counter" +} + +// CounterIncrementAPIPath returns the URL path to the api service CounterIncrement HTTP endpoint. +func CounterIncrementAPIPath() string { + return "/counter/inc" +} + +// EchoAPIPath returns the URL path to the api service Echo HTTP endpoint. +func EchoAPIPath() string { + return "/echo" +} 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..a92300a --- /dev/null +++ b/app/api/v1/gen/http/api/client/types.go @@ -0,0 +1,457 @@ +// 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" +) + +// CounterIncrementRequestBody is the type of the "api" service +// "CounterIncrement" endpoint HTTP request body. +type CounterIncrementRequestBody struct { + User string `form:"user" json:"user" xml:"user"` +} + +// EchoRequestBody is the type of the "api" service "Echo" endpoint HTTP +// request body. +type EchoRequestBody struct { + Text string `form:"text" json:"text" xml:"text"` +} + +// 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"` +} + +// EchoResponseBody is the type of the "api" service "Echo" endpoint HTTP +// response body. +type EchoResponseBody struct { + Text *string `form:"text,omitempty" json:"text,omitempty" xml:"text,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"` +} + +// CounterGetExistingIncrementRequestResponseBody is the type of the "api" +// service "CounterGet" endpoint HTTP response body for the +// "existing_increment_request" error. +type CounterGetExistingIncrementRequestResponseBody 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"` +} + +// 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"` +} + +// EchoUnauthorizedResponseBody is the type of the "api" service "Echo" +// endpoint HTTP response body for the "unauthorized" error. +type EchoUnauthorizedResponseBody 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"` +} + +// EchoExistingIncrementRequestResponseBody is the type of the "api" service +// "Echo" endpoint HTTP response body for the "existing_increment_request" +// error. +type EchoExistingIncrementRequestResponseBody 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"` +} + +// NewCounterIncrementRequestBody builds the HTTP request body from the payload +// of the "CounterIncrement" endpoint of the "api" service. +func NewCounterIncrementRequestBody(p *api.CounterIncrementPayload) *CounterIncrementRequestBody { + body := &CounterIncrementRequestBody{ + User: p.User, + } + return body +} + +// NewEchoRequestBody builds the HTTP request body from the payload of the +// "Echo" endpoint of the "api" service. +func NewEchoRequestBody(p *api.EchoPayload) *EchoRequestBody { + body := &EchoRequestBody{ + Text: p.Text, + } + return body +} + +// 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 +} + +// NewCounterGetExistingIncrementRequest builds a api service CounterGet +// endpoint existing_increment_request error. +func NewCounterGetExistingIncrementRequest(body *CounterGetExistingIncrementRequestResponseBody) *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 +} + +// 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 +} + +// 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 +} + +// NewEchoResultOK builds a "api" service "Echo" endpoint result from a HTTP +// "OK" response. +func NewEchoResultOK(body *EchoResponseBody) *api.EchoResult { + v := &api.EchoResult{ + Text: *body.Text, + } + + return v +} + +// NewEchoUnauthorized builds a api service Echo endpoint unauthorized error. +func NewEchoUnauthorized(body *EchoUnauthorizedResponseBody) *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 +} + +// NewEchoExistingIncrementRequest builds a api service Echo endpoint +// existing_increment_request error. +func NewEchoExistingIncrementRequest(body *EchoExistingIncrementRequestResponseBody) *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 +} + +// 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 +} + +// 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 +} + +// ValidateCounterGetExistingIncrementRequestResponseBody runs the validations +// defined on CounterGet_existing_increment_request_Response_Body +func ValidateCounterGetExistingIncrementRequestResponseBody(body *CounterGetExistingIncrementRequestResponseBody) (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 +} + +// 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 +} + +// ValidateEchoUnauthorizedResponseBody runs the validations defined on +// Echo_unauthorized_Response_Body +func ValidateEchoUnauthorizedResponseBody(body *EchoUnauthorizedResponseBody) (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 +} + +// ValidateEchoExistingIncrementRequestResponseBody runs the validations +// defined on Echo_existing_increment_request_Response_Body +func ValidateEchoExistingIncrementRequestResponseBody(body *EchoExistingIncrementRequestResponseBody) (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..6e7545a --- /dev/null +++ b/app/api/v1/gen/http/api/server/encode_decode.go @@ -0,0 +1,240 @@ +// 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" + + 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" +) + +// 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 "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 = NewCounterGetExistingIncrementRequestResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusTooManyRequests) + 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 ( + body CounterIncrementRequestBody + 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 = ValidateCounterIncrementRequestBody(&body) + if err != nil { + return nil, err + } + payload := NewCounterIncrementPayload(&body) + + 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 "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 "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) + default: + return encodeError(ctx, w, v) + } + } +} + +// EncodeEchoResponse returns an encoder for responses returned by the api 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.(*api.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 api 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 + } +} + +// EncodeEchoError returns an encoder for errors returned by the Echo api +// endpoint. +func EncodeEchoError(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 = NewEchoUnauthorizedResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusUnauthorized) + return enc.Encode(body) + 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 = NewEchoExistingIncrementRequestResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusTooManyRequests) + 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..e1aae0e --- /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 + +// CounterGetAPIPath returns the URL path to the api service CounterGet HTTP endpoint. +func CounterGetAPIPath() string { + return "/counter" +} + +// CounterIncrementAPIPath returns the URL path to the api service CounterIncrement HTTP endpoint. +func CounterIncrementAPIPath() string { + return "/counter/inc" +} + +// EchoAPIPath returns the URL path to the api service Echo HTTP endpoint. +func EchoAPIPath() string { + return "/echo" +} 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..bb690b0 --- /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 + CounterGet http.Handler + CounterIncrement http.Handler + 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 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{ + {"CounterGet", "GET", "/counter"}, + {"CounterIncrement", "POST", "/counter/inc"}, + {"Echo", "POST", "/echo"}, + {"Serve gen/http/openapi3.json", "GET", "/openapi.json"}, + }, + CounterGet: NewCounterGetHandler(e.CounterGet, mux, decoder, encoder, errhandler, formatter), + CounterIncrement: NewCounterIncrementHandler(e.CounterIncrement, mux, decoder, encoder, errhandler, formatter), + 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 "api" } + +// Use wraps the server handlers with the given middleware. +func (s *Server) Use(m func(http.Handler) http.Handler) { + s.CounterGet = m(s.CounterGet) + s.CounterIncrement = m(s.CounterIncrement) + s.Echo = m(s.Echo) +} + +// 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) { + MountCounterGetHandler(mux, h.CounterGet) + MountCounterIncrementHandler(mux, h.CounterIncrement) + MountEchoHandler(mux, h.Echo) + MountGenHTTPOpenapi3JSON(mux, h.GenHTTPOpenapi3JSON) +} + +// Mount configures the mux to serve the api endpoints. +func (s *Server) Mount(mux goahttp.Muxer) { + Mount(mux, s) +} + +// 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", "/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", "/counter/inc", 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) + } + }) +} + +// MountEchoHandler configures the mux to serve the "api" 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 "api" 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 = EncodeEchoError(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, "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 +// "/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/api/server/types.go b/app/api/v1/gen/http/api/server/types.go new file mode 100644 index 0000000..c517257 --- /dev/null +++ b/app/api/v1/gen/http/api/server/types.go @@ -0,0 +1,315 @@ +// 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" +) + +// CounterIncrementRequestBody is the type of the "api" service +// "CounterIncrement" endpoint HTTP request body. +type CounterIncrementRequestBody struct { + User *string `form:"user,omitempty" json:"user,omitempty" xml:"user,omitempty"` +} + +// EchoRequestBody is the type of the "api" service "Echo" endpoint HTTP +// request body. +type EchoRequestBody struct { + Text *string `form:"text,omitempty" json:"text,omitempty" xml:"text,omitempty"` +} + +// 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"` +} + +// EchoResponseBody is the type of the "api" service "Echo" endpoint HTTP +// response body. +type EchoResponseBody struct { + Text string `form:"text" json:"text" xml:"text"` +} + +// 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"` +} + +// CounterGetExistingIncrementRequestResponseBody is the type of the "api" +// service "CounterGet" endpoint HTTP response body for the +// "existing_increment_request" error. +type CounterGetExistingIncrementRequestResponseBody 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"` +} + +// 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"` +} + +// EchoUnauthorizedResponseBody is the type of the "api" service "Echo" +// endpoint HTTP response body for the "unauthorized" error. +type EchoUnauthorizedResponseBody 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"` +} + +// EchoExistingIncrementRequestResponseBody is the type of the "api" service +// "Echo" endpoint HTTP response body for the "existing_increment_request" +// error. +type EchoExistingIncrementRequestResponseBody 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"` +} + +// 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 +} + +// NewEchoResponseBody builds the HTTP response body from the result of the +// "Echo" endpoint of the "api" service. +func NewEchoResponseBody(res *api.EchoResult) *EchoResponseBody { + body := &EchoResponseBody{ + Text: res.Text, + } + 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 +} + +// NewCounterGetExistingIncrementRequestResponseBody builds the HTTP response +// body from the result of the "CounterGet" endpoint of the "api" service. +func NewCounterGetExistingIncrementRequestResponseBody(res *goa.ServiceError) *CounterGetExistingIncrementRequestResponseBody { + body := &CounterGetExistingIncrementRequestResponseBody{ + 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 +} + +// 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 +} + +// NewEchoUnauthorizedResponseBody builds the HTTP response body from the +// result of the "Echo" endpoint of the "api" service. +func NewEchoUnauthorizedResponseBody(res *goa.ServiceError) *EchoUnauthorizedResponseBody { + body := &EchoUnauthorizedResponseBody{ + Name: res.Name, + ID: res.ID, + Message: res.Message, + Temporary: res.Temporary, + Timeout: res.Timeout, + Fault: res.Fault, + } + return body +} + +// NewEchoExistingIncrementRequestResponseBody builds the HTTP response body +// from the result of the "Echo" endpoint of the "api" service. +func NewEchoExistingIncrementRequestResponseBody(res *goa.ServiceError) *EchoExistingIncrementRequestResponseBody { + body := &EchoExistingIncrementRequestResponseBody{ + Name: res.Name, + ID: res.ID, + Message: res.Message, + Temporary: res.Temporary, + Timeout: res.Timeout, + Fault: res.Fault, + } + return body +} + +// NewCounterIncrementPayload builds a api service CounterIncrement endpoint +// payload. +func NewCounterIncrementPayload(body *CounterIncrementRequestBody) *api.CounterIncrementPayload { + v := &api.CounterIncrementPayload{ + User: *body.User, + } + + return v +} + +// NewEchoPayload builds a api service Echo endpoint payload. +func NewEchoPayload(body *EchoRequestBody) *api.EchoPayload { + v := &api.EchoPayload{ + Text: *body.Text, + } + + return v +} + +// ValidateCounterIncrementRequestBody runs the validations defined on +// CounterIncrementRequestBody +func ValidateCounterIncrementRequestBody(body *CounterIncrementRequestBody) (err error) { + if body.User == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("user", "body")) + } + return +} + +// 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/cli/countup/cli.go b/app/api/v1/gen/http/cli/countup/cli.go new file mode 100644 index 0000000..fb50aaa --- /dev/null +++ b/app/api/v1/gen/http/cli/countup/cli.go @@ -0,0 +1,262 @@ +// 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" + 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 (counter-get|counter-increment|echo) +web (index|another) +` +} + +// UsageExamples produces an example of a valid invocation of the CLI tool. +func UsageExamples() string { + return os.Args[0] + ` api counter-get` + "\n" + + os.Args[0] + ` web index` + "\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) + + apiCounterGetFlags = flag.NewFlagSet("counter-get", flag.ExitOnError) + + apiCounterIncrementFlags = flag.NewFlagSet("counter-increment", flag.ExitOnError) + apiCounterIncrementBodyFlag = apiCounterIncrementFlags.String("body", "REQUIRED", "") + + apiEchoFlags = flag.NewFlagSet("echo", flag.ExitOnError) + apiEchoBodyFlag = apiEchoFlags.String("body", "REQUIRED", "") + + webFlags = flag.NewFlagSet("web", flag.ContinueOnError) + + webIndexFlags = flag.NewFlagSet("index", flag.ExitOnError) + + webAnotherFlags = flag.NewFlagSet("another", flag.ExitOnError) + ) + apiFlags.Usage = apiUsage + apiCounterGetFlags.Usage = apiCounterGetUsage + apiCounterIncrementFlags.Usage = apiCounterIncrementUsage + apiEchoFlags.Usage = apiEchoUsage + + webFlags.Usage = webUsage + webIndexFlags.Usage = webIndexUsage + webAnotherFlags.Usage = webAnotherUsage + + 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 + 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 "counter-get": + epf = apiCounterGetFlags + + case "counter-increment": + epf = apiCounterIncrementFlags + + case "echo": + epf = apiEchoFlags + + } + + case "web": + switch epn { + case "index": + epf = webIndexFlags + + case "another": + epf = webAnotherFlags + + } + + } + } + 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 "counter-get": + endpoint = c.CounterGet() + case "counter-increment": + endpoint = c.CounterIncrement() + data, err = apic.BuildCounterIncrementPayload(*apiCounterIncrementBodyFlag) + case "echo": + endpoint = c.Echo() + data, err = apic.BuildEchoPayload(*apiEchoBodyFlag) + } + case "web": + c := webc.NewClient(scheme, host, doer, enc, dec, restore) + switch epn { + case "index": + endpoint = c.Index() + case "another": + endpoint = c.Another() + } + } + } + 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: + counter-get: CounterGet implements CounterGet. + counter-increment: CounterIncrement implements CounterIncrement. + echo: Echo implements Echo. + +Additional help: + %[1]s api COMMAND --help +`, 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 -body JSON + +CounterIncrement implements CounterIncrement. + -body JSON: + +Example: + %[1]s api counter-increment --body '{ + "user": "Nihil doloribus et sed sequi consequatur." + }' +`, os.Args[0]) +} + +func apiEchoUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] api echo -body JSON + +Echo implements Echo. + -body JSON: + +Example: + %[1]s api echo --body '{ + "text": "Vel omnis quo sit." + }' +`, 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. + +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]) +} diff --git a/app/api/v1/gen/http/openapi.json b/app/api/v1/gen/http/openapi.json new file mode 100644 index 0000000..3cfe53a --- /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"}}},"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"}}},"schemes":["http"]}},"/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"}},"429":{"description":"Too Many Requests response.","schema":{"$ref":"#/definitions/APICounterGetExistingIncrementRequestResponseBody"}}},"schemes":["http"]}},"/counter/inc":{"post":{"tags":["api"],"summary":"CounterIncrement api","operationId":"api#CounterIncrement","parameters":[{"name":"CounterIncrementRequestBody","in":"body","required":true,"schema":{"$ref":"#/definitions/APICounterIncrementRequestBody","required":["user"]}}],"responses":{"202":{"description":"Accepted response.","schema":{"$ref":"#/definitions/CounterInfo"}},"401":{"description":"Unauthorized response.","schema":{"$ref":"#/definitions/APICounterIncrementUnauthorizedResponseBody"}},"429":{"description":"Too Many Requests response.","schema":{"$ref":"#/definitions/APICounterIncrementExistingIncrementRequestResponseBody"}}},"schemes":["http"]}},"/echo":{"post":{"tags":["api"],"summary":"Echo api","operationId":"api#Echo","parameters":[{"name":"EchoRequestBody","in":"body","required":true,"schema":{"$ref":"#/definitions/APIEchoRequestBody","required":["text"]}}],"responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/APIEchoResponseBody","required":["text"]}},"401":{"description":"Unauthorized response.","schema":{"$ref":"#/definitions/APIEchoUnauthorizedResponseBody"}},"429":{"description":"Too Many Requests response.","schema":{"$ref":"#/definitions/APIEchoExistingIncrementRequestResponseBody"}}},"schemes":["http"]}},"/openapi.json":{"get":{"tags":["api"],"summary":"Download gen/http/openapi3.json","operationId":"api#/openapi.json","responses":{"200":{"description":"File downloaded","schema":{"type":"file"}}},"schemes":["http"]}},"/static/{path}":{"get":{"tags":["web"],"summary":"Download static/","operationId":"web#/static/{*path}","parameters":[{"name":"path","in":"path","description":"Relative file path","required":true,"type":"string"}],"responses":{"200":{"description":"File downloaded","schema":{"type":"file"}},"404":{"description":"File not found","schema":{"$ref":"#/definitions/Error"}}},"schemes":["http"]}}},"definitions":{"APICounterGetExistingIncrementRequestResponseBody":{"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":"CounterGet_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":false,"timeout":false},"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":false},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":true}},"description":"CounterGet_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"]},"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":false,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":false,"timeout":true},"required":["name","id","message","temporary","timeout","fault"]},"APICounterIncrementRequestBody":{"title":"APICounterIncrementRequestBody","type":"object","properties":{"user":{"type":"string","example":"Non perspiciatis eum dicta sit."}},"example":{"user":"Odio tenetur temporibus."},"required":["user"]},"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":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":"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":false},"required":["name","id","message","temporary","timeout","fault"]},"APIEchoExistingIncrementRequestResponseBody":{"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":"Echo_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":false,"timeout":false},"required":["name","id","message","temporary","timeout","fault"]},"APIEchoRequestBody":{"title":"APIEchoRequestBody","type":"object","properties":{"text":{"type":"string","example":"Deleniti necessitatibus numquam perspiciatis quia ipsa quam."}},"example":{"text":"Nemo dolorem ullam magnam."},"required":["text"]},"APIEchoResponseBody":{"title":"APIEchoResponseBody","type":"object","properties":{"text":{"type":"string","example":"Praesentium qui debitis."}},"example":{"text":"Odit cum blanditiis ut."},"required":["text"]},"APIEchoUnauthorizedResponseBody":{"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":false}},"description":"Echo_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":1315490442,"format":"int32"},"last_increment_at":{"type":"string","example":"Eligendi quisquam."},"last_increment_by":{"type":"string","example":"Vero molestiae."},"next_finalize_at":{"type":"string","example":"Aut in dolor eum consequatur."}},"description":"CounterGetResponseBody result type (default view)","example":{"count":1162746042,"last_increment_at":"Tempore asperiores.","last_increment_by":"Tempora repellendus.","next_finalize_at":"Quae voluptatibus dolor fugit quia."},"required":["count","last_increment_by","last_increment_at","next_finalize_at"]},"Error":{"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":"Error response 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"]}}} \ 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..9ccd634 --- /dev/null +++ b/app/api/v1/gen/http/openapi.yaml @@ -0,0 +1,527 @@ +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 + 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 + schemes: + - http + /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' + "429": + description: Too Many Requests response. + schema: + $ref: '#/definitions/APICounterGetExistingIncrementRequestResponseBody' + schemes: + - http + /counter/inc: + post: + tags: + - api + summary: CounterIncrement api + operationId: api#CounterIncrement + parameters: + - name: CounterIncrementRequestBody + in: body + required: true + schema: + $ref: '#/definitions/APICounterIncrementRequestBody' + required: + - user + responses: + "202": + description: Accepted response. + schema: + $ref: '#/definitions/CounterInfo' + "401": + description: Unauthorized response. + schema: + $ref: '#/definitions/APICounterIncrementUnauthorizedResponseBody' + "429": + description: Too Many Requests response. + schema: + $ref: '#/definitions/APICounterIncrementExistingIncrementRequestResponseBody' + schemes: + - http + /echo: + post: + tags: + - api + summary: Echo api + operationId: api#Echo + parameters: + - name: EchoRequestBody + in: body + required: true + schema: + $ref: '#/definitions/APIEchoRequestBody' + required: + - text + responses: + "200": + description: OK response. + schema: + $ref: '#/definitions/APIEchoResponseBody' + required: + - text + "401": + description: Unauthorized response. + schema: + $ref: '#/definitions/APIEchoUnauthorizedResponseBody' + "429": + description: Too Many Requests response. + schema: + $ref: '#/definitions/APIEchoExistingIncrementRequestResponseBody' + schemes: + - http + /openapi.json: + get: + tags: + - api + summary: Download gen/http/openapi3.json + operationId: api#/openapi.json + responses: + "200": + description: File downloaded + schema: + type: file + schemes: + - http + /static/{path}: + get: + tags: + - web + summary: Download static/ + operationId: web#/static/{*path} + parameters: + - name: path + in: path + description: Relative file path + required: true + type: string + responses: + "200": + description: File downloaded + schema: + type: file + "404": + description: File not found + schema: + $ref: '#/definitions/Error' + schemes: + - http +definitions: + APICounterGetExistingIncrementRequestResponseBody: + 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: CounterGet_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: false + timeout: false + 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: false + timeout: + type: boolean + description: Is the error a timeout? + example: true + description: CounterGet_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 + 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: false + id: 123abc + message: parameter 'p' must be an integer + name: bad_request + temporary: false + timeout: true + required: + - name + - id + - message + - temporary + - timeout + - fault + APICounterIncrementRequestBody: + title: APICounterIncrementRequestBody + type: object + properties: + user: + type: string + example: Non perspiciatis eum dicta sit. + example: + user: Odio tenetur temporibus. + required: + - user + 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: 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: 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: false + required: + - name + - id + - message + - temporary + - timeout + - fault + APIEchoExistingIncrementRequestResponseBody: + 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: Echo_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: false + timeout: false + required: + - name + - id + - message + - temporary + - timeout + - fault + APIEchoRequestBody: + title: APIEchoRequestBody + type: object + properties: + text: + type: string + example: Deleniti necessitatibus numquam perspiciatis quia ipsa quam. + example: + text: Nemo dolorem ullam magnam. + required: + - text + APIEchoResponseBody: + title: APIEchoResponseBody + type: object + properties: + text: + type: string + example: Praesentium qui debitis. + example: + text: Odit cum blanditiis ut. + required: + - text + APIEchoUnauthorizedResponseBody: + 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: false + description: Echo_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: 1315490442 + format: int32 + last_increment_at: + type: string + example: Eligendi quisquam. + last_increment_by: + type: string + example: Vero molestiae. + next_finalize_at: + type: string + example: Aut in dolor eum consequatur. + description: CounterGetResponseBody result type (default view) + example: + count: 1162746042 + last_increment_at: Tempore asperiores. + last_increment_by: Tempora repellendus. + next_finalize_at: Quae voluptatibus dolor fugit quia. + required: + - count + - last_increment_by + - last_increment_at + - next_finalize_at + Error: + 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: Error response 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 diff --git a/app/api/v1/gen/http/openapi3.json b/app/api/v1/gen/http/openapi3.json new file mode 100644 index 0000000..0787a4c --- /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":"TmVtbyBtYWduaSBkdWNpbXVzIGltcGVkaXQu","format":"binary"},"example":"TW9sbGl0aWEgZGVsZW5pdGkgZXhwZWRpdGEgYWNjdXNhbnRpdW0u"}}}}}},"/another":{"get":{"tags":["web"],"summary":"another web","operationId":"web#another","responses":{"200":{"description":"OK response.","content":{"text/html":{"schema":{"type":"string","example":"TnVtcXVhbSBkb2xvcmlidXMgc3VzY2lwaXQu","format":"binary"},"example":"SW52ZW50b3JlIGxhYm9yZSBlcnJvciBhbmltaS4="}}}}}},"/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":1425094436,"last_increment_at":"Iste et distinctio accusantium.","last_increment_by":"Sequi ipsa aliquam esse.","next_finalize_at":"A non."}}}},"401":{"description":"unauthorized: Unauthorized 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"}}}}}}},"/counter/inc":{"post":{"tags":["api"],"summary":"CounterIncrement api","operationId":"api#CounterIncrement","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CounterIncrementRequestBody"},"example":{"user":"Nihil doloribus et sed sequi consequatur."}}}},"responses":{"202":{"description":"Accepted response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CounterInfo"},"example":{"count":278214526,"last_increment_at":"Inventore accusantium.","last_increment_by":"Laborum vel mollitia aut.","next_finalize_at":"Voluptas ut eius."}}}},"401":{"description":"unauthorized: Unauthorized 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"}}}}}}},"/echo":{"post":{"tags":["api"],"summary":"Echo api","operationId":"api#Echo","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EchoRequestBody"},"example":{"text":"Vel omnis quo sit."}}}},"responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EchoRequestBody"},"example":{"text":"Distinctio illo."}}}},"401":{"description":"unauthorized: Unauthorized 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"}}}}}}},"/openapi.json":{"get":{"tags":["api"],"summary":"Download gen/http/openapi3.json","operationId":"api#/openapi.json","responses":{"200":{"description":"File downloaded"}}}},"/static/{*path}":{"get":{"tags":["web"],"summary":"Download static/","operationId":"web#/static/{*path}","parameters":[{"name":"path","in":"path","description":"Relative file path","required":true}],"responses":{"200":{"description":"File not found"},"404":{"description":"File not found"}}}}},"components":{"schemas":{"CounterIncrementRequestBody":{"type":"object","properties":{"user":{"type":"string","example":"Omnis debitis."}},"example":{"user":"Eos minima dolorem id sunt voluptates voluptas."},"required":["user"]},"CounterInfo":{"type":"object","properties":{"count":{"type":"integer","example":495281550,"format":"int32"},"last_increment_at":{"type":"string","example":"Aut tenetur eos."},"last_increment_by":{"type":"string","example":"Consectetur odio."},"next_finalize_at":{"type":"string","example":"Laborum et veniam et illum quaerat et."}},"example":{"count":1773148578,"last_increment_at":"Voluptatum omnis possimus saepe deleniti.","last_increment_by":"Corporis est sunt voluptatem reprehenderit neque modi.","next_finalize_at":"Rerum facere veritatis."},"required":["count","last_increment_by","last_increment_at","next_finalize_at"]},"EchoRequestBody":{"type":"object","properties":{"text":{"type":"string","example":"Cumque maxime dolore hic laboriosam."}},"example":{"text":"Aut recusandae cum."},"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":false}},"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"]}}},"tags":[{"name":"api"},{"name":"web"}]} \ 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..659f61e --- /dev/null +++ b/app/api/v1/gen/http/openapi3.yaml @@ -0,0 +1,383 @@ +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: + - 78 + - 101 + - 109 + - 111 + - 32 + - 109 + - 97 + - 103 + - 110 + - 105 + - 32 + - 100 + - 117 + - 99 + - 105 + - 109 + - 117 + - 115 + - 32 + - 105 + - 109 + - 112 + - 101 + - 100 + - 105 + - 116 + - 46 + format: binary + example: + - 77 + - 111 + - 108 + - 108 + - 105 + - 116 + - 105 + - 97 + - 32 + - 100 + - 101 + - 108 + - 101 + - 110 + - 105 + - 116 + - 105 + - 32 + - 101 + - 120 + - 112 + - 101 + - 100 + - 105 + - 116 + - 97 + - 32 + - 97 + - 99 + - 99 + - 117 + - 115 + - 97 + - 110 + - 116 + - 105 + - 117 + - 109 + - 46 + /another: + get: + tags: + - web + summary: another web + operationId: web#another + responses: + "200": + description: OK response. + content: + text/html: + schema: + type: string + example: + - 78 + - 117 + - 109 + - 113 + - 117 + - 97 + - 109 + - 32 + - 100 + - 111 + - 108 + - 111 + - 114 + - 105 + - 98 + - 117 + - 115 + - 32 + - 115 + - 117 + - 115 + - 99 + - 105 + - 112 + - 105 + - 116 + - 46 + format: binary + example: + - 73 + - 110 + - 118 + - 101 + - 110 + - 116 + - 111 + - 114 + - 101 + - 32 + - 108 + - 97 + - 98 + - 111 + - 114 + - 101 + - 32 + - 101 + - 114 + - 114 + - 111 + - 114 + - 32 + - 97 + - 110 + - 105 + - 109 + - 105 + - 46 + /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: 1425094436 + last_increment_at: Iste et distinctio accusantium. + last_increment_by: Sequi ipsa aliquam esse. + next_finalize_at: A non. + "401": + description: 'unauthorized: Unauthorized 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' + /counter/inc: + post: + tags: + - api + summary: CounterIncrement api + operationId: api#CounterIncrement + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CounterIncrementRequestBody' + example: + user: Nihil doloribus et sed sequi consequatur. + responses: + "202": + description: Accepted response. + content: + application/json: + schema: + $ref: '#/components/schemas/CounterInfo' + example: + count: 278214526 + last_increment_at: Inventore accusantium. + last_increment_by: Laborum vel mollitia aut. + next_finalize_at: Voluptas ut eius. + "401": + description: 'unauthorized: Unauthorized 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' + /echo: + post: + tags: + - api + summary: Echo api + operationId: api#Echo + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EchoRequestBody' + example: + text: Vel omnis quo sit. + responses: + "200": + description: OK response. + content: + application/json: + schema: + $ref: '#/components/schemas/EchoRequestBody' + example: + text: Distinctio illo. + "401": + description: 'unauthorized: Unauthorized 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' + /openapi.json: + get: + tags: + - api + summary: Download gen/http/openapi3.json + operationId: api#/openapi.json + responses: + "200": + description: File downloaded + /static/{*path}: + get: + tags: + - web + summary: Download static/ + operationId: web#/static/{*path} + parameters: + - name: path + in: path + description: Relative file path + required: true + responses: + "200": + description: File not found + "404": + description: File not found +components: + schemas: + CounterIncrementRequestBody: + type: object + properties: + user: + type: string + example: Omnis debitis. + example: + user: Eos minima dolorem id sunt voluptates voluptas. + required: + - user + CounterInfo: + type: object + properties: + count: + type: integer + example: 495281550 + format: int32 + last_increment_at: + type: string + example: Aut tenetur eos. + last_increment_by: + type: string + example: Consectetur odio. + next_finalize_at: + type: string + example: Laborum et veniam et illum quaerat et. + example: + count: 1773148578 + last_increment_at: Voluptatum omnis possimus saepe deleniti. + last_increment_by: Corporis est sunt voluptatem reprehenderit neque modi. + next_finalize_at: Rerum facere veritatis. + required: + - count + - last_increment_by + - last_increment_at + - next_finalize_at + EchoRequestBody: + type: object + properties: + text: + type: string + example: Cumque maxime dolore hic laboriosam. + example: + text: Aut recusandae cum. + 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: false + 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 +tags: + - name: api + - name: web 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..fafe96a --- /dev/null +++ b/app/api/v1/gen/http/web/client/cli.go @@ -0,0 +1,8 @@ +// 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 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..df4b614 --- /dev/null +++ b/app/api/v1/gen/http/web/client/client.go @@ -0,0 +1,93 @@ +// 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 + + // 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, + 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) + } +} 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..ebee749 --- /dev/null +++ b/app/api/v1/gen/http/web/client/encode_decode.go @@ -0,0 +1,118 @@ +// 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" + + goahttp "goa.design/goa/v3/http" +) + +// 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. +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 + 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. +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 + default: + body, _ := io.ReadAll(resp.Body) + return nil, goahttp.ErrInvalidResponse("web", "another", 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..6ea65ce --- /dev/null +++ b/app/api/v1/gen/http/web/client/paths.go @@ -0,0 +1,18 @@ +// 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" +} 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..0f1ccd1 --- /dev/null +++ b/app/api/v1/gen/http/web/client/types.go @@ -0,0 +1,8 @@ +// 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 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..57b11ff --- /dev/null +++ b/app/api/v1/gen/http/web/server/encode_decode.go @@ -0,0 +1,41 @@ +// 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" + "net/http" + + goahttp "goa.design/goa/v3/http" +) + +// 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) + } +} + +// 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) + } +} 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..a72edb5 --- /dev/null +++ b/app/api/v1/gen/http/web/server/paths.go @@ -0,0 +1,18 @@ +// 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" +} 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..b1225c6 --- /dev/null +++ b/app/api/v1/gen/http/web/server/server.go @@ -0,0 +1,207 @@ +// 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 + 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"}, + {"Serve static/", "GET", "/static"}, + }, + Index: NewIndexHandler(e.Index, mux, decoder, encoder, errhandler, formatter), + Another: NewAnotherHandler(e.Another, 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) +} + +// 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) + 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 = 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, "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 = 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, "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) + } + }) +} + +// 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 { + } + 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) + mux.Handle("GET", "/static/{*path}", 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..573bc71 --- /dev/null +++ b/app/api/v1/gen/http/web/server/types.go @@ -0,0 +1,8 @@ +// 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 diff --git a/app/api/v1/gen/web/client.go b/app/api/v1/gen/web/client.go new file mode 100644 index 0000000..2a2f38a --- /dev/null +++ b/app/api/v1/gen/web/client.go @@ -0,0 +1,48 @@ +// 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 +} + +// NewClient initializes a "web" service client given the endpoints. +func NewClient(index, another goa.Endpoint) *Client { + return &Client{ + IndexEndpoint: index, + AnotherEndpoint: another, + } +} + +// Index calls the "index" endpoint of the "web" service. +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. +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 +} diff --git a/app/api/v1/gen/web/endpoints.go b/app/api/v1/gen/web/endpoints.go new file mode 100644 index 0000000..280b9d1 --- /dev/null +++ b/app/api/v1/gen/web/endpoints.go @@ -0,0 +1,50 @@ +// 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 +} + +// NewEndpoints wraps the methods of the "web" service with endpoints. +func NewEndpoints(s Service) *Endpoints { + return &Endpoints{ + Index: NewIndexEndpoint(s), + Another: NewAnotherEndpoint(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) +} + +// 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) + } +} diff --git a/app/api/v1/gen/web/service.go b/app/api/v1/gen/web/service.go new file mode 100644 index 0000000..6aaa0e1 --- /dev/null +++ b/app/api/v1/gen/web/service.go @@ -0,0 +1,36 @@ +// 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" +) + +// 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) +} + +// 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 = [2]string{"index", "another"} diff --git a/app/atlas.hcl b/app/atlas.hcl new file mode 100644 index 0000000..75f4a1b --- /dev/null +++ b/app/atlas.hcl @@ -0,0 +1,9 @@ +env "dev" { + 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..c16bf71 --- /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", "dev-http", "Server host (valid values: dev-http, dev-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 "dev-http": + addr = "http://localhost:8080" + case "dev-grpc": + addr = "grpc://localhost:8081" + default: + fmt.Fprintf(os.Stderr, "invalid host argument: %q (valid hosts: dev-http|dev-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 (dev-http). valid values: dev-http, dev-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/globals.go b/app/cmd/countup/globals.go new file mode 100644 index 0000000..8cb1178 --- /dev/null +++ b/app/cmd/countup/globals.go @@ -0,0 +1,10 @@ +package main + +import ( + "io" +) + +type Globals struct { + Debug bool `env:"DEBUG" help:"Enable debug logging."` + Writer io.Writer `kong:"-"` +} diff --git a/app/cmd/countup/main.go b/app/cmd/countup/main.go new file mode 100644 index 0000000..db56b13 --- /dev/null +++ b/app/cmd/countup/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "context" + "os" + "os/signal" + "syscall" + + "github.com/alecthomas/kong" + + "github.com/jace-ys/countup/api/v1/gen/api" + "github.com/jace-ys/countup/internal/slog" +) + +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."` +} + +func main() { + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + go func() { + <-ctx.Done() + stop() + }() + + var root RootCmd + cli := kong.Parse(&root, + kong.Name(api.APIName), + kong.UsageOnError(), + kong.ConfigureHelp(kong.HelpOptions{ + Compact: true, + FlagsLast: true, + NoExpandSubcommands: true, + }), + ) + + root.Writer = os.Stdout + ctx = slog.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..a66a46b --- /dev/null +++ b/app/cmd/countup/migrate.go @@ -0,0 +1,63 @@ +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/internal/slog" + "github.com/jace-ys/countup/schema/migrations" +) + +type MigrateCmd struct { + Command string `arg:"" help:"The 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 { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + db, err := postgres.NewPool(ctx, c.Database.ConnectionURI) + if err != nil { + return fmt.Errorf("init db pool: %w", err) + } + defer db.Close() + + if err := migrations.WithRiverMigrate(db); err != nil { + return fmt.Errorf("init river migrate: %w", err) + } + + if err := goose.SetDialect(string(goose.DialectPostgres)); err != nil { + return fmt.Errorf("set goose dailect: %w", err) + } + + conn := stdlib.OpenDBFromPool(db) + defer func() { + if err := conn.Close(); err != nil { + slog.Error(ctx, "error closing db connection", 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..cb3a252 --- /dev/null +++ b/app/cmd/countup/server.go @@ -0,0 +1,120 @@ +package main + +import ( + "context" + "fmt" + "time" + + apiv1 "github.com/jace-ys/countup/api/v1" + genapi "github.com/jace-ys/countup/api/v1/gen/api" + apipb "github.com/jace-ys/countup/api/v1/gen/grpc/api/pb" + grpcapi "github.com/jace-ys/countup/api/v1/gen/grpc/api/server" + httpapi "github.com/jace-ys/countup/api/v1/gen/http/api/server" + httpweb "github.com/jace-ys/countup/api/v1/gen/http/web/server" + genweb "github.com/jace-ys/countup/api/v1/gen/web" + "github.com/jace-ys/countup/internal/app" + "github.com/jace-ys/countup/internal/endpoints" + "github.com/jace-ys/countup/internal/handler/api" + "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/slog" + "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."` + + 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 { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + instrument.MustInitOTelProvider(ctx, genapi.APIName, genapi.APIVersion, c.OTLP.MetricsEndpoint, c.OTLP.TracesEndpoint) + defer func() { + if err := instrument.OTel.Shutdown(context.Background()); err != nil { + slog.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, g.Writer, c.Worker.Concurrency) + if err != nil { + return fmt.Errorf("init worker pool: %w", err) + } + + 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 := counter.New(db, counterstore.New()) + + handler, err := api.NewHandler(worker, countersvc) + if err != nil { + return fmt.Errorf("init handler: %w", err) + } + admin.Administer(handler) + + ep := endpoints.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 := web.NewHandler() + if err != nil { + return fmt.Errorf("init web: %w", err) + } + admin.Administer(handler) + + ep := endpoints.Goa(genweb.NewEndpoints).Adapt(handler) + + transport := transport.GoaHTTP(httpweb.New, httpweb.Mount) + httpsrv.RegisterHandler("/", transport.Adapt(ctx, ep, web.StaticFS)) + } + + err = app.New(httpsrv, grpcsrv, admin, worker).Run(ctx) + if err != nil { + slog.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..560f040 --- /dev/null +++ b/app/go.mod @@ -0,0 +1,75 @@ +module github.com/jace-ys/countup + +go 1.23.2 + +require ( + github.com/alecthomas/kong v1.2.1 + github.com/alexliesenfeld/health v0.8.0 + github.com/exaring/otelpgx v0.6.2 + github.com/go-chi/chi/v5 v5.1.0 + github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 + github.com/jackc/pgx/v5 v5.7.1 + github.com/pressly/goose/v3 v3.22.1 + github.com/riverqueue/river v0.13.0 + github.com/riverqueue/river/riverdriver/riverpgxv5 v0.13.0 + github.com/riverqueue/river/rivertype v0.13.0 + github.com/stretchr/testify v1.9.0 + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.56.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 + go.opentelemetry.io/contrib/instrumentation/runtime v0.56.0 + go.opentelemetry.io/otel v1.31.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.31.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0 + go.opentelemetry.io/otel/metric v1.31.0 + go.opentelemetry.io/otel/sdk v1.31.0 + go.opentelemetry.io/otel/trace v1.31.0 + goa.design/clue v1.0.7 + goa.design/goa/v3 v3.19.1 + golang.org/x/sync v0.8.0 + google.golang.org/grpc v1.67.1 + google.golang.org/protobuf v1.35.1 +) + +require ( + github.com/aws/smithy-go v1.22.0 // 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.22.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/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.13.0 // indirect + github.com/riverqueue/river/rivershared v0.13.0 // 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.31.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.31.0 // indirect + go.opentelemetry.io/proto/otlp v1.3.1 // indirect + go.uber.org/goleak v1.3.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/mod v0.21.0 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/term v0.25.0 // indirect + golang.org/x/text v0.19.0 // indirect + golang.org/x/tools v0.26.0 // indirect + google.golang.org/genproto v0.0.0-20241015192408-796eee8c2d53 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // 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..694f93a --- /dev/null +++ b/app/go.sum @@ -0,0 +1,193 @@ +github.com/alecthomas/assert/v2 v2.10.0 h1:jjRCHsj6hBJhkmhznrCzoNpbA3zqy0fYiUcYZP/GkPY= +github.com/alecthomas/assert/v2 v2.10.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/kong v1.2.1 h1:E8jH4Tsgv6wCRX2nGrdPyHDUCSG83WH2qE4XLACD33Q= +github.com/alecthomas/kong v1.2.1/go.mod h1:rKTSFhbdp3Ryefn8x5MOEprnRFQ7nlmMC01GKhehhBM= +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.0 h1:uunKnWlcoL3zO7q+gG2Pk53joueEOsnNB28QdMsmiMM= +github.com/aws/smithy-go v1.22.0/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.6.2 h1:z1ayuDusPITNOhzvmx3nLpFax+tv7Hu7mdrjtgW3ZeA= +github.com/exaring/otelpgx v0.6.2/go.mod h1:DuRveXIeRNz6VJrMTj2uCBFqiocMx4msCN1mIMmbZUI= +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/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/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/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.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= +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/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.22.1 h1:2zICEfr1O3yTP9BRZMGPj7qFxQ+ik6yeo+z1LMuioLc= +github.com/pressly/goose/v3 v3.22.1/go.mod h1:xtMpbstWyCpyH+0cxLTMCENWBG+0CSxvTsXhW95d5eo= +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.13.0 h1:BvEJfXAnHJ7HwraoPZWiD271t2jDVvX1SPCtvLzojiA= +github.com/riverqueue/river v0.13.0/go.mod h1:SOG+j28RQpKDsTA8AlfxjFdYpoPm+MSOio+Ev4ljN2U= +github.com/riverqueue/river/riverdriver v0.13.0 h1:UVzMtNfp3R+Ehr/yaRqgF58YOFEWGVqIAamCeK7RMkA= +github.com/riverqueue/river/riverdriver v0.13.0/go.mod h1:pxmx6qmGl+dNCrfa+xuktg8zrrZO3AEqlUFlFWOy8U4= +github.com/riverqueue/river/riverdriver/riverdatabasesql v0.13.0 h1:xiiwQVFUoPv/7PQIsEIerpw2ux1lZ14oZScgiB4JHdE= +github.com/riverqueue/river/riverdriver/riverdatabasesql v0.13.0/go.mod h1:f7TWWD965tE6v96qi1Y40IP2shsAai0qJBHbqT7yFLM= +github.com/riverqueue/river/riverdriver/riverpgxv5 v0.13.0 h1:wjLgea/eI5rIMh0+TCjS+/+dsULIst3Wu8bZQo2DHno= +github.com/riverqueue/river/riverdriver/riverpgxv5 v0.13.0/go.mod h1:Vzt3E33kNks2vN9lTgLJL8VFrbcAWDbwzyZLo02FlBk= +github.com/riverqueue/river/rivershared v0.13.0 h1:AqRP54GgtwoLIvV5eoZmOGOCZXL8Ce5Zm8s60R8NKOA= +github.com/riverqueue/river/rivershared v0.13.0/go.mod h1:vzvawQpDy2Z1U5chkvh1NykzWNkRhc9RLcURsJRhlbE= +github.com/riverqueue/river/rivertype v0.13.0 h1:PkT3h9tP0ZV3h0EGy2MiwEhgZqpRMN4fXfj27UKc9Q0= +github.com/riverqueue/river/rivertype v0.13.0/go.mod h1:wVOhGBeay6+JcIi0pTFlF4KtUgHYFkhMYv8dpxU46W0= +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/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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.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.56.0 h1:yMkBS9yViCc7U7yeLzJPM2XizlfdVvBRSmsQDWu6qc0= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.56.0/go.mod h1:n8MR6/liuGB5EmTETUBeU5ZgqMOlqKRxUaqPQBOANZ8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= +go.opentelemetry.io/contrib/instrumentation/runtime v0.56.0 h1:s7wHG+t8bEoH7ibWk1nk682h7EoWLJ5/8j+TSO3bX/o= +go.opentelemetry.io/contrib/instrumentation/runtime v0.56.0/go.mod h1:Q8Hsv3d9DwryfIl+ebj4mY81IYVRSPy4QfxroVZwqLo= +go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= +go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.31.0 h1:FZ6ei8GFW7kyPYdxJaV2rgI6M+4tvZzhYsQ2wgyVC08= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.31.0/go.mod h1:MdEu/mC6j3D+tTEfvI15b5Ci2Fn7NneJ71YMoiS3tpI= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.31.0 h1:ZsXq73BERAiNuuFXYqP4MR5hBrjXfMGSO+Cx7qoOZiM= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.31.0/go.mod h1:hg1zaDMpyZJuUzjFxFsRYBoccE86tM9Uf4IqNMUxvrY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 h1:K0XaT3DwHAcV4nKLzcQvwAgSyisUghWoY20I7huthMk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0/go.mod h1:B5Ki776z/MBnVha1Nzwp5arlzBbE3+1jk+pGmaP5HME= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0 h1:FFeLy03iVTXP6ffeN2iXrxfGsZGCjVx0/4KlizjyBwU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0/go.mod h1:TMu73/k1CP8nBUpDLc71Wj/Kf7ZS9FK5b53VapRsP9o= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 h1:lUsI2TYsQw2r1IASwoROaCnjdj2cvC2+Jbxvk6nHnWU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0/go.mod h1:2HpZxxQurfGxJlJDblybejHB6RX6pmExPNe517hREw4= +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.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= +go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= +go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= +go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= +go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc= +go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= +go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= +go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +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.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +google.golang.org/genproto v0.0.0-20241015192408-796eee8c2d53 h1:Df6WuGvthPzc+JiQ/G+m+sNX24kc0aTBqoDN/0yyykE= +google.golang.org/genproto v0.0.0-20241015192408-796eee8c2d53/go.mod h1:fheguH3Am2dGp1LfXkrvwqC/KlFq8F0nLq3LryOMrrE= +google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 h1:fVoAXEKA4+yufmbdVYv+SE73+cPZbbbe8paLsHfkK+U= +google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53/go.mod h1:riSXTwQ4+nqmPGtobMFyW5FqVAmIs0St6VPp4Ug7CE4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/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/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.33.0 h1:WWkA/T2G17okiLGgKAj4/RMIvgyMT19yQ038160IeYk= +modernc.org/sqlite v1.33.0/go.mod h1:9uQ9hF/pCZoYZK73D/ud5Z7cIRIILSZI8NdIemVMTX8= +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..85427df --- /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" + + "github.com/jace-ys/countup/internal/healthz" + "github.com/jace-ys/countup/internal/slog" + "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...))) + + goamux := goahttp.NewMuxer() + debug.MountPprofHandlers(debug.Adapt(goamux), debug.WithPrefix("/pprof")) + if s.debug { + debug.MountDebugLogEnabler(debug.Adapt(goamux), debug.WithPath("/settings")) + } + s.mux.Mount("/debug", goamux) + + logCtx := log.With(ctx, slog.KV("server", s.Name())) + return chainMiddleware(s.mux, + recovery.HTTP(logCtx), + slog.HTTP(logCtx), + debug.HTTP(), + ) +} + +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..d175bf3 --- /dev/null +++ b/app/internal/app/application.go @@ -0,0 +1,69 @@ +package app + +import ( + "context" + "time" + + "golang.org/x/sync/errgroup" + + "github.com/jace-ys/countup/internal/slog" +) + +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 { + g, ctx := errgroup.WithContext(ctx) + + for _, srv := range a.servers { + g.Go(func() error { + slog.Print(ctx, "server listening", + slog.KV("server", srv.Name()), + slog.KV("kind", srv.Kind()), + slog.KV("addr", srv.Addr()), + ) + return srv.Serve(ctx) + }) + } + + slog.Print(ctx, "application started") + <-ctx.Done() + slog.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 { + slog.Error(ctx, "server shutdown error", err, + slog.KV("server", srv.Name()), + slog.KV("kind", srv.Kind()), + ) + } + slog.Print(ctx, "server shutdown complete", + slog.KV("server", srv.Name()), + slog.KV("kind", srv.Kind()), + ) + return nil + }) + } + + defer slog.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..b45b434 --- /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/healthz" + "github.com/jace-ys/countup/internal/slog" + "github.com/jace-ys/countup/internal/transport/middleware/idgen" + "github.com/jace-ys/countup/internal/transport/middleware/recovery" +) + +type GRPCServer[SS any] struct { + name string + addr string + srv *grpc.Server +} + +func NewGRPCServer[SS any](ctx context.Context, name string, port int) *GRPCServer[SS] { + 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, slog.KV("server", name)) + srv := grpc.NewServer( + grpc.ChainUnaryInterceptor( + recovery.UnaryServerInterceptor(logCtx), + withMethodFilter(idgen.UnaryServerInterceptor(), excludedMethods), + withMethodFilter(slog.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[SS]{ + 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[SS]) RegisterHandler(sd *grpc.ServiceDesc, ss SS) { + s.srv.RegisterService(sd, ss) +} + +var _ Server = (*GRPCServer[any])(nil) + +func (s *GRPCServer[SS]) Name() string { + return s.name +} + +func (s *GRPCServer[SS]) Kind() string { + return "grpc" +} + +func (s *GRPCServer[SS]) Addr() string { + return s.addr +} + +func (s *GRPCServer[SS]) 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[SS]) 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() //nolint:wrapcheck + } +} + +var _ healthz.Target = (*GRPCServer[any])(nil) + +func (s *GRPCServer[SS]) 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..bea276f --- /dev/null +++ b/app/internal/app/http.go @@ -0,0 +1,120 @@ +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" + + "github.com/jace-ys/countup/internal/healthz" + "github.com/jace-ys/countup/internal/slog" + "github.com/jace-ys/countup/internal/transport/middleware/idgen" + "github.com/jace-ys/countup/internal/transport/middleware/recovery" + "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) { + if path == "/" { + s.mux.Mount(path, h) + } else { + s.mux.Mount(path, http.StripPrefix(path, 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, slog.KV("server", s.Name())) + return chainMiddleware(s.mux, + withPathFilter(telemetry.HTTP(attribute.String("http.server.name", s.Name())), excludedPaths), + recovery.HTTP(logCtx), + withPathFilter(idgen.HTTP(), excludedPaths), + withPathFilter(slog.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/endpoints/goa.go b/app/internal/endpoints/goa.go new file mode 100644 index 0000000..4d3616d --- /dev/null +++ b/app/internal/endpoints/goa.go @@ -0,0 +1,45 @@ +package endpoints + +import ( + "goa.design/clue/debug" + "goa.design/clue/log" + goa "goa.design/goa/v3/pkg" + + "github.com/jace-ys/countup/internal/endpoints/middleware/goaerror" + "github.com/jace-ys/countup/internal/endpoints/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/endpoints/middleware/goaerror/reporter.go b/app/internal/endpoints/middleware/goaerror/reporter.go new file mode 100644 index 0000000..393340b --- /dev/null +++ b/app/internal/endpoints/middleware/goaerror/reporter.go @@ -0,0 +1,36 @@ +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/slog" + "github.com/jace-ys/countup/internal/transport/middleware/idgen" +) + +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 = idgen.RequestIDFromContext(ctx) + + span := trace.SpanFromContext(ctx) + span.SetStatus(codes.Error, gerr.Name) + span.SetAttributes(attribute.String("error", err.Error())) + slog.Error(ctx, "endpoint error", err, slog.KV("err.name", gerr.Name)) + + return res, gerr + } +} diff --git a/app/internal/endpoints/middleware/tracer/endpoint.go b/app/internal/endpoints/middleware/tracer/endpoint.go new file mode 100644 index 0000000..df5fa2b --- /dev/null +++ b/app/internal/endpoints/middleware/tracer/endpoint.go @@ -0,0 +1,31 @@ +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 + } + + ctx, span := instrument.OTel.Tracer().Start(ctx, fmt.Sprintf("goa.endpoint/%s.%s", service, method)) + 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/echo.go b/app/internal/handler/api/echo.go new file mode 100644 index 0000000..01b708a --- /dev/null +++ b/app/internal/handler/api/echo.go @@ -0,0 +1,72 @@ +package api + +import ( + "context" + "errors" + "strings" + "time" + + "github.com/riverqueue/river" + + "github.com/jace-ys/countup/api/v1/gen/api" +) + +func (h *Handler) Echo(ctx context.Context, req *api.EchoPayload) (*api.EchoResult, error) { + switch { + case strings.HasPrefix(req.Text, "error: "): + msg := strings.TrimPrefix(req.Text, "error: ") + h.workers.Enqueue(ctx, &EchoJobArgs{Error: msg}) + return nil, api.MakeUnauthorized(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, api.MakeUnauthorized(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 &api.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/api/handler.go b/app/internal/handler/api/handler.go new file mode 100644 index 0000000..d7c9ac9 --- /dev/null +++ b/app/internal/handler/api/handler.go @@ -0,0 +1,46 @@ +package api + +import ( + "context" + + "github.com/alexliesenfeld/health" + + "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/worker" +) + +var _ api.Service = (*Handler)(nil) + +type Handler struct { + workers *worker.Pool + counter CounterService +} + +type CounterService interface { + GetInfo(ctx context.Context) (*counter.Info, error) + RequestIncrement(ctx context.Context, user string) error +} + +func NewHandler(workers *worker.Pool, counter CounterService) (*Handler, error) { + worker.Register(workers, &EchoWorker{}) + + return &Handler{ + workers: workers, + counter: counter, + }, nil +} + +var _ healthz.Target = (*Handler)(nil) + +func (h *Handler) HealthChecks() []health.Check { + return []health.Check{ + { + Name: "handler:countup", + Check: func(ctx context.Context) error { + return nil + }, + }, + } +} diff --git a/app/internal/handler/api/increment.go b/app/internal/handler/api/increment.go new file mode 100644 index 0000000..ce2bb12 --- /dev/null +++ b/app/internal/handler/api/increment.go @@ -0,0 +1,51 @@ +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" +) + +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) { + if err := h.counter.RequestIncrement(ctx, req.User); err != nil { + var multipleRequestErr *counter.MultipleIncrementRequestError + switch { + case errors.As(err, &multipleRequestErr): + return nil, api.MakeExistingIncrementRequest( + errors.New("user already made an increment request in the current finalize window, please try again after the next finalize time"), + ) + default: + return nil, goa.Fault("request increment: %s", err) + } + } + + 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 +} diff --git a/app/internal/handler/web/handler.go b/app/internal/handler/web/handler.go new file mode 100644 index 0000000..aa9f268 --- /dev/null +++ b/app/internal/handler/web/handler.go @@ -0,0 +1,69 @@ +package web + +import ( + "bytes" + "context" + "embed" + "fmt" + "html/template" + + "github.com/alexliesenfeld/health" + + "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 { + tmpls *template.Template +} + +func NewHandler() (*Handler, error) { + tmpls, err := template.New("tmpls").ParseFS(templateFS, "**/*.html") + if err != nil { + return nil, fmt.Errorf("parse templates: %w", err) + } + + return &Handler{ + 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 + }, + }, + } +} + +var commonVars = struct { + BaseAPIEndpoint string +}{ + BaseAPIEndpoint: "/api/v1", +} + +type renderData struct { + Vars any + Data any +} + +func (h *Handler) render(page string, data any) ([]byte, error) { + buf := &bytes.Buffer{} + err := h.tmpls.ExecuteTemplate(buf, page, renderData{commonVars, data}) + return buf.Bytes(), err +} diff --git a/app/internal/handler/web/pages.go b/app/internal/handler/web/pages.go new file mode 100644 index 0000000..24dcbf6 --- /dev/null +++ b/app/internal/handler/web/pages.go @@ -0,0 +1,25 @@ +package web + +import ( + "context" +) + +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) +} 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..3c1b229 --- /dev/null +++ b/app/internal/handler/web/templates/index.html @@ -0,0 +1,19 @@ + + + + + + + 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..66de0e2 --- /dev/null +++ b/app/internal/healthz/checker.go @@ -0,0 +1,24 @@ +package healthz + +import ( + "time" + + "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 { + check.Timeout = time.Second + 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..46232d6 --- /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: fmt.Sprintf("grpc:%s", 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..33df5eb --- /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: fmt.Sprintf("http:%s", 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/instrument/otel.go b/app/internal/instrument/otel.go new file mode 100644 index 0000000..bbfcc89 --- /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 { + ctx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + + select { + case <-time.After(5 * time.Second): + case <-ctx.Done(): + return ctx.Err() //nolint:wrapcheck + } + + var err error + for _, fn := range i.shutdownFuncs { + err = errors.Join(err, fn(ctx)) + } + + i.shutdownFuncs = nil + return err +} + +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..59b9821 --- /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/slog" +) + +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 { + slog.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) + + slog.Error(ctx, "recovered from panic", err, slog.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..da87935 --- /dev/null +++ b/app/internal/postgres/pool.go @@ -0,0 +1,41 @@ +package postgres + +import ( + "context" + "fmt" + "strings" + + "github.com/exaring/otelpgx" + "github.com/jackc/pgx/v5/pgxpool" + "go.opentelemetry.io/otel/attribute" +) + +func NewPool(ctx context.Context, connString string) (*pgxpool.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 + }), + ) + + pool, err := pgxpool.NewWithConfig(ctx, cfg) + if err != nil { + return nil, fmt.Errorf("create pool: %w", err) + } + + return pool, nil +} diff --git a/app/internal/service/counter/errors.go b/app/internal/service/counter/errors.go new file mode 100644 index 0000000..afb6fd3 --- /dev/null +++ b/app/internal/service/counter/errors.go @@ -0,0 +1,30 @@ +package counter + +import ( + "errors" + "time" +) + +var ( + ErrDBConn = errors.New("db conn") + + 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..147d6b2 --- /dev/null +++ b/app/internal/service/counter/finalize.go @@ -0,0 +1,99 @@ +package counter + +import ( + "context" + "fmt" + "time" + + "github.com/jackc/pgx/v5/pgtype" + "github.com/jackc/pgx/v5/pgxpool" + "github.com/riverqueue/river" + + counterstore "github.com/jace-ys/countup/internal/service/counter/store" + "github.com/jace-ys/countup/internal/slog" +) + +type FinalizeIncrementJobArgs struct { + FinalizeWindow time.Duration +} + +func (FinalizeIncrementJobArgs) Kind() string { + return "counter.FinalizeIncrement" +} + +type FinalizeIncrementWorker struct { + river.WorkerDefaults[FinalizeIncrementJobArgs] + + db *pgxpool.Pool + store counterstore.Querier +} + +func NewIncrementWorker(db *pgxpool.Pool, store counterstore.Querier) *FinalizeIncrementWorker { + return &FinalizeIncrementWorker{ + db: db, + store: store, + } +} + +func (w *FinalizeIncrementWorker) Work(ctx context.Context, job *river.Job[FinalizeIncrementJobArgs]) error { + tx, err := w.db.Begin(ctx) + if err != nil { + return fmt.Errorf("%w: tx begin: %w", ErrDBConn, err) + } + defer tx.Rollback(ctx) + + requests, err := w.store.ListIncrementRequests(ctx, tx) + if err != nil { + return fmt.Errorf("%w: %w", ErrListIncrementRequests, err) + } + + switch len(requests) { + case 0: + slog.Info(ctx, "no increment requests in finalize window, returning", + slog.KV("finalize.window", job.Args.FinalizeWindow), + ) + return nil + + case 1: + slog.Info(ctx, "only one increment request in finalize window, incrementing counter", + slog.KV("finalize.window", job.Args.FinalizeWindow), + slog.KV("finalize.user", requests[0].RequestedBy), + ) + + if err := w.store.IncrementCounter(ctx, tx, 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: + slog.Info(ctx, "multiple increment requests in finalize window, resetting counter", + slog.KV("requests.count", len(requests)), + slog.KV("finalize.window", job.Args.FinalizeWindow), + slog.KV("finalize.user", requests[0].RequestedBy), + ) + + if err := w.store.ResetCounter(ctx, tx); err != nil { + return fmt.Errorf("%w: %w", ErrResetCounter, err) + } + } + + slog.Info(ctx, "truncating increment requests table") + + if err := w.store.TruncateIncrementRequests(ctx, tx); err != nil { + return fmt.Errorf("%w: %w", ErrTruncateIncrementRequests, err) + } + + if err := tx.Commit(ctx); err != nil { + return fmt.Errorf("%w: tx commit: %w", ErrDBConn, 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..ff380d2 --- /dev/null +++ b/app/internal/service/counter/service.go @@ -0,0 +1,131 @@ +package counter + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/jackc/pgerrcode" + "github.com/jackc/pgx/v5/pgconn" + "github.com/jackc/pgx/v5/pgtype" + "github.com/jackc/pgx/v5/pgxpool" + + counterstore "github.com/jace-ys/countup/internal/service/counter/store" + "github.com/jace-ys/countup/internal/slog" + "github.com/jace-ys/countup/internal/worker" +) + +type Service struct { + db *pgxpool.Pool + workers *worker.Pool + store counterstore.Querier + + finalizeWindow time.Duration +} + +func New(db *pgxpool.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) { + slog.Info(ctx, "getting counter") + + tx, err := s.db.Begin(ctx) + if err != nil { + return nil, fmt.Errorf("%w: tx begin: %w", ErrDBConn, err) + } + defer tx.Rollback(ctx) + + counter, err := s.store.GetCounter(ctx, tx) + if err != nil { + return nil, fmt.Errorf("%w: %w", ErrGetCounter, err) + } + + if err := tx.Commit(ctx); err != nil { + return nil, fmt.Errorf("%w: tx commit: %w", ErrDBConn, 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 { + ctx = slog.With(ctx, slog.KV("request.user", user)) + + tx, err := s.db.Begin(ctx) + if err != nil { + return fmt.Errorf("%w: tx begin: %w", ErrDBConn, err) + } + defer tx.Rollback(ctx) + + slog.Info(ctx, "inserting increment request") + + existing, 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 existing > 0 { + slog.Info(ctx, "existing increment requests in finalize window, skip enqueuing finalize job", + slog.KV("requests.count", existing), + slog.KV("finalize.window", s.finalizeWindow), + ) + } else { + slog.Info(ctx, "first increment request in finalize window, enqueuing finalize job", + slog.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) + } + + slog.Info(ctx, "updating counter finalize time", slog.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) + } + } + + if err := tx.Commit(ctx); err != nil { + return fmt.Errorf("%w: tx commit: %w", ErrDBConn, 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..e7606cf --- /dev/null +++ b/app/internal/service/counter/store/counter.sql.go @@ -0,0 +1,136 @@ +// 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 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) +), existing AS ( + SELECT COUNT(*) AS requests FROM increment_requests +) +SELECT + existing.requests AS existing_requests +FROM existing +` + +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 existing_requests int64 + err := row.Scan(&existing_requests) + return existing_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 truncateIncrementRequests = `-- name: TruncateIncrementRequests :exec +TRUNCATE TABLE increment_requests +` + +func (q *Queries) TruncateIncrementRequests(ctx context.Context, db DBTX) error { + _, err := db.Exec(ctx, truncateIncrementRequests) + 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..3bbc047 --- /dev/null +++ b/app/internal/service/counter/store/models.go @@ -0,0 +1,22 @@ +// 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 +} diff --git a/app/internal/service/counter/store/querier.go b/app/internal/service/counter/store/querier.go new file mode 100644 index 0000000..562a746 --- /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 { + 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 + TruncateIncrementRequests(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/slog/clue.go b/app/internal/slog/clue.go new file mode 100644 index 0000000..5a8825b --- /dev/null +++ b/app/internal/slog/clue.go @@ -0,0 +1,61 @@ +package slog + +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 := []log.Fielder{KV(log.MessageKey, msg)} + return append(kvs, kv...) +} diff --git a/app/internal/slog/middleware.go b/app/internal/slog/middleware.go new file mode 100644 index 0000000..ce10e43 --- /dev/null +++ b/app/internal/slog/middleware.go @@ -0,0 +1,47 @@ +package slog + +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/idgen" +) + +func UnaryServerInterceptor(logCtx context.Context) grpc.UnaryServerInterceptor { + return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, next grpc.UnaryHandler) (_ any, err error) { + requestID := idgen.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 := idgen.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/slog/stdslog.go b/app/internal/slog/stdslog.go new file mode 100644 index 0000000..ef5e0b5 --- /dev/null +++ b/app/internal/slog/stdslog.go @@ -0,0 +1,105 @@ +package slog + +import ( + "context" + "errors" + "fmt" + "log/slog" + + "goa.design/clue/log" +) + +type StdSlogHandler struct { + ctx context.Context + level slog.Level + group string +} + +func AsStdSlogHandler(ctx context.Context, level slog.Level) *StdSlogHandler { + return &StdSlogHandler{ + ctx: log.Context(ctx, log.WithDebug()), + level: level, + } +} + +var _ slog.Handler = (*StdSlogHandler)(nil) + +func (l *StdSlogHandler) Enabled(ctx context.Context, level slog.Level) bool { + return level >= l.level +} + +func (l *StdSlogHandler) 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 *StdSlogHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + kvs := make([]log.Fielder, len(attrs)) + for i, attr := range attrs { + if l.group != "" { + kvs[i] = KV(fmt.Sprintf("%s.%s", l.group, attr.Key), attr.Value.Any()) + } else { + kvs[i] = KV(attr.Key, attr.Value.Any()) + } + } + + cp := l.clone() + cp.ctx = log.With(cp.ctx, kvs...) + return cp +} + +func (l *StdSlogHandler) WithGroup(name string) slog.Handler { + cp := l.clone() + cp.group = name + return cp +} + +func (l *StdSlogHandler) clone() *StdSlogHandler { + 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/slog/stdslog_test.go b/app/internal/slog/stdslog_test.go new file mode 100644 index 0000000..b7c570b --- /dev/null +++ b/app/internal/slog/stdslog_test.go @@ -0,0 +1,184 @@ +package slog_test + +import ( + "bufio" + "bytes" + "context" + "encoding/json" + "io" + stdslog "log/slog" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/jace-ys/countup/internal/slog" +) + +func TestStdSlogHandler(t *testing.T) { + buf := &bytes.Buffer{} + ctx := slog.NewContext(context.Background(), buf, false) + + logger := stdslog.New(slog.AsStdSlogHandler(ctx, stdslog.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 TestStdSlogHandlerLevel(t *testing.T) { + buf := &bytes.Buffer{} + ctx := slog.NewContext(context.Background(), buf, false) + + logger := stdslog.New(slog.AsStdSlogHandler(ctx, stdslog.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 TestStdSlogHandlerContext(t *testing.T) { + buf := &bytes.Buffer{} + ctx := slog.NewContext(context.Background(), buf, false) + ctx = slog.With(ctx, slog.KV("foo", "bar")) + + logger := stdslog.New(slog.AsStdSlogHandler(ctx, stdslog.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 = slog.With(ctx, slog.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 TestStdSlogHandlerWith(t *testing.T) { + buf := &bytes.Buffer{} + ctx := slog.NewContext(context.Background(), buf, false) + + logger := stdslog.New(slog.AsStdSlogHandler(ctx, stdslog.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 TestStdSlogHandlerGroup(t *testing.T) { + buf := &bytes.Buffer{} + ctx := slog.NewContext(context.Background(), buf, false) + + logger := stdslog.New(slog.AsStdSlogHandler(ctx, stdslog.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 TestStdSlogHandlerConcurrent(t *testing.T) { + ctx := slog.NewContext(context.Background(), io.Discard, false) + logger := stdslog.New(slog.AsStdSlogHandler(ctx, stdslog.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/transport/goa.go b/app/internal/transport/goa.go new file mode 100644 index 0000000..716d97c --- /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/clue/log" + "goa.design/goa/v3/grpc" + goahttp "goa.design/goa/v3/http" + goa "goa.design/goa/v3/pkg" + + "github.com/jace-ys/countup/internal/endpoints" + "github.com/jace-ys/countup/internal/slog" +) + +type GoaGRPCAdapter[E endpoints.GoaEndpoints, S any] struct { + newFunc GoaGRPCNewFunc[E, S] +} + +type GoaGRPCNewFunc[E endpoints.GoaEndpoints, S any] func(e E, uh grpc.UnaryHandler) S + +func GoaGRPC[E endpoints.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 endpoints.GoaEndpoints, S GoaHTTPServer] struct { + newFunc GoaHTTPNewFunc[E, S] + mountFunc GoaHTTPMountFunc[S] +} + +type GoaHTTPNewFunc[E endpoints.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 endpoints.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) { + slog.Error(ctx, "failed to encode response", err, + slog.KV(log.GoaMethodKey, ctx.Value(goa.MethodKey)), + slog.KV(log.GoaServiceKey, ctx.Value(goa.ServiceKey)), + ) + + 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/idgen/request.go b/app/internal/transport/middleware/idgen/request.go new file mode 100644 index 0000000..49813de --- /dev/null +++ b/app/internal/transport/middleware/idgen/request.go @@ -0,0 +1,56 @@ +package idgen + +import ( + "context" + "crypto/rand" + "encoding/base64" + "io" + "net/http" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + "goa.design/clue/log" + "google.golang.org/grpc" +) + +type ctxRequestIDKey struct{} + +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)) + }) + } +} + +func newRequestID(ctx context.Context) context.Context { + requestID := shortIDGen() + ctx = context.WithValue(ctx, ctxRequestIDKey{}, requestID) + + span := trace.SpanFromContext(ctx) + span.SetAttributes(attribute.String(log.RequestIDKey, requestID)) + + return ctx +} + +func shortIDGen() string { + b := make([]byte, 6) + io.ReadFull(rand.Reader, b) //nolint:errcheck + return base64.RawURLEncoding.EncodeToString(b) +} + +func RequestIDFromContext(ctx context.Context) string { + requestID, ok := ctx.Value(ctxRequestIDKey{}).(string) + if !ok { + return "null" + } + return requestID +} diff --git a/app/internal/transport/middleware/recovery/recovery.go b/app/internal/transport/middleware/recovery/recovery.go new file mode 100644 index 0000000..14e3e8d --- /dev/null +++ b/app/internal/transport/middleware/recovery/recovery.go @@ -0,0 +1,53 @@ +package recovery + +import ( + "context" + "errors" + "fmt" + "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 := fmt.Sprintf("%s %s", 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/telemetry/http.go b/app/internal/transport/middleware/telemetry/http.go new file mode 100644 index 0000000..5e47fcb --- /dev/null +++ b/app/internal/transport/middleware/telemetry/http.go @@ -0,0 +1,25 @@ +package telemetry + +import ( + "fmt" + "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 := fmt.Sprintf("%s %s", 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..541382f --- /dev/null +++ b/app/internal/worker/instrumented.go @@ -0,0 +1,159 @@ +package worker + +import ( + "context" + "fmt" + "strings" + + "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/instrument" + "github.com/jace-ys/countup/internal/slog" +) + +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{ + slog.KV("job.worker", "river"), + slog.KV("job.kind", job.Kind), + slog.KV("job.id", job.ID), + slog.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 { + slog.Error(ctx, "failed to extract metadata from job", err) + } + + for k, v := range md { + kvs = append(kvs, slog.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 = slog.With(ctx, kvs...) + slog.Print(ctx, "job started", + slog.KV("job.queue", job.Queue), + slog.KV("job.priority", job.Priority), + slog.KV("job.scheduled_at", job.ScheduledAt), + slog.KV("job.attempted_at", job.AttemptedAt), + ) + + func() { + defer func() { + if rvr := recover(); rvr != nil { + instrument.EmitRecoveredPanicTelemetry(ctx, rvr, source) + err = fmt.Errorf("%v", rvr) + } + }() + err = doInner(ctx) + }() + + if err != nil { + var errReason string + switch { + case strings.HasPrefix(err.Error(), "jobCancelError:"): + errReason = "job cancelled" + span.SetAttributes(attribute.Bool("job.cancelled", true)) + + case strings.HasPrefix(err.Error(), "jobSnoozeError:"): + slog.Print(ctx, "job snoozed") + span.SetAttributes(attribute.Bool("job.snoozed", true)) + return err + + case job.Attempt == job.MaxAttempts: + errReason = "job failed, discarded due to max attempts exceeded" + span.SetAttributes(attribute.Bool("job.discarded", true)) + + default: + errReason = "job failed" + span.SetAttributes(attribute.Bool("job.failed", true)) + } + + slog.Error(ctx, errReason, err) + span.SetStatus(codes.Error, errReason) + span.SetAttributes(attribute.String("error", err.Error())) + + return err + } + + slog.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{ + slog.KV("job.worker", "river"), + slog.KV("job.kind", enqueued.Job.Kind), + slog.KV("job.id", enqueued.Job.ID), + slog.KV("job.queue", enqueued.Job.Queue), + slog.KV("job.priority", enqueued.Job.Priority), + slog.KV("job.scheduled_at", enqueued.Job.ScheduledAt), + slog.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), + } + + slog.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), + ), + ) + + attrset := attribute.NewSet(attrs...) + m.metrics.jobsEnqueuedTotal.Add(ctx, 1, metric.WithAttributeSet(attrset)) + m.metrics.jobsAvailableCount.Add(ctx, 1, metric.WithAttributeSet(attrset)) +} diff --git a/app/internal/worker/metadata.go b/app/internal/worker/metadata.go new file mode 100644 index 0000000..7208594 --- /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/idgen" +) + +var _ propagation.TextMapCarrier = (*JobMetadata)(nil) + +type JobMetadata = propagation.MapCarrier + +func withContextMetadata(ctx context.Context) EnqueueOption { + md := make(JobMetadata) + + md[log.RequestIDKey] = idgen.RequestIDFromContext(ctx) + instrument.OTel.Propagators().Inject(ctx, md) + + return WithMetadata(ctx, 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..2ac2538 --- /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 := new(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.KeyValue{ + 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..c00c762 --- /dev/null +++ b/app/internal/worker/options.go @@ -0,0 +1,47 @@ +package worker + +import ( + "context" + "encoding/json" + "maps" + "time" + + "github.com/riverqueue/river" +) + +type EnqueueOption func(*river.InsertOpts) + +func WithMetadata(ctx context.Context, 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..eb50a23 --- /dev/null +++ b/app/internal/worker/pool.go @@ -0,0 +1,133 @@ +package worker + +import ( + "context" + "errors" + "fmt" + "io" + stdslog "log/slog" + + "github.com/alexliesenfeld/health" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" + "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/healthz" + "github.com/jace-ys/countup/internal/slog" +) + +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 *pgxpool.Pool, w io.Writer, 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), &river.Config{ + Workers: workers, + Queues: map[string]river.QueueConfig{ + river.QueueDefault: {MaxWorkers: concurrency}, + }, + JobInsertMiddleware: []rivertype.JobInsertMiddleware{ + &instrumentedJobInsertMiddleware{metrics: metrics}, + }, + WorkerMiddleware: []rivertype.WorkerMiddleware{ + &instrumentedWorkerMiddleware{}, + }, + Logger: stdslog.New(slog.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..5170922 --- /dev/null +++ b/app/schema/counter.sql @@ -0,0 +1,46 @@ +-- 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) +), existing AS ( + SELECT COUNT(*) AS requests FROM increment_requests +) +SELECT + existing.requests AS existing_requests +FROM existing; + +-- name: TruncateIncrementRequests :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..738685c --- /dev/null +++ b/app/schema/migrations/20241003194615_rivermigrate002.go @@ -0,0 +1,27 @@ +package migrations + +import ( + "context" + "database/sql" + + "github.com/pressly/goose/v3" + "github.com/riverqueue/river/rivermigrate" +) + +func init() { + goose.AddMigrationContext(upRiverMigrate002, downRiverMigrate002) +} + +func upRiverMigrate002(ctx context.Context, tx *sql.Tx) error { + _, err := rivermigrator.Migrate(ctx, rivermigrate.DirectionUp, &rivermigrate.MigrateOpts{ + TargetVersion: 2, + }) + return err +} + +func downRiverMigrate002(ctx context.Context, tx *sql.Tx) error { + _, err := rivermigrator.Migrate(ctx, rivermigrate.DirectionDown, &rivermigrate.MigrateOpts{ + TargetVersion: -1, + }) + return err +} diff --git a/app/schema/migrations/20241003194729_rivermigrate003.go b/app/schema/migrations/20241003194729_rivermigrate003.go new file mode 100644 index 0000000..ab5875d --- /dev/null +++ b/app/schema/migrations/20241003194729_rivermigrate003.go @@ -0,0 +1,27 @@ +package migrations + +import ( + "context" + "database/sql" + + "github.com/pressly/goose/v3" + "github.com/riverqueue/river/rivermigrate" +) + +func init() { + goose.AddMigrationContext(upRiverMigrate003, downRiverMigrate003) +} + +func upRiverMigrate003(ctx context.Context, tx *sql.Tx) error { + _, err := rivermigrator.Migrate(ctx, rivermigrate.DirectionUp, &rivermigrate.MigrateOpts{ + TargetVersion: 3, + }) + return err +} + +func downRiverMigrate003(ctx context.Context, tx *sql.Tx) error { + _, err := rivermigrator.Migrate(ctx, rivermigrate.DirectionDown, &rivermigrate.MigrateOpts{ + TargetVersion: 2, + }) + return err +} diff --git a/app/schema/migrations/20241003195032_rivermigrate004.go b/app/schema/migrations/20241003195032_rivermigrate004.go new file mode 100644 index 0000000..3505b29 --- /dev/null +++ b/app/schema/migrations/20241003195032_rivermigrate004.go @@ -0,0 +1,27 @@ +package migrations + +import ( + "context" + "database/sql" + + "github.com/pressly/goose/v3" + "github.com/riverqueue/river/rivermigrate" +) + +func init() { + goose.AddMigrationContext(upRiverMigrate004, downRiverMigrate004) +} + +func upRiverMigrate004(ctx context.Context, tx *sql.Tx) error { + _, err := rivermigrator.Migrate(ctx, rivermigrate.DirectionUp, &rivermigrate.MigrateOpts{ + TargetVersion: 4, + }) + return err +} + +func downRiverMigrate004(ctx context.Context, tx *sql.Tx) error { + _, err := rivermigrator.Migrate(ctx, rivermigrate.DirectionDown, &rivermigrate.MigrateOpts{ + TargetVersion: 3, + }) + return err +} diff --git a/app/schema/migrations/20241003195147_rivermigrate005.go b/app/schema/migrations/20241003195147_rivermigrate005.go new file mode 100644 index 0000000..485decc --- /dev/null +++ b/app/schema/migrations/20241003195147_rivermigrate005.go @@ -0,0 +1,27 @@ +package migrations + +import ( + "context" + "database/sql" + + "github.com/pressly/goose/v3" + "github.com/riverqueue/river/rivermigrate" +) + +func init() { + goose.AddMigrationContext(upRiverMigrate005, downRiverMigrate005) +} + +func upRiverMigrate005(ctx context.Context, tx *sql.Tx) error { + _, err := rivermigrator.Migrate(ctx, rivermigrate.DirectionUp, &rivermigrate.MigrateOpts{ + TargetVersion: 5, + }) + return err +} + +func downRiverMigrate005(ctx context.Context, tx *sql.Tx) error { + _, err := rivermigrator.Migrate(ctx, rivermigrate.DirectionDown, &rivermigrate.MigrateOpts{ + TargetVersion: 4, + }) + return err +} diff --git a/app/schema/migrations/20241003195218_rivermigrate006.go b/app/schema/migrations/20241003195218_rivermigrate006.go new file mode 100644 index 0000000..7302f20 --- /dev/null +++ b/app/schema/migrations/20241003195218_rivermigrate006.go @@ -0,0 +1,27 @@ +package migrations + +import ( + "context" + "database/sql" + + "github.com/pressly/goose/v3" + "github.com/riverqueue/river/rivermigrate" +) + +func init() { + goose.AddMigrationContext(upRiverMigrate006, downRiverMigrate006) +} + +func upRiverMigrate006(ctx context.Context, tx *sql.Tx) error { + _, err := rivermigrator.Migrate(ctx, rivermigrate.DirectionUp, &rivermigrate.MigrateOpts{ + TargetVersion: 6, + }) + return err +} + +func downRiverMigrate006(ctx context.Context, tx *sql.Tx) error { + _, err := rivermigrator.Migrate(ctx, rivermigrate.DirectionDown, &rivermigrate.MigrateOpts{ + TargetVersion: 5, + }) + return err +} 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/atlas.sum b/app/schema/migrations/atlas.sum new file mode 100644 index 0000000..19ce625 --- /dev/null +++ b/app/schema/migrations/atlas.sum @@ -0,0 +1,5 @@ +h1:9gV77Wmax6qqAAnTlbE0f4FEsxfHIt9RySYga3UwBYk= +20241005193820_create_table_counter.sql h1:Ui0s/qnIp8ibNveOPX8JtlTJbeN50N7/i51oiwV12AA= +20241005201313_init_table_counter.sql h1:CUxofZ7Xm6jz6uJnYboeazrISEgYJR2DLNqhx0J8ftM= +20241006142909_create_table_increment_requests.sql h1:/HurEk0XNVHafHi7GkurUW0F0fI8K0XzMOKsSSlu6Wo= +20241008214418_increment_requests_requested_by_unique.sql h1:awnLFBm+7S1+NitfcMjZwAt+78Xvq8Lk14+FhJQP0+4= 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/migrations/rivermigrate.go b/app/schema/migrations/rivermigrate.go new file mode 100644 index 0000000..2da4562 --- /dev/null +++ b/app/schema/migrations/rivermigrate.go @@ -0,0 +1,22 @@ +package migrations + +import ( + "fmt" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" + "github.com/riverqueue/river/riverdriver/riverpgxv5" + "github.com/riverqueue/river/rivermigrate" +) + +var rivermigrator *rivermigrate.Migrator[pgx.Tx] + +func WithRiverMigrate(db *pgxpool.Pool) error { + migrator, err := rivermigrate.New(riverpgxv5.New(db), &rivermigrate.Config{}) + if err != nil { + return fmt.Errorf("init river migrator: %w", err) + } + + rivermigrator = migrator + return nil +} diff --git a/app/schema/schema.sql b/app/schema/schema.sql new file mode 100644 index 0000000..cb569c9 --- /dev/null +++ b/app/schema/schema.sql @@ -0,0 +1,12 @@ +CREATE TABLE counter ( + id SERIAL PRIMARY KEY CHECK (id = 1), + count INTEGER NOT NULL, + last_increment_by TEXT, + last_increment_at TIMESTAMPTZ, + next_finalize_at TIMESTAMPTZ +); + +CREATE UNLOGGED TABLE increment_requests ( + requested_by TEXT NOT NULL UNIQUE, + 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..e69de29 diff --git a/app/sqlc.yaml b/app/sqlc.yaml new file mode 100644 index 0000000..0d1cf03 --- /dev/null +++ b/app/sqlc.yaml @@ -0,0 +1,15 @@ +--- +version: "2" + +sql: +- 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..36129e3 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,202 @@ +name: countup + +services: + countup: + image: jace-ys/countup:0.0.0 + build: + context: ./app + profiles: [apps] + labels: + service: countup + tier: app + environment: dev + 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=dev + OTLP_METRICS_ENDPOINT: otel-collector:4317 + OTLP_TRACES_ENDPOINT: otel-collector:4317 + DATABASE_CONNECTION_URI: postgresql://countup:countup@postgres:5432/countup + + postgres: + image: postgres:15.8-alpine + labels: + service: postgres + component: primary + tier: database + environment: dev + 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: dev + command: + - migrate + - up + depends_on: + postgres: + condition: service_healthy + otel-collector: + condition: service_started + environment: + OTLP_METRICS_ENDPOINT: otel-collector:4317 + OTLP_TRACES_ENDPOINT: otel-collector:4317 + 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: dev + 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: monitoring + environment: dev + ports: + - 3000:3000 + command: + - --config=/etc/grafana/config.ini + volumes: + - ./infra/envs/dev/grafana/config.ini:/etc/grafana/config.ini + - ./infra/envs/dev/grafana/provisioning:/etc/grafana/provisioning + - ./infra/envs/dev/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: monitoring + environment: dev + user: '0' + ports: + - 4317:4317 + - 8888:8888 + command: + - --config=/etc/otel-collector/config.yaml + depends_on: + mimir: + condition: service_started + tempo: + condition: service_started + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - ./infra/envs/dev/otel-collector/config.yaml:/etc/otel-collector/config.yaml + + promtail: + image: grafana/promtail:3.1.1 + labels: + service: promtail + tier: monitoring + environment: dev + 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/envs/dev/promtail/config.yaml:/etc/promtail/config.yaml + + loki: + image: grafana/loki:3.1.1 + labels: + service: loki + tier: monitoring + environment: dev + command: + - -config.file=/etc/loki/config.yaml + ports: + - 3100:3100 + volumes: + - ./infra/envs/dev/loki/config.yaml:/etc/loki/config.yaml + + tempo: + image: grafana/tempo:2.5.0 + labels: + service: tempo + tier: monitoring + environment: dev + ports: + - 3200:3200 + command: + - -config.file=/etc/tempo/config.yaml + volumes: + - ./infra/envs/dev/tempo/config.yaml:/etc/tempo/config.yaml + + mimir: + image: grafana/mimir:2.13.0 + labels: + service: mimir + tier: monitoring + environment: dev + command: + - -ingester.native-histograms-ingestion-enabled=true + - -config.file=/etc/mimir/config.yaml + ports: + - 3300:3300 + volumes: + - ./infra/envs/dev/mimir/config.yaml:/etc/mimir/config.yaml + + swagger: + image: swaggerapi/swagger-ui:v5.17.14 + labels: + service: swagger + tier: devtool + environment: dev + ports: + - 5000:8080 + volumes: + - ./app/api/v1/gen/http/openapi3.json:/etc/swagger/openapi3.json + environment: + SWAGGER_JSON: /etc/swagger/openapi3.json \ No newline at end of file diff --git a/infra/environments/dev/grafana/config.ini b/infra/environments/dev/grafana/config.ini new file mode 100644 index 0000000..011c225 --- /dev/null +++ b/infra/environments/dev/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/dev/grafana/definitions/otel-collector.json b/infra/environments/dev/grafana/definitions/otel-collector.json new file mode 100644 index 0000000..376ab9e --- /dev/null +++ b/infra/environments/dev/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/dev/grafana/definitions/service.json b/infra/environments/dev/grafana/definitions/service.json new file mode 100644 index 0000000..ab89f15 --- /dev/null +++ b/infra/environments/dev/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": [ + "dev" + ], + "value": [ + "dev" + ] + }, + "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/dev/grafana/provisioning/dashboards/dashboards.yaml b/infra/environments/dev/grafana/provisioning/dashboards/dashboards.yaml new file mode 100644 index 0000000..b39257b --- /dev/null +++ b/infra/environments/dev/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/dev/grafana/provisioning/datasources/datasources.yaml b/infra/environments/dev/grafana/provisioning/datasources/datasources.yaml new file mode 100644 index 0000000..bfd8bd6 --- /dev/null +++ b/infra/environments/dev/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/dev/grafana/provisioning/plugins/loki-explorer-app.yaml b/infra/environments/dev/grafana/provisioning/plugins/loki-explorer-app.yaml new file mode 100644 index 0000000..b531d8f --- /dev/null +++ b/infra/environments/dev/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/dev/loki/config.yaml b/infra/environments/dev/loki/config.yaml new file mode 100644 index 0000000..e6e9348 --- /dev/null +++ b/infra/environments/dev/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/dev/mimir/config.yaml b/infra/environments/dev/mimir/config.yaml new file mode 100644 index 0000000..ad9d9b5 --- /dev/null +++ b/infra/environments/dev/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/dev/otel-collector/config.yaml b/infra/environments/dev/otel-collector/config.yaml new file mode 100644 index 0000000..ba189ed --- /dev/null +++ b/infra/environments/dev/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/dev/postgres/init/river.sql b/infra/environments/dev/postgres/init/river.sql new file mode 100644 index 0000000..e42ee43 --- /dev/null +++ b/infra/environments/dev/postgres/init/river.sql @@ -0,0 +1,271 @@ +-- River migration 001 [up] +CREATE TABLE river_migration( + id bigserial PRIMARY KEY, + created_at timestamptz NOT NULL DEFAULT NOW(), + version bigint NOT NULL, + CONSTRAINT version CHECK (version >= 1) +); + +CREATE UNIQUE INDEX ON river_migration USING btree(version); + +-- River migration 002 [up] +CREATE TYPE river_job_state AS ENUM( + 'available', + 'cancelled', + 'completed', + 'discarded', + 'retryable', + 'running', + 'scheduled' +); + +CREATE TABLE river_job( + -- 8 bytes + id bigserial PRIMARY KEY, + + -- 8 bytes (4 bytes + 2 bytes + 2 bytes) + -- + -- `state` is kept near the top of the table for operator convenience -- when + -- looking at jobs with `SELECT *` it'll appear first after ID. The other two + -- fields aren't as important but are kept adjacent to `state` for alignment + -- to get an 8-byte block. + state river_job_state NOT NULL DEFAULT 'available', + attempt smallint NOT NULL DEFAULT 0, + max_attempts smallint NOT NULL, + + -- 8 bytes each (no alignment needed) + attempted_at timestamptz, + created_at timestamptz NOT NULL DEFAULT NOW(), + finalized_at timestamptz, + scheduled_at timestamptz NOT NULL DEFAULT NOW(), + + -- 2 bytes (some wasted padding probably) + priority smallint NOT NULL DEFAULT 1, + + -- types stored out-of-band + args jsonb, + attempted_by text[], + errors jsonb[], + kind text NOT NULL, + metadata jsonb NOT NULL DEFAULT '{}', + queue text NOT NULL DEFAULT 'default', + tags varchar(255)[], + + CONSTRAINT finalized_or_finalized_at_null CHECK ((state IN ('cancelled', 'completed', 'discarded') AND finalized_at IS NOT NULL) OR finalized_at IS NULL), + CONSTRAINT max_attempts_is_positive CHECK (max_attempts > 0), + CONSTRAINT priority_in_range CHECK (priority >= 1 AND priority <= 4), + CONSTRAINT queue_length CHECK (char_length(queue) > 0 AND char_length(queue) < 128), + CONSTRAINT kind_length CHECK (char_length(kind) > 0 AND char_length(kind) < 128) +); + +-- We may want to consider adding another property here after `kind` if it seems +-- like it'd be useful for something. +CREATE INDEX river_job_kind ON river_job USING btree(kind); + +CREATE INDEX river_job_state_and_finalized_at_index ON river_job USING btree(state, finalized_at) WHERE finalized_at IS NOT NULL; + +CREATE INDEX river_job_prioritized_fetching_index ON river_job USING btree(state, queue, priority, scheduled_at, id); + +CREATE INDEX river_job_args_index ON river_job USING GIN(args); + +CREATE INDEX river_job_metadata_index ON river_job USING GIN(metadata); + +CREATE OR REPLACE FUNCTION river_job_notify() + RETURNS TRIGGER + AS $$ +DECLARE + payload json; +BEGIN + IF NEW.state = 'available' THEN + -- Notify will coalesce duplicate notificiations within a transaction, so + -- keep these payloads generalized: + payload = json_build_object('queue', NEW.queue); + PERFORM + pg_notify('river_insert', payload::text); + END IF; + RETURN NULL; +END; +$$ +LANGUAGE plpgsql; + +CREATE TRIGGER river_notify + AFTER INSERT ON river_job + FOR EACH ROW + EXECUTE PROCEDURE river_job_notify(); + +CREATE UNLOGGED TABLE river_leader( + -- 8 bytes each (no alignment needed) + elected_at timestamptz NOT NULL, + expires_at timestamptz NOT NULL, + + -- types stored out-of-band + leader_id text NOT NULL, + name text PRIMARY KEY, + + CONSTRAINT name_length CHECK (char_length(name) > 0 AND char_length(name) < 128), + CONSTRAINT leader_id_length CHECK (char_length(leader_id) > 0 AND char_length(leader_id) < 128) +); + +-- River migration 003 [up] +ALTER TABLE river_job ALTER COLUMN tags SET DEFAULT '{}'; +UPDATE river_job SET tags = '{}' WHERE tags IS NULL; +ALTER TABLE river_job ALTER COLUMN tags SET NOT NULL; + +-- River migration 004 [up] +-- The args column never had a NOT NULL constraint or default value at the +-- database level, though we tried to ensure one at the application level. +ALTER TABLE river_job ALTER COLUMN args SET DEFAULT '{}'; +UPDATE river_job SET args = '{}' WHERE args IS NULL; +ALTER TABLE river_job ALTER COLUMN args SET NOT NULL; +ALTER TABLE river_job ALTER COLUMN args DROP DEFAULT; + +-- The metadata column never had a NOT NULL constraint or default value at the +-- database level, though we tried to ensure one at the application level. +ALTER TABLE river_job ALTER COLUMN metadata SET DEFAULT '{}'; +UPDATE river_job SET metadata = '{}' WHERE metadata IS NULL; +ALTER TABLE river_job ALTER COLUMN metadata SET NOT NULL; + +-- The 'pending' job state will be used for upcoming functionality: +ALTER TYPE river_job_state ADD VALUE IF NOT EXISTS 'pending' AFTER 'discarded'; + +ALTER TABLE river_job DROP CONSTRAINT finalized_or_finalized_at_null; +ALTER TABLE river_job ADD CONSTRAINT finalized_or_finalized_at_null CHECK ( + (finalized_at IS NULL AND state NOT IN ('cancelled', 'completed', 'discarded')) OR + (finalized_at IS NOT NULL AND state IN ('cancelled', 'completed', 'discarded')) +); + +DROP TRIGGER river_notify ON river_job; +DROP FUNCTION river_job_notify; + +CREATE TABLE river_queue( + name text PRIMARY KEY NOT NULL, + created_at timestamptz NOT NULL DEFAULT NOW(), + metadata jsonb NOT NULL DEFAULT '{}' ::jsonb, + paused_at timestamptz, + updated_at timestamptz NOT NULL +); + +ALTER TABLE river_leader + ALTER COLUMN name SET DEFAULT 'default', + DROP CONSTRAINT name_length, + ADD CONSTRAINT name_length CHECK (name = 'default'); + +-- River migration 005 [up] +-- +-- Rebuild the migration table so it's based on `(line, version)`. +-- + +DO +$body$ +BEGIN + -- Tolerate users who may be using their own migration system rather than + -- River's. If they are, they will have skipped version 001 containing + -- `CREATE TABLE river_migration`, so this table won't exist. + IF (SELECT to_regclass('river_migration') IS NOT NULL) THEN + ALTER TABLE river_migration + RENAME TO river_migration_old; + + CREATE TABLE river_migration( + line TEXT NOT NULL, + version bigint NOT NULL, + created_at timestamptz NOT NULL DEFAULT NOW(), + CONSTRAINT line_length CHECK (char_length(line) > 0 AND char_length(line) < 128), + CONSTRAINT version_gte_1 CHECK (version >= 1), + PRIMARY KEY (line, version) + ); + + INSERT INTO river_migration + (created_at, line, version) + SELECT created_at, 'main', version + FROM river_migration_old; + + DROP TABLE river_migration_old; + END IF; +END; +$body$ +LANGUAGE 'plpgsql'; + +-- +-- Add `river_job.unique_key` and bring up an index on it. +-- + +-- These statements use `IF NOT EXISTS` to allow users with a `river_job` table +-- of non-trivial size to build the index `CONCURRENTLY` out of band of this +-- migration, then follow by completing the migration. +ALTER TABLE river_job + ADD COLUMN IF NOT EXISTS unique_key bytea; + +CREATE UNIQUE INDEX IF NOT EXISTS river_job_kind_unique_key_idx ON river_job (kind, unique_key) WHERE unique_key IS NOT NULL; + +-- +-- Create `river_client` and derivative. +-- +-- This feature hasn't quite yet been implemented, but we're taking advantage of +-- the migration to add the schema early so that we can add it later without an +-- additional migration. +-- + +CREATE UNLOGGED TABLE river_client ( + id text PRIMARY KEY NOT NULL, + created_at timestamptz NOT NULL DEFAULT now(), + metadata jsonb NOT NULL DEFAULT '{}', + paused_at timestamptz, + updated_at timestamptz NOT NULL, + CONSTRAINT name_length CHECK (char_length(id) > 0 AND char_length(id) < 128) +); + +-- Differs from `river_queue` in that it tracks the queue state for a particular +-- active client. +CREATE UNLOGGED TABLE river_client_queue ( + river_client_id text NOT NULL REFERENCES river_client (id) ON DELETE CASCADE, + name text NOT NULL, + created_at timestamptz NOT NULL DEFAULT now(), + max_workers bigint NOT NULL DEFAULT 0, + metadata jsonb NOT NULL DEFAULT '{}', + num_jobs_completed bigint NOT NULL DEFAULT 0, + num_jobs_running bigint NOT NULL DEFAULT 0, + updated_at timestamptz NOT NULL, + PRIMARY KEY (river_client_id, name), + CONSTRAINT name_length CHECK (char_length(name) > 0 AND char_length(name) < 128), + CONSTRAINT num_jobs_completed_zero_or_positive CHECK (num_jobs_completed >= 0), + CONSTRAINT num_jobs_running_zero_or_positive CHECK (num_jobs_running >= 0) +); + +-- River migration 006 [up] +CREATE OR REPLACE FUNCTION river_job_state_in_bitmask(bitmask BIT(8), state river_job_state) +RETURNS boolean +LANGUAGE SQL +IMMUTABLE +AS $$ + SELECT CASE state + WHEN 'available' THEN get_bit(bitmask, 7) + WHEN 'cancelled' THEN get_bit(bitmask, 6) + WHEN 'completed' THEN get_bit(bitmask, 5) + WHEN 'discarded' THEN get_bit(bitmask, 4) + WHEN 'pending' THEN get_bit(bitmask, 3) + WHEN 'retryable' THEN get_bit(bitmask, 2) + WHEN 'running' THEN get_bit(bitmask, 1) + WHEN 'scheduled' THEN get_bit(bitmask, 0) + ELSE 0 + END = 1; +$$; + +-- +-- Add `river_job.unique_states` and bring up an index on it. +-- +ALTER TABLE river_job ADD COLUMN unique_states BIT(8); + +-- This statements uses `IF NOT EXISTS` to allow users with a `river_job` table +-- of non-trivial size to build the index `CONCURRENTLY` out of band of this +-- migration, then follow by completing the migration. +CREATE UNIQUE INDEX IF NOT EXISTS river_job_unique_idx ON river_job (unique_key) + WHERE unique_key IS NOT NULL + AND unique_states IS NOT NULL + AND river_job_state_in_bitmask(unique_states, state); + +-- Remove the old unique index. Users who are actively using the unique jobs +-- feature and who wish to avoid deploy downtime may want od drop this in a +-- subsequent migration once all jobs using the old unique system have been +-- completed (i.e. no more rows with non-null unique_key and null +-- unique_states). +DROP INDEX river_job_kind_unique_key_idx; diff --git a/infra/environments/dev/promtail/config.yaml b/infra/environments/dev/promtail/config.yaml new file mode 100644 index 0000000..1e2231e --- /dev/null +++ b/infra/environments/dev/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/dev/tempo/config.yaml b/infra/environments/dev/tempo/config.yaml new file mode 100644 index 0000000..086f9ad --- /dev/null +++ b/infra/environments/dev/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/prd/.gitkeep b/infra/environments/prd/.gitkeep new file mode 100644 index 0000000..e69de29