Compare commits
No commits in common. "development" and "main" have entirely different histories.
developmen
...
main
1373
.claude/SKILL.md
Normal file
1373
.claude/SKILL.md
Normal file
File diff suppressed because it is too large
Load Diff
11
.dockerignore
Normal file
11
.dockerignore
Normal file
@ -0,0 +1,11 @@
|
||||
**/.env
|
||||
!**/.env.example
|
||||
|
||||
**/*.exe
|
||||
**/*.test
|
||||
**/*.out
|
||||
|
||||
.git
|
||||
.gitignore
|
||||
.claude
|
||||
docs
|
||||
13
.gitignore
vendored
13
.gitignore
vendored
@ -1,8 +1,7 @@
|
||||
.go-cache/
|
||||
*.log
|
||||
.env
|
||||
.DS_Store
|
||||
*.env
|
||||
!.env.example
|
||||
|
||||
# compiled binary from `go build`
|
||||
/omnix-sopiga
|
||||
/omnix-sopiga.exe
|
||||
*.exe
|
||||
*.test
|
||||
*.out
|
||||
vendor/
|
||||
|
||||
@ -4,20 +4,11 @@ stages:
|
||||
|
||||
variables:
|
||||
GO_VERSION: "1.25"
|
||||
# TODO: confirm final remote URL once go-dw-framework is pushed, and set up
|
||||
# a deploy token / CI_JOB_TOKEN with read access if the repo is private.
|
||||
FRAMEWORK_REPO_URL: "https://repository.promas.id/prana/go-dw-framework.git"
|
||||
|
||||
# go.mod currently has `replace repository.promas.id/prana/go-dw-framework => ../go-dw-framework`
|
||||
# for pre-release local development. CI clones it into that exact relative
|
||||
# path so the replace directive resolves. Once go-dw-framework is tagged
|
||||
# (e.g. v0.1.0), remove the replace line from go.mod, drop this clone step,
|
||||
# and let `go mod download` fetch the tagged version normally.
|
||||
.with-framework:
|
||||
.go-cache:
|
||||
image: golang:${GO_VERSION}-alpine
|
||||
before_script:
|
||||
- apk add --no-cache git
|
||||
- git clone --depth 1 "$FRAMEWORK_REPO_URL" ../go-dw-framework
|
||||
cache:
|
||||
key: go-mod-cache
|
||||
paths:
|
||||
@ -25,27 +16,68 @@ variables:
|
||||
variables:
|
||||
GOPATH: "$CI_PROJECT_DIR/.go-cache"
|
||||
|
||||
test:
|
||||
# ---------- omnix-broadcast ----------
|
||||
test:omnix-broadcast:
|
||||
stage: test
|
||||
extends: .with-framework
|
||||
extends: .go-cache
|
||||
script:
|
||||
- cd services/omnix-broadcast
|
||||
- go vet ./...
|
||||
- go test ./tests/... -v
|
||||
rules:
|
||||
- changes:
|
||||
- framework/**/*
|
||||
- services/omnix-broadcast/**/*
|
||||
- go.work
|
||||
- go.work.sum
|
||||
|
||||
# NOTE: assumes the GitLab project path checks out into a folder literally
|
||||
# named `omnix-sopiga` (matches CI_PROJECT_DIR's basename) so the Dockerfile's
|
||||
# `COPY omnix-sopiga ./omnix-sopiga` line lines up. Adjust if your project slug differs.
|
||||
build:
|
||||
build:omnix-broadcast:
|
||||
stage: build
|
||||
image: docker:24
|
||||
services:
|
||||
- docker:24-dind
|
||||
before_script:
|
||||
- apk add --no-cache git
|
||||
- git clone --depth 1 "$FRAMEWORK_REPO_URL" ../go-dw-framework
|
||||
script:
|
||||
- docker build -f Dockerfile -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" ..
|
||||
- docker build -f deployments/docker/omnix-broadcast/Dockerfile
|
||||
-t "$CI_REGISTRY_IMAGE/omnix-broadcast:$CI_COMMIT_SHORT_SHA" .
|
||||
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
|
||||
- docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"
|
||||
- docker push "$CI_REGISTRY_IMAGE/omnix-broadcast:$CI_COMMIT_SHORT_SHA"
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "main"'
|
||||
changes:
|
||||
- framework/**/*
|
||||
- services/omnix-broadcast/**/*
|
||||
- go.work
|
||||
- go.work.sum
|
||||
|
||||
# ---------- template-service ----------
|
||||
test:template-service:
|
||||
stage: test
|
||||
extends: .go-cache
|
||||
script:
|
||||
- cd services/template-service
|
||||
- go vet ./...
|
||||
- go test ./tests/... -v
|
||||
rules:
|
||||
- changes:
|
||||
- framework/**/*
|
||||
- services/template-service/**/*
|
||||
- go.work
|
||||
- go.work.sum
|
||||
|
||||
build:template-service:
|
||||
stage: build
|
||||
image: docker:24
|
||||
services:
|
||||
- docker:24-dind
|
||||
script:
|
||||
- docker build -f deployments/docker/template-service/Dockerfile
|
||||
-t "$CI_REGISTRY_IMAGE/template-service:$CI_COMMIT_SHORT_SHA" .
|
||||
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
|
||||
- docker push "$CI_REGISTRY_IMAGE/template-service:$CI_COMMIT_SHORT_SHA"
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "main"'
|
||||
changes:
|
||||
- framework/**/*
|
||||
- services/template-service/**/*
|
||||
- go.work
|
||||
- go.work.sum
|
||||
|
||||
17
CODEOWNERS
Normal file
17
CODEOWNERS
Normal file
@ -0,0 +1,17 @@
|
||||
# GitLab Code Owners — https://docs.gitlab.com/ee/user/project/codeowners/
|
||||
# Update the group handles below to match your actual GitLab groups/usernames.
|
||||
|
||||
[Platform / Framework]
|
||||
framework/ @platform-team
|
||||
go.work
|
||||
go.work.sum
|
||||
|
||||
[Omnix Broadcast Team]
|
||||
services/omnix-broadcast/ @team-omnix-broadcast
|
||||
|
||||
[Template Service Team]
|
||||
services/template-service/ @team-template-service
|
||||
|
||||
[Deployment]
|
||||
deployments/ @platform-team
|
||||
docker-compose.yml @platform-team
|
||||
36
Dockerfile
36
Dockerfile
@ -1,36 +0,0 @@
|
||||
# Build context: PARENT directory of this repo, i.e. the folder that
|
||||
# contains both `omnix-sopiga/` and `go-dw-framework/` as siblings:
|
||||
#
|
||||
# docker build -f omnix-sopiga/Dockerfile -t omnix-sopiga ..
|
||||
#
|
||||
# Why: go.mod still uses a local `replace` directive pointing at
|
||||
# ../go-dw-framework (see go.mod) for pre-release development, since
|
||||
# go-dw-framework isn't tagged/published yet. Once it is tagged (e.g. v0.1.0)
|
||||
# and reachable from the Docker build environment:
|
||||
# 1. go.mod: remove the `replace` line, keep `require ... v0.1.0`
|
||||
# 2. This Dockerfile: build context becomes just this repo (`.`), and the
|
||||
# COPY lines below collapse to `COPY . .`
|
||||
|
||||
FROM golang:1.25-alpine AS builder
|
||||
|
||||
RUN apk add --no-cache git
|
||||
|
||||
WORKDIR /src
|
||||
COPY go-dw-framework ./go-dw-framework
|
||||
COPY omnix-sopiga ./omnix-sopiga
|
||||
|
||||
WORKDIR /src/omnix-sopiga
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/omnix-sopiga .
|
||||
|
||||
FROM alpine:3.20
|
||||
|
||||
RUN apk add --no-cache ca-certificates tzdata && \
|
||||
adduser -D -u 10001 app
|
||||
USER app
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=builder /out/omnix-sopiga .
|
||||
COPY omnix-sopiga/migrations ./migrations
|
||||
|
||||
EXPOSE 8081
|
||||
ENTRYPOINT ["./omnix-sopiga"]
|
||||
29
deployments/docker/omnix-broadcast/Dockerfile
Normal file
29
deployments/docker/omnix-broadcast/Dockerfile
Normal file
@ -0,0 +1,29 @@
|
||||
# Build context: repo root (../../..)
|
||||
# docker build -f deployments/docker/omnix-broadcast/Dockerfile -t omnix-broadcast .
|
||||
|
||||
FROM golang:1.25-alpine AS builder
|
||||
|
||||
RUN apk add --no-cache git
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
COPY go.work go.work.sum ./
|
||||
COPY framework ./framework
|
||||
COPY services/omnix-broadcast ./services/omnix-broadcast
|
||||
COPY services/template-service ./services/template-service
|
||||
|
||||
WORKDIR /src/services/omnix-broadcast
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/omnix-broadcast .
|
||||
|
||||
FROM alpine:3.20
|
||||
|
||||
RUN apk add --no-cache ca-certificates tzdata && \
|
||||
adduser -D -u 10001 app
|
||||
USER app
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=builder /out/omnix-broadcast .
|
||||
COPY services/omnix-broadcast/migrations ./migrations
|
||||
|
||||
EXPOSE 8081
|
||||
ENTRYPOINT ["./omnix-broadcast"]
|
||||
29
deployments/docker/template-service/Dockerfile
Normal file
29
deployments/docker/template-service/Dockerfile
Normal file
@ -0,0 +1,29 @@
|
||||
# Build context: repo root (../../..)
|
||||
# docker build -f deployments/docker/template-service/Dockerfile -t template-service .
|
||||
|
||||
FROM golang:1.25-alpine AS builder
|
||||
|
||||
RUN apk add --no-cache git
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
COPY go.work go.work.sum ./
|
||||
COPY framework ./framework
|
||||
COPY services/omnix-broadcast ./services/omnix-broadcast
|
||||
COPY services/template-service ./services/template-service
|
||||
|
||||
WORKDIR /src/services/template-service
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/template-service .
|
||||
|
||||
FROM alpine:3.20
|
||||
|
||||
RUN apk add --no-cache ca-certificates tzdata && \
|
||||
adduser -D -u 10001 app
|
||||
USER app
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=builder /out/template-service .
|
||||
COPY services/template-service/migrations ./migrations
|
||||
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["./template-service"]
|
||||
22
docker-compose.yml
Normal file
22
docker-compose.yml
Normal file
@ -0,0 +1,22 @@
|
||||
services:
|
||||
omnix-broadcast:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: deployments/docker/omnix-broadcast/Dockerfile
|
||||
image: int-omnix/omnix-broadcast:latest
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- services/omnix-broadcast/.env
|
||||
ports:
|
||||
- "8081:8081"
|
||||
|
||||
template-service:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: deployments/docker/template-service/Dockerfile
|
||||
image: int-omnix/template-service:latest
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- services/template-service/.env
|
||||
ports:
|
||||
- "8080:8080"
|
||||
75
framework/config/loader.go
Normal file
75
framework/config/loader.go
Normal file
@ -0,0 +1,75 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// LoadDotEnv reads KEY=VALUE pairs from path (default ".env") and applies
|
||||
// them via os.Setenv, without overriding variables already set in the
|
||||
// environment. Missing file is not an error — real env vars always win.
|
||||
func LoadDotEnv(path string) error {
|
||||
if path == "" {
|
||||
path = ".env"
|
||||
}
|
||||
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("open %s: %w", path, err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
key, value, ok := strings.Cut(line, "=")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
key = strings.TrimSpace(key)
|
||||
value = strings.Trim(strings.TrimSpace(value), `"'`)
|
||||
|
||||
if _, exists := os.LookupEnv(key); !exists {
|
||||
os.Setenv(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
return scanner.Err()
|
||||
}
|
||||
|
||||
func GetString(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func GetInt(key string, fallback int) int {
|
||||
v := os.Getenv(key)
|
||||
if v == "" {
|
||||
return fallback
|
||||
}
|
||||
n, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return fallback
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func MustGetString(key string) (string, error) {
|
||||
v := os.Getenv(key)
|
||||
if v == "" {
|
||||
return "", fmt.Errorf("missing required environment variable: %s", key)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
30
framework/db/batch.go
Normal file
30
framework/db/batch.go
Normal file
@ -0,0 +1,30 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
func ExecBatch(ctx context.Context, pool *pgxpool.Pool, stmt string, args [][]any) (int64, error) {
|
||||
batch := &pgx.Batch{}
|
||||
for _, a := range args {
|
||||
batch.Queue(stmt, a...)
|
||||
}
|
||||
|
||||
results := pool.SendBatch(ctx, batch)
|
||||
defer results.Close()
|
||||
|
||||
var rowsAffected int64
|
||||
for i := 0; i < len(args); i++ {
|
||||
tag, err := results.Exec()
|
||||
if err != nil {
|
||||
return rowsAffected, fmt.Errorf("exec batch item %d: %w", i, err)
|
||||
}
|
||||
rowsAffected += tag.RowsAffected()
|
||||
}
|
||||
|
||||
return rowsAffected, nil
|
||||
}
|
||||
30
framework/db/pool.go
Normal file
30
framework/db/pool.go
Normal file
@ -0,0 +1,30 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
DSN string
|
||||
MaxConns int32
|
||||
MinConns int32
|
||||
MaxConnLifetime time.Duration
|
||||
MaxConnIdleTime time.Duration
|
||||
}
|
||||
|
||||
func NewPool(ctx context.Context, cfg Config) (*pgxpool.Pool, error) {
|
||||
poolCfg, err := pgxpool.ParseConfig(cfg.DSN)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
poolCfg.MaxConns = cfg.MaxConns
|
||||
poolCfg.MinConns = cfg.MinConns
|
||||
poolCfg.MaxConnLifetime = cfg.MaxConnLifetime
|
||||
poolCfg.MaxConnIdleTime = cfg.MaxConnIdleTime
|
||||
|
||||
return pgxpool.NewWithConfig(ctx, poolCfg)
|
||||
}
|
||||
26
framework/db/tx.go
Normal file
26
framework/db/tx.go
Normal file
@ -0,0 +1,26 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
func WithTx(ctx context.Context, pool *pgxpool.Pool, fn func(ctx context.Context) error) error {
|
||||
tx, err := pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("begin tx: %w", err)
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
if err := fn(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return fmt.Errorf("commit tx: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
61
framework/go.mod
Normal file
61
framework/go.mod
Normal file
@ -0,0 +1,61 @@
|
||||
module github.com/yourorg/go-dw-platform/framework
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.12.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/jackc/pgx/v5 v5.5.0
|
||||
github.com/prometheus/client_golang v1.17.0
|
||||
github.com/redis/go-redis/v9 v9.3.0
|
||||
go.opentelemetry.io/otel v1.44.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic v1.15.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect
|
||||
github.com/prometheus/common v0.44.0 // indirect
|
||||
github.com/prometheus/procfs v0.11.1 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.44.0 // indirect
|
||||
golang.org/x/arch v0.22.0 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/net v0.51.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
)
|
||||
146
framework/go.sum
Normal file
146
framework/go.sum
Normal file
@ -0,0 +1,146 @@
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/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-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.5.0 h1:NxstgwndsTRy7eq9/kqYc/BZh5w2hHJV86wjvO+1xPw=
|
||||
github.com/jackc/pgx/v5 v5.5.0/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
|
||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
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/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
|
||||
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
|
||||
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM=
|
||||
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
|
||||
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
|
||||
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
|
||||
github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI=
|
||||
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/redis/go-redis/v9 v9.3.0 h1:RiVDjmig62jIWp7Kk4XVLs0hzV6pI3PyTnnL0cnn0u0=
|
||||
github.com/redis/go-redis/v9 v9.3.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/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.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
|
||||
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
|
||||
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
|
||||
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
|
||||
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
|
||||
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
||||
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
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=
|
||||
62
framework/ingestion/batcher.go
Normal file
62
framework/ingestion/batcher.go
Normal file
@ -0,0 +1,62 @@
|
||||
package ingestion
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Batcher[T any] struct {
|
||||
mu sync.Mutex
|
||||
items []T
|
||||
size int
|
||||
timeout time.Duration
|
||||
createdAt time.Time
|
||||
}
|
||||
|
||||
func NewBatcher[T any](size int, timeout time.Duration) *Batcher[T] {
|
||||
return &Batcher[T]{
|
||||
items: make([]T, 0, size),
|
||||
size: size,
|
||||
timeout: timeout,
|
||||
createdAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Batcher[T]) Add(item T) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
if len(b.items) == 0 {
|
||||
b.createdAt = time.Now()
|
||||
}
|
||||
b.items = append(b.items, item)
|
||||
}
|
||||
|
||||
func (b *Batcher[T]) IsFull() bool {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return len(b.items) >= b.size
|
||||
}
|
||||
|
||||
func (b *Batcher[T]) IsExpired() bool {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if len(b.items) == 0 {
|
||||
return false
|
||||
}
|
||||
return time.Since(b.createdAt) >= b.timeout
|
||||
}
|
||||
|
||||
func (b *Batcher[T]) Get() []T {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
items := make([]T, len(b.items))
|
||||
copy(items, b.items)
|
||||
return items
|
||||
}
|
||||
|
||||
func (b *Batcher[T]) Clear() {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
b.items = b.items[:0]
|
||||
}
|
||||
33
framework/ingestion/deduplicator.go
Normal file
33
framework/ingestion/deduplicator.go
Normal file
@ -0,0 +1,33 @@
|
||||
package ingestion
|
||||
|
||||
import "sync"
|
||||
|
||||
type Deduplicator struct {
|
||||
mu sync.RWMutex
|
||||
seen map[string]struct{}
|
||||
}
|
||||
|
||||
func NewDeduplicator() *Deduplicator {
|
||||
return &Deduplicator{
|
||||
seen: make(map[string]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Deduplicator) Exists(key string) bool {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
_, ok := d.seen[key]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (d *Deduplicator) Mark(key string) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
d.seen[key] = struct{}{}
|
||||
}
|
||||
|
||||
func (d *Deduplicator) Reset() {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
d.seen = make(map[string]struct{})
|
||||
}
|
||||
41
framework/ingestion/retry.go
Normal file
41
framework/ingestion/retry.go
Normal file
@ -0,0 +1,41 @@
|
||||
package ingestion
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Retrier struct {
|
||||
MaxRetries int
|
||||
BaseDelay time.Duration
|
||||
}
|
||||
|
||||
func NewRetrier(maxRetries int, baseDelay time.Duration) *Retrier {
|
||||
return &Retrier{
|
||||
MaxRetries: maxRetries,
|
||||
BaseDelay: baseDelay,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Retrier) Do(ctx context.Context, fn func() (int64, error)) (int64, error) {
|
||||
var lastErr error
|
||||
|
||||
for attempt := 0; attempt <= r.MaxRetries; attempt++ {
|
||||
if attempt > 0 {
|
||||
delay := r.BaseDelay * time.Duration(1<<uint(attempt-1))
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return 0, ctx.Err()
|
||||
case <-time.After(delay):
|
||||
}
|
||||
}
|
||||
|
||||
result, err := fn()
|
||||
if err == nil {
|
||||
return result, nil
|
||||
}
|
||||
lastErr = err
|
||||
}
|
||||
|
||||
return 0, lastErr
|
||||
}
|
||||
20
framework/logger/structured.go
Normal file
20
framework/logger/structured.go
Normal file
@ -0,0 +1,20 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Logger struct {
|
||||
*slog.Logger
|
||||
}
|
||||
|
||||
func New(module string) *Logger {
|
||||
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
})
|
||||
|
||||
base := slog.New(handler).With("module", module)
|
||||
|
||||
return &Logger{Logger: base}
|
||||
}
|
||||
60
framework/metrics/prometheus.go
Normal file
60
framework/metrics/prometheus.go
Normal file
@ -0,0 +1,60 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
var (
|
||||
httpRequestDuration = prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "http_request_duration_seconds",
|
||||
Help: "HTTP request duration in seconds",
|
||||
},
|
||||
[]string{"method", "endpoint", "status_code"},
|
||||
)
|
||||
|
||||
dbQueryDuration = prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "db_query_duration_seconds",
|
||||
Help: "Database query duration in seconds",
|
||||
},
|
||||
[]string{"operation"},
|
||||
)
|
||||
|
||||
batchInsertRows = prometheus.NewHistogram(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "batch_insert_rows",
|
||||
Help: "Number of rows inserted per batch",
|
||||
},
|
||||
)
|
||||
|
||||
cacheHits = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "cache_hits_total",
|
||||
Help: "Total number of cache hits",
|
||||
},
|
||||
[]string{"key"},
|
||||
)
|
||||
)
|
||||
|
||||
func init() {
|
||||
prometheus.MustRegister(httpRequestDuration, dbQueryDuration, batchInsertRows, cacheHits)
|
||||
}
|
||||
|
||||
func RecordHTTPRequest(method, endpoint, statusCode string, duration time.Duration) {
|
||||
httpRequestDuration.WithLabelValues(method, endpoint, statusCode).Observe(duration.Seconds())
|
||||
}
|
||||
|
||||
func RecordDatabaseQuery(operation string, duration time.Duration) {
|
||||
dbQueryDuration.WithLabelValues(operation).Observe(duration.Seconds())
|
||||
}
|
||||
|
||||
func RecordBatchInsert(rowCount int, duration time.Duration) {
|
||||
batchInsertRows.Observe(float64(rowCount))
|
||||
}
|
||||
|
||||
func RecordCacheHit(key string, duration time.Duration) {
|
||||
cacheHits.WithLabelValues(key).Inc()
|
||||
}
|
||||
48
framework/middleware/auth.go
Normal file
48
framework/middleware/auth.go
Normal file
@ -0,0 +1,48 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func AuthMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
token := c.GetHeader("Authorization")
|
||||
if token == "" {
|
||||
c.JSON(401, gin.H{
|
||||
"success": false,
|
||||
"message": "missing authorization token",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: verify token and set user context
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func RoleMiddleware(required []string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
role := c.GetString("user_role")
|
||||
|
||||
allowed := false
|
||||
for _, r := range required {
|
||||
if r == role {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !allowed {
|
||||
c.JSON(403, gin.H{
|
||||
"success": false,
|
||||
"message": "insufficient permissions",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
36
framework/middleware/logging.go
Normal file
36
framework/middleware/logging.go
Normal file
@ -0,0 +1,36 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/yourorg/go-dw-platform/framework/logger"
|
||||
"github.com/yourorg/go-dw-platform/framework/metrics"
|
||||
)
|
||||
|
||||
func Logging(log *logger.Logger) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
requestID := c.GetHeader("X-Request-ID")
|
||||
if requestID == "" {
|
||||
requestID = uuid.NewString()
|
||||
}
|
||||
c.Set("request_id", requestID)
|
||||
|
||||
start := time.Now()
|
||||
c.Next()
|
||||
duration := time.Since(start)
|
||||
|
||||
log.Info("request handled",
|
||||
"request_id", requestID,
|
||||
"method", c.Request.Method,
|
||||
"path", c.Request.URL.Path,
|
||||
"status", c.Writer.Status(),
|
||||
"duration_ms", duration.Milliseconds(),
|
||||
)
|
||||
|
||||
metrics.RecordHTTPRequest(c.Request.Method, c.FullPath(), strconv.Itoa(c.Writer.Status()), duration)
|
||||
}
|
||||
}
|
||||
24
framework/middleware/tracing.go
Normal file
24
framework/middleware/tracing.go
Normal file
@ -0,0 +1,24 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
)
|
||||
|
||||
func Tracing(serviceName string) gin.HandlerFunc {
|
||||
tracer := otel.Tracer(serviceName)
|
||||
|
||||
return func(c *gin.Context) {
|
||||
ctx, span := tracer.Start(c.Request.Context(), c.FullPath())
|
||||
defer span.End()
|
||||
|
||||
span.SetAttributes(
|
||||
attribute.String("http.method", c.Request.Method),
|
||||
attribute.String("http.path", c.Request.URL.Path),
|
||||
)
|
||||
|
||||
c.Request = c.Request.WithContext(ctx)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
30
framework/query/analyzer.go
Normal file
30
framework/query/analyzer.go
Normal file
@ -0,0 +1,30 @@
|
||||
package query
|
||||
|
||||
const (
|
||||
DefaultLimit = 100
|
||||
MaxLimit = 10000
|
||||
)
|
||||
|
||||
type Filter struct {
|
||||
StartDate string
|
||||
EndDate string
|
||||
Limit int
|
||||
Page int
|
||||
}
|
||||
|
||||
func NormalizeLimit(limit int) int {
|
||||
if limit <= 0 {
|
||||
return DefaultLimit
|
||||
}
|
||||
if limit > MaxLimit {
|
||||
return MaxLimit
|
||||
}
|
||||
return limit
|
||||
}
|
||||
|
||||
func Offset(page, limit int) int {
|
||||
if page <= 1 {
|
||||
return 0
|
||||
}
|
||||
return (page - 1) * limit
|
||||
}
|
||||
37
framework/query/cache.go
Normal file
37
framework/query/cache.go
Normal file
@ -0,0 +1,37 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
type Cache struct {
|
||||
client *redis.Client
|
||||
}
|
||||
|
||||
func NewCache(client *redis.Client) *Cache {
|
||||
return &Cache{client: client}
|
||||
}
|
||||
|
||||
func (c *Cache) Get(ctx context.Context, key string, dest any) error {
|
||||
val, err := c.client.Get(ctx, key).Bytes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(val, dest)
|
||||
}
|
||||
|
||||
func (c *Cache) Set(ctx context.Context, key string, value any, ttl time.Duration) error {
|
||||
data, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.client.Set(ctx, key, data, ttl).Err()
|
||||
}
|
||||
|
||||
func (c *Cache) Invalidate(ctx context.Context, key string) error {
|
||||
return c.client.Del(ctx, key).Err()
|
||||
}
|
||||
20
go.mod
20
go.mod
@ -1,20 +0,0 @@
|
||||
module repository.promas.id/prana/omnix-sopiga
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/jackc/pgx/v5 v5.5.0
|
||||
repository.promas.id/prana/go-dw-framework v0.0.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||
github.com/stretchr/testify v1.8.3 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
)
|
||||
|
||||
replace repository.promas.id/prana/go-dw-framework => ../go-dw-framework
|
||||
28
go.sum
28
go.sum
@ -1,28 +0,0 @@
|
||||
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/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-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.5.0 h1:NxstgwndsTRy7eq9/kqYc/BZh5w2hHJV86wjvO+1xPw=
|
||||
github.com/jackc/pgx/v5 v5.5.0/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
|
||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
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.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
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=
|
||||
7
go.work
Normal file
7
go.work
Normal file
@ -0,0 +1,7 @@
|
||||
go 1.25.0
|
||||
|
||||
use (
|
||||
./framework
|
||||
./services/omnix-broadcast
|
||||
./services/template-service
|
||||
)
|
||||
66
go.work.sum
Normal file
66
go.work.sum
Normal file
@ -0,0 +1,66 @@
|
||||
github.com/alecthomas/kingpin/v2 v2.3.2/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
|
||||
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
|
||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||
github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
|
||||
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
||||
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/jordanlewis/gcassert v0.0.0-20250430164644-389ef753e22e/go.mod h1:ZybsQk6DWyN5t7An1MuPm1gtSZ1xDaTXS9ZjIOxvQrk=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
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/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8=
|
||||
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
||||
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2/go.mod h1:b7fPSJ0pKZ3ccUh8gnTONJxhn3c/PS6tyzQvyqw4iA8=
|
||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
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.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
@ -1,11 +1,7 @@
|
||||
# omnix-sopiga
|
||||
# omnix-broadcast
|
||||
|
||||
Collection broadcast worker: mengirim invoice via WhatsApp (Sopiga Collar API) ke nasabah Gadai Mulia.
|
||||
|
||||
> Repo standalone (bukan bagian monorepo). Dependency internal ada di
|
||||
> [`go-dw-framework`](../go-dw-framework) — lihat `go.mod` untuk detail
|
||||
> `replace` directive yang dipakai selama development lokal.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
@ -41,10 +37,8 @@ Tiga komponen jalan bersamaan (lihat `main.go`):
|
||||
|
||||
Template baru cukup didaftarkan lewat database (`sopiga_template_config` +
|
||||
`template_variable_mapping` + `sopiga_collar_config`) — tanpa redeploy kode.
|
||||
Lihat [docs/implementasi_guide.md](docs/implementasi_guide.md) dan
|
||||
[docs/quick_start.md](docs/quick_start.md) untuk panduan lengkap. Detail
|
||||
permintaan integrasi webhook ke tim Omnix ada di
|
||||
[docs/webhook_integration_request.md](docs/webhook_integration_request.md).
|
||||
Lihat [docs/implementasi_guide.md](../../docs/implementasi_guide.md) dan
|
||||
[docs/quick_start.md](../../docs/quick_start.md) untuk panduan lengkap.
|
||||
|
||||
## Layers
|
||||
|
||||
@ -79,26 +73,6 @@ permintaan integrasi webhook ke tim Omnix ada di
|
||||
| `WEBHOOK_PORT` | 8081 |
|
||||
| `WEBHOOK_SECRET` | — (HMAC-SHA256 shared secret; kosongkan untuk skip verifikasi saat dev) |
|
||||
|
||||
## Development setup
|
||||
|
||||
Clone this repo and `go-dw-framework` as **siblings** (same parent folder) —
|
||||
`go.mod` uses a local `replace` directive during pre-release development:
|
||||
|
||||
```
|
||||
some-folder/
|
||||
├── go-dw-framework/
|
||||
└── omnix-sopiga/ ← you are here
|
||||
```
|
||||
|
||||
```bash
|
||||
git clone <go-dw-framework-remote> ../go-dw-framework
|
||||
go build ./...
|
||||
```
|
||||
|
||||
Once `go-dw-framework` is tagged (e.g. `v0.1.0`) and pushed to its remote,
|
||||
switch `go.mod` to a plain `require repository.promas.id/prana/go-dw-framework v0.1.0`
|
||||
(drop the `replace` line) and `go mod tidy`.
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
@ -107,14 +81,7 @@ psql -d gadai_mulia -f migrations/001_create_collection_broadcasts.up.sql
|
||||
export DATABASE_DSN="postgres://user:pass@localhost:5432/gadai_mulia"
|
||||
export SOPIGA_TOKEN="your_api_token"
|
||||
|
||||
go run .
|
||||
```
|
||||
|
||||
## Docker build
|
||||
|
||||
```bash
|
||||
# from the parent folder containing both omnix-sopiga/ and go-dw-framework/
|
||||
docker build -f omnix-sopiga/Dockerfile -t omnix-sopiga ..
|
||||
go run ./...
|
||||
```
|
||||
|
||||
## Test
|
||||
@ -9,7 +9,7 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"repository.promas.id/prana/omnix-sopiga/dto"
|
||||
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/dto"
|
||||
)
|
||||
|
||||
type SopigaClient struct {
|
||||
@ -3,7 +3,7 @@ package main
|
||||
import (
|
||||
"time"
|
||||
|
||||
fwconfig "repository.promas.id/prana/go-dw-framework/config"
|
||||
fwconfig "github.com/yourorg/go-dw-platform/framework/config"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
20
services/omnix-broadcast/go.mod
Normal file
20
services/omnix-broadcast/go.mod
Normal file
@ -0,0 +1,20 @@
|
||||
module github.com/yourorg/go-dw-platform/services/omnix-broadcast
|
||||
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/jackc/pgx/v5 v5.5.0
|
||||
github.com/yourorg/go-dw-platform/framework v0.0.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||
github.com/stretchr/testify v1.8.3 // indirect
|
||||
golang.org/x/crypto v0.9.0 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/text v0.9.0 // indirect
|
||||
)
|
||||
|
||||
replace github.com/yourorg/go-dw-platform/framework => ../../framework
|
||||
11
services/omnix-broadcast/go.sum
Normal file
11
services/omnix-broadcast/go.sum
Normal file
@ -0,0 +1,11 @@
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||
github.com/jackc/pgx/v5 v5.5.0 h1:NxstgwndsTRy7eq9/kqYc/BZh5w2hHJV86wjvO+1xPw=
|
||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
||||
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
@ -9,10 +9,10 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
fwlogger "repository.promas.id/prana/go-dw-framework/logger"
|
||||
fwlogger "github.com/yourorg/go-dw-platform/framework/logger"
|
||||
|
||||
"repository.promas.id/prana/omnix-sopiga/dto"
|
||||
"repository.promas.id/prana/omnix-sopiga/service"
|
||||
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/dto"
|
||||
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/service"
|
||||
)
|
||||
|
||||
const signatureHeader = "X-Sopiga-Signature"
|
||||
@ -8,17 +8,17 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
fwconfig "repository.promas.id/prana/go-dw-framework/config"
|
||||
fwdb "repository.promas.id/prana/go-dw-framework/db"
|
||||
fwingestion "repository.promas.id/prana/go-dw-framework/ingestion"
|
||||
fwlogger "repository.promas.id/prana/go-dw-framework/logger"
|
||||
fwconfig "github.com/yourorg/go-dw-platform/framework/config"
|
||||
fwdb "github.com/yourorg/go-dw-platform/framework/db"
|
||||
fwingestion "github.com/yourorg/go-dw-platform/framework/ingestion"
|
||||
fwlogger "github.com/yourorg/go-dw-platform/framework/logger"
|
||||
|
||||
"repository.promas.id/prana/omnix-sopiga/client"
|
||||
"repository.promas.id/prana/omnix-sopiga/handler"
|
||||
"repository.promas.id/prana/omnix-sopiga/repository"
|
||||
"repository.promas.id/prana/omnix-sopiga/service"
|
||||
"repository.promas.id/prana/omnix-sopiga/transformer"
|
||||
"repository.promas.id/prana/omnix-sopiga/worker"
|
||||
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/client"
|
||||
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/handler"
|
||||
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/repository"
|
||||
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/service"
|
||||
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/transformer"
|
||||
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/worker"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@ -7,7 +7,7 @@ import (
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"repository.promas.id/prana/omnix-sopiga/entity"
|
||||
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/entity"
|
||||
)
|
||||
|
||||
type Repository struct {
|
||||
@ -4,15 +4,15 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
fwingestion "repository.promas.id/prana/go-dw-framework/ingestion"
|
||||
fwlogger "repository.promas.id/prana/go-dw-framework/logger"
|
||||
fwingestion "github.com/yourorg/go-dw-platform/framework/ingestion"
|
||||
fwlogger "github.com/yourorg/go-dw-platform/framework/logger"
|
||||
|
||||
"repository.promas.id/prana/omnix-sopiga/client"
|
||||
"repository.promas.id/prana/omnix-sopiga/domain"
|
||||
"repository.promas.id/prana/omnix-sopiga/dto"
|
||||
"repository.promas.id/prana/omnix-sopiga/repository"
|
||||
"repository.promas.id/prana/omnix-sopiga/transformer"
|
||||
"repository.promas.id/prana/omnix-sopiga/validator"
|
||||
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/client"
|
||||
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/domain"
|
||||
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/dto"
|
||||
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/repository"
|
||||
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/transformer"
|
||||
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/validator"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
@ -3,8 +3,8 @@ package tests
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"repository.promas.id/prana/omnix-sopiga/domain"
|
||||
"repository.promas.id/prana/omnix-sopiga/transformer"
|
||||
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/domain"
|
||||
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/transformer"
|
||||
)
|
||||
|
||||
func TestBuildDynamicMessage_Ordered(t *testing.T) {
|
||||
@ -3,8 +3,8 @@ package tests
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"repository.promas.id/prana/omnix-sopiga/domain"
|
||||
"repository.promas.id/prana/omnix-sopiga/validator"
|
||||
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/domain"
|
||||
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/validator"
|
||||
)
|
||||
|
||||
func TestValidateBroadcastPayload_MissingPhone(t *testing.T) {
|
||||
@ -5,8 +5,8 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"repository.promas.id/prana/omnix-sopiga/domain"
|
||||
"repository.promas.id/prana/omnix-sopiga/entity"
|
||||
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/domain"
|
||||
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/entity"
|
||||
)
|
||||
|
||||
type Transformer struct{}
|
||||
@ -3,7 +3,7 @@ package validator
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"repository.promas.id/prana/omnix-sopiga/domain"
|
||||
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/domain"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -4,10 +4,10 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
fwlogger "repository.promas.id/prana/go-dw-framework/logger"
|
||||
fwlogger "github.com/yourorg/go-dw-platform/framework/logger"
|
||||
|
||||
"repository.promas.id/prana/omnix-sopiga/domain"
|
||||
"repository.promas.id/prana/omnix-sopiga/service"
|
||||
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/domain"
|
||||
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/service"
|
||||
)
|
||||
|
||||
const maxConcurrency = 5
|
||||
@ -4,10 +4,10 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
fwlogger "repository.promas.id/prana/go-dw-framework/logger"
|
||||
fwlogger "github.com/yourorg/go-dw-platform/framework/logger"
|
||||
|
||||
"repository.promas.id/prana/omnix-sopiga/domain"
|
||||
"repository.promas.id/prana/omnix-sopiga/service"
|
||||
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/domain"
|
||||
"github.com/yourorg/go-dw-platform/services/omnix-broadcast/service"
|
||||
)
|
||||
|
||||
type DeliverySyncWorker struct {
|
||||
11
services/template-service/.env.example
Normal file
11
services/template-service/.env.example
Normal file
@ -0,0 +1,11 @@
|
||||
PORT=8080
|
||||
|
||||
DATABASE_DSN=postgres://user:pass@localhost:5432/gadai_mulia
|
||||
DB_MAX_CONNS=25
|
||||
DB_MIN_CONNS=5
|
||||
|
||||
BATCH_SIZE=5000
|
||||
BATCH_TIMEOUT_SEC=30
|
||||
BATCH_MAX_RETRIES=3
|
||||
|
||||
LOG_LEVEL=info
|
||||
49
services/template-service/config.go
Normal file
49
services/template-service/config.go
Normal file
@ -0,0 +1,49 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
fwconfig "github.com/yourorg/go-dw-platform/framework/config"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Port string
|
||||
DB DBConfig
|
||||
Batch BatchConfig
|
||||
LogLevel string
|
||||
}
|
||||
|
||||
type DBConfig struct {
|
||||
DSN string
|
||||
MaxConns int32
|
||||
MinConns int32
|
||||
MaxConnLifetime time.Duration
|
||||
MaxConnIdleTime time.Duration
|
||||
}
|
||||
|
||||
type BatchConfig struct {
|
||||
Size int
|
||||
TimeoutSec int
|
||||
MaxRetries int
|
||||
RetryDelay time.Duration
|
||||
}
|
||||
|
||||
func LoadConfig() *Config {
|
||||
return &Config{
|
||||
Port: fwconfig.GetString("PORT", "8080"),
|
||||
DB: DBConfig{
|
||||
DSN: fwconfig.GetString("DATABASE_DSN", ""),
|
||||
MaxConns: int32(fwconfig.GetInt("DB_MAX_CONNS", 25)),
|
||||
MinConns: int32(fwconfig.GetInt("DB_MIN_CONNS", 5)),
|
||||
MaxConnLifetime: 15 * time.Minute,
|
||||
MaxConnIdleTime: 5 * time.Minute,
|
||||
},
|
||||
Batch: BatchConfig{
|
||||
Size: fwconfig.GetInt("BATCH_SIZE", 5000),
|
||||
TimeoutSec: fwconfig.GetInt("BATCH_TIMEOUT_SEC", 30),
|
||||
MaxRetries: fwconfig.GetInt("BATCH_MAX_RETRIES", 3),
|
||||
RetryDelay: time.Second,
|
||||
},
|
||||
LogLevel: fwconfig.GetString("LOG_LEVEL", "info"),
|
||||
}
|
||||
}
|
||||
30
services/template-service/domain/order.go
Normal file
30
services/template-service/domain/order.go
Normal file
@ -0,0 +1,30 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Order struct {
|
||||
OrderID string
|
||||
CustomerID string
|
||||
Amount float64
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
func (o *Order) Validate() error {
|
||||
if o.OrderID == "" {
|
||||
return fmt.Errorf("order_id is required")
|
||||
}
|
||||
if o.CustomerID == "" {
|
||||
return fmt.Errorf("customer_id is required")
|
||||
}
|
||||
if o.Amount <= 0 {
|
||||
return fmt.Errorf("amount must be positive")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Order) NaturalKey() string {
|
||||
return fmt.Sprintf("%s_%s_%d", o.OrderID, o.CustomerID, o.CreatedAt.Unix())
|
||||
}
|
||||
7
services/template-service/dto/request.go
Normal file
7
services/template-service/dto/request.go
Normal file
@ -0,0 +1,7 @@
|
||||
package dto
|
||||
|
||||
type IngestRequest struct {
|
||||
OrderID string `json:"order_id" binding:"required"`
|
||||
CustomerID string `json:"customer_id" binding:"required"`
|
||||
Amount float64 `json:"amount" binding:"required"`
|
||||
}
|
||||
20
services/template-service/dto/response.go
Normal file
20
services/template-service/dto/response.go
Normal file
@ -0,0 +1,20 @@
|
||||
package dto
|
||||
|
||||
type ProcessResult struct {
|
||||
Status string `json:"status"`
|
||||
RowsInserted int64 `json:"rows_inserted,omitempty"`
|
||||
}
|
||||
|
||||
type SuccessResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Data any `json:"data,omitempty"`
|
||||
RequestID string `json:"request_id"`
|
||||
}
|
||||
|
||||
type ErrorResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
ErrorCode string `json:"error_code,omitempty"`
|
||||
RequestID string `json:"request_id,omitempty"`
|
||||
}
|
||||
10
services/template-service/entity/order.go
Normal file
10
services/template-service/entity/order.go
Normal file
@ -0,0 +1,10 @@
|
||||
package entity
|
||||
|
||||
import "time"
|
||||
|
||||
type Order struct {
|
||||
OrderID string `db:"order_id"`
|
||||
CustomerID string `db:"customer_id"`
|
||||
Amount float64 `db:"amount"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
}
|
||||
11
services/template-service/go.mod
Normal file
11
services/template-service/go.mod
Normal file
@ -0,0 +1,11 @@
|
||||
module github.com/yourorg/go-dw-platform/services/template-service
|
||||
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/jackc/pgx/v5 v5.5.0
|
||||
github.com/yourorg/go-dw-platform/framework v0.0.0
|
||||
)
|
||||
|
||||
replace github.com/yourorg/go-dw-platform/framework => ../../framework
|
||||
28
services/template-service/handler/health.go
Normal file
28
services/template-service/handler/health.go
Normal file
@ -0,0 +1,28 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
type HealthHandler struct {
|
||||
pool *pgxpool.Pool
|
||||
}
|
||||
|
||||
func NewHealthHandler(pool *pgxpool.Pool) *HealthHandler {
|
||||
return &HealthHandler{pool: pool}
|
||||
}
|
||||
|
||||
func (h *HealthHandler) Health(c *gin.Context) {
|
||||
dbOK := h.pool.Ping(c.Request.Context()) == nil
|
||||
|
||||
status := "healthy"
|
||||
if !dbOK {
|
||||
status = "degraded"
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"status": status,
|
||||
"database": dbOK,
|
||||
})
|
||||
}
|
||||
71
services/template-service/handler/ingest.go
Normal file
71
services/template-service/handler/ingest.go
Normal file
@ -0,0 +1,71 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
fwlogger "github.com/yourorg/go-dw-platform/framework/logger"
|
||||
|
||||
"github.com/yourorg/go-dw-platform/services/template-service/dto"
|
||||
"github.com/yourorg/go-dw-platform/services/template-service/service"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
service *service.Service
|
||||
logger *fwlogger.Logger
|
||||
}
|
||||
|
||||
func New(svc *service.Service, log *fwlogger.Logger) *Handler {
|
||||
return &Handler{service: svc, logger: log}
|
||||
}
|
||||
|
||||
func (h *Handler) IngestOrders(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
requestID := c.GetString("request_id")
|
||||
|
||||
var req dto.IngestRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
h.logger.Error("validation failed",
|
||||
"request_id", requestID,
|
||||
"error", err)
|
||||
c.JSON(400, dto.ErrorResponse{
|
||||
Success: false,
|
||||
Message: "validation failed",
|
||||
ErrorCode: "VALIDATION_ERROR",
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.service.Process(ctx, &req)
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrDuplicate) {
|
||||
c.JSON(409, dto.ErrorResponse{
|
||||
Success: false,
|
||||
Message: "duplicate order",
|
||||
ErrorCode: "DUPLICATE_ORDER",
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Error("process failed",
|
||||
"request_id", requestID,
|
||||
"error", err)
|
||||
c.JSON(500, dto.ErrorResponse{
|
||||
Success: false,
|
||||
Message: "failed to process order",
|
||||
ErrorCode: "INTERNAL_ERROR",
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, dto.SuccessResponse{
|
||||
Success: true,
|
||||
Message: "order processed successfully",
|
||||
Data: result,
|
||||
RequestID: requestID,
|
||||
})
|
||||
}
|
||||
76
services/template-service/main.go
Normal file
76
services/template-service/main.go
Normal file
@ -0,0 +1,76 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
fwdb "github.com/yourorg/go-dw-platform/framework/db"
|
||||
fwlogger "github.com/yourorg/go-dw-platform/framework/logger"
|
||||
fwmiddleware "github.com/yourorg/go-dw-platform/framework/middleware"
|
||||
|
||||
"github.com/yourorg/go-dw-platform/services/template-service/handler"
|
||||
"github.com/yourorg/go-dw-platform/services/template-service/repository"
|
||||
"github.com/yourorg/go-dw-platform/services/template-service/service"
|
||||
"github.com/yourorg/go-dw-platform/services/template-service/transformer"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg := LoadConfig()
|
||||
log := fwlogger.New("template-service")
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
pool, err := fwdb.NewPool(ctx, fwdb.Config{
|
||||
DSN: cfg.DB.DSN,
|
||||
MaxConns: cfg.DB.MaxConns,
|
||||
MinConns: cfg.DB.MinConns,
|
||||
MaxConnLifetime: cfg.DB.MaxConnLifetime,
|
||||
MaxConnIdleTime: cfg.DB.MaxConnIdleTime,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("failed to connect to database", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer pool.Close()
|
||||
|
||||
repo := repository.New(pool)
|
||||
tf := transformer.New()
|
||||
svc := service.New(
|
||||
repo,
|
||||
tf,
|
||||
cfg.Batch.Size,
|
||||
time.Duration(cfg.Batch.TimeoutSec)*time.Second,
|
||||
cfg.Batch.MaxRetries,
|
||||
cfg.Batch.RetryDelay,
|
||||
)
|
||||
|
||||
ingestHandler := handler.New(svc, log)
|
||||
healthHandler := handler.NewHealthHandler(pool)
|
||||
|
||||
router := gin.New()
|
||||
router.Use(fwmiddleware.Logging(log))
|
||||
router.Use(gin.Recovery())
|
||||
|
||||
router.GET("/health", healthHandler.Health)
|
||||
|
||||
api := router.Group("/api")
|
||||
api.Use(fwmiddleware.AuthMiddleware())
|
||||
api.POST("/ingest", ingestHandler.IngestOrders)
|
||||
|
||||
go func() {
|
||||
if err := router.Run(":" + cfg.Port); err != nil {
|
||||
log.Error("server stopped", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-sigChan
|
||||
|
||||
log.Info("shutting down gracefully")
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS orders;
|
||||
@ -0,0 +1,11 @@
|
||||
CREATE TABLE IF NOT EXISTS orders (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
order_id VARCHAR(64) NOT NULL,
|
||||
customer_id VARCHAR(64) NOT NULL,
|
||||
amount NUMERIC(18, 2) NOT NULL,
|
||||
natural_key VARCHAR(255) NOT NULL UNIQUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_orders_customer_created
|
||||
ON orders (customer_id, created_at);
|
||||
103
services/template-service/repository/orders.go
Normal file
103
services/template-service/repository/orders.go
Normal file
@ -0,0 +1,103 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"github.com/yourorg/go-dw-platform/services/template-service/entity"
|
||||
)
|
||||
|
||||
type Filter struct {
|
||||
StartDate time.Time
|
||||
EndDate time.Time
|
||||
Limit int
|
||||
}
|
||||
|
||||
type Repository struct {
|
||||
pool *pgxpool.Pool
|
||||
}
|
||||
|
||||
func New(pool *pgxpool.Pool) *Repository {
|
||||
return &Repository{pool: pool}
|
||||
}
|
||||
|
||||
func (r *Repository) ExistsByKey(ctx context.Context, naturalKey string) (bool, error) {
|
||||
var exists bool
|
||||
err := r.pool.QueryRow(ctx,
|
||||
`SELECT EXISTS(SELECT 1 FROM orders WHERE natural_key = $1)`,
|
||||
naturalKey,
|
||||
).Scan(&exists)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("exists by key: %w", err)
|
||||
}
|
||||
return exists, nil
|
||||
}
|
||||
|
||||
func (r *Repository) InsertBatch(ctx context.Context, entities []*entity.Order) (int64, error) {
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("begin tx: %w", err)
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
stmt := `INSERT INTO orders (order_id, customer_id, amount, natural_key, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5)`
|
||||
|
||||
batch := &pgx.Batch{}
|
||||
for _, e := range entities {
|
||||
naturalKey := fmt.Sprintf("%s_%s_%d", e.OrderID, e.CustomerID, e.CreatedAt.Unix())
|
||||
batch.Queue(stmt, e.OrderID, e.CustomerID, e.Amount, naturalKey, e.CreatedAt)
|
||||
}
|
||||
|
||||
results := tx.SendBatch(ctx, batch)
|
||||
|
||||
var rowsInserted int64
|
||||
for i := 0; i < len(entities); i++ {
|
||||
tag, err := results.Exec()
|
||||
if err != nil {
|
||||
results.Close()
|
||||
return 0, fmt.Errorf("exec batch: %w", err)
|
||||
}
|
||||
rowsInserted += tag.RowsAffected()
|
||||
}
|
||||
if err := results.Close(); err != nil {
|
||||
return 0, fmt.Errorf("close batch results: %w", err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return 0, fmt.Errorf("commit: %w", err)
|
||||
}
|
||||
|
||||
return rowsInserted, nil
|
||||
}
|
||||
|
||||
func (r *Repository) FindByFilters(ctx context.Context, filter *Filter) ([]*entity.Order, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
query := `SELECT order_id, customer_id, amount, created_at
|
||||
FROM orders
|
||||
WHERE created_at >= $1 AND created_at <= $2
|
||||
LIMIT $3`
|
||||
|
||||
rows, err := r.pool.Query(ctx, query, filter.StartDate, filter.EndDate, filter.Limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var orders []*entity.Order
|
||||
for rows.Next() {
|
||||
var o entity.Order
|
||||
if err := rows.Scan(&o.OrderID, &o.CustomerID, &o.Amount, &o.CreatedAt); err != nil {
|
||||
return nil, fmt.Errorf("scan: %w", err)
|
||||
}
|
||||
orders = append(orders, &o)
|
||||
}
|
||||
|
||||
return orders, rows.Err()
|
||||
}
|
||||
83
services/template-service/service/orders.go
Normal file
83
services/template-service/service/orders.go
Normal file
@ -0,0 +1,83 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
fwingestion "github.com/yourorg/go-dw-platform/framework/ingestion"
|
||||
fwmetrics "github.com/yourorg/go-dw-platform/framework/metrics"
|
||||
|
||||
"github.com/yourorg/go-dw-platform/services/template-service/domain"
|
||||
"github.com/yourorg/go-dw-platform/services/template-service/dto"
|
||||
"github.com/yourorg/go-dw-platform/services/template-service/entity"
|
||||
"github.com/yourorg/go-dw-platform/services/template-service/repository"
|
||||
"github.com/yourorg/go-dw-platform/services/template-service/transformer"
|
||||
)
|
||||
|
||||
var ErrDuplicate = errors.New("duplicate order")
|
||||
|
||||
type Service struct {
|
||||
repository *repository.Repository
|
||||
transformer *transformer.Transformer
|
||||
batcher *fwingestion.Batcher[*domain.Order]
|
||||
retrier *fwingestion.Retrier
|
||||
}
|
||||
|
||||
func New(repo *repository.Repository, tf *transformer.Transformer, batchSize int, batchTimeout time.Duration, maxRetries int, retryDelay time.Duration) *Service {
|
||||
return &Service{
|
||||
repository: repo,
|
||||
transformer: tf,
|
||||
batcher: fwingestion.NewBatcher[*domain.Order](batchSize, batchTimeout),
|
||||
retrier: fwingestion.NewRetrier(maxRetries, retryDelay),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Process(ctx context.Context, req *dto.IngestRequest) (*dto.ProcessResult, error) {
|
||||
order, err := s.transformer.RequestToDomain(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("process: %w", err)
|
||||
}
|
||||
|
||||
exists, err := s.repository.ExistsByKey(ctx, order.NaturalKey())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("check duplicate: %w", err)
|
||||
}
|
||||
if exists {
|
||||
return nil, ErrDuplicate
|
||||
}
|
||||
|
||||
s.batcher.Add(order)
|
||||
|
||||
if s.batcher.IsFull() || s.batcher.IsExpired() {
|
||||
return s.FlushBatch(ctx)
|
||||
}
|
||||
|
||||
return &dto.ProcessResult{Status: "queued"}, nil
|
||||
}
|
||||
|
||||
func (s *Service) FlushBatch(ctx context.Context) (*dto.ProcessResult, error) {
|
||||
start := time.Now()
|
||||
batch := s.batcher.Get()
|
||||
|
||||
entities := make([]*entity.Order, len(batch))
|
||||
for i, d := range batch {
|
||||
entities[i] = s.transformer.DomainToEntity(d)
|
||||
}
|
||||
|
||||
rows, err := s.retrier.Do(ctx, func() (int64, error) {
|
||||
return s.repository.InsertBatch(ctx, entities)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("flush batch: %w", err)
|
||||
}
|
||||
|
||||
fwmetrics.RecordBatchInsert(len(batch), time.Since(start))
|
||||
|
||||
s.batcher.Clear()
|
||||
return &dto.ProcessResult{
|
||||
Status: "success",
|
||||
RowsInserted: rows,
|
||||
}, nil
|
||||
}
|
||||
16
services/template-service/tests/fixtures.go
Normal file
16
services/template-service/tests/fixtures.go
Normal file
@ -0,0 +1,16 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/yourorg/go-dw-platform/services/template-service/domain"
|
||||
)
|
||||
|
||||
func NewTestOrder(orderID, customerID string, amount float64) *domain.Order {
|
||||
return &domain.Order{
|
||||
OrderID: orderID,
|
||||
CustomerID: customerID,
|
||||
Amount: amount,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
26
services/template-service/tests/service_test.go
Normal file
26
services/template-service/tests/service_test.go
Normal file
@ -0,0 +1,26 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/yourorg/go-dw-platform/services/template-service/dto"
|
||||
"github.com/yourorg/go-dw-platform/services/template-service/validator"
|
||||
)
|
||||
|
||||
func TestValidateIngestRequest_MissingOrderID(t *testing.T) {
|
||||
req := &dto.IngestRequest{CustomerID: "cust-1", Amount: 100}
|
||||
|
||||
err := validator.ValidateIngestRequest(req)
|
||||
|
||||
if err != validator.ErrMissingOrderID {
|
||||
t.Fatalf("expected ErrMissingOrderID, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateIngestRequest_Valid(t *testing.T) {
|
||||
req := &dto.IngestRequest{OrderID: "ord-1", CustomerID: "cust-1", Amount: 100}
|
||||
|
||||
if err := validator.ValidateIngestRequest(req); err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
}
|
||||
40
services/template-service/transformer/orders.go
Normal file
40
services/template-service/transformer/orders.go
Normal file
@ -0,0 +1,40 @@
|
||||
package transformer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/yourorg/go-dw-platform/services/template-service/domain"
|
||||
"github.com/yourorg/go-dw-platform/services/template-service/dto"
|
||||
"github.com/yourorg/go-dw-platform/services/template-service/entity"
|
||||
)
|
||||
|
||||
type Transformer struct{}
|
||||
|
||||
func New() *Transformer {
|
||||
return &Transformer{}
|
||||
}
|
||||
|
||||
func (t *Transformer) RequestToDomain(req *dto.IngestRequest) (*domain.Order, error) {
|
||||
order := &domain.Order{
|
||||
OrderID: req.OrderID,
|
||||
CustomerID: req.CustomerID,
|
||||
Amount: req.Amount,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := order.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("transform: %w", err)
|
||||
}
|
||||
|
||||
return order, nil
|
||||
}
|
||||
|
||||
func (t *Transformer) DomainToEntity(d *domain.Order) *entity.Order {
|
||||
return &entity.Order{
|
||||
OrderID: d.OrderID,
|
||||
CustomerID: d.CustomerID,
|
||||
Amount: d.Amount,
|
||||
CreatedAt: d.CreatedAt,
|
||||
}
|
||||
}
|
||||
22
services/template-service/validator/orders.go
Normal file
22
services/template-service/validator/orders.go
Normal file
@ -0,0 +1,22 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/yourorg/go-dw-platform/services/template-service/dto"
|
||||
)
|
||||
|
||||
var ErrMissingOrderID = errors.New("order_id is required")
|
||||
|
||||
func ValidateIngestRequest(req *dto.IngestRequest) error {
|
||||
if req.OrderID == "" {
|
||||
return ErrMissingOrderID
|
||||
}
|
||||
if req.CustomerID == "" {
|
||||
return errors.New("customer_id is required")
|
||||
}
|
||||
if req.Amount <= 0 {
|
||||
return errors.New("amount must be positive")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user