chore(init): Create Initial repo

Upload First repo
This commit is contained in:
ulflow_phattt2901 2025-05-21 12:39:40 +07:00
commit 86cff0489e
48 changed files with 4839 additions and 0 deletions

58
.air.toml Normal file
View File

@ -0,0 +1,58 @@
# Air Tomb Configuration for ULFlow Starter Kit
root = "."
tmp_dir = "tmp"
[build]
# Just plain old shell command. You could use `make` as well.
cmd = "go build -o ./tmp/main.exe ./cmd/app"
# Binary file yields from `cmd`.
bin = "tmp/main.exe"
# Customize binary.
full_bin = "./tmp/main.exe"
# Watch these filename extensions.
include_ext = ["go", "tpl", "tmpl", "html"]
# Ignore these filename extensions or directories.
exclude_dir = ["assets", "tmp", "vendor", ".git", "node_modules"]
# Watch these directories if you specified.
include_dir = []
# Exclude files.
exclude_file = []
# This log file places in your tmp_dir.
log = "air.log"
# It's not necessary to trigger build each time file changes if it's too frequent.
delay = 1000 # ms
# Stop running old binary when build errors occur.
stop_on_error = true
# Send Interrupt signal before killing process (windows does not support this feature)
send_interrupt = true
# Delay after sending Interrupt signal
kill_delay = 500 # ms
[log]
# Show log time
time = true
[color]
# Customize each part's color.
main = "magenta"
watcher = "cyan"
build = "yellow"
runner = "green"
[misc]
# Delete tmp directory on exit
clean_on_exit = true
[screen]
clear_on_rebuild = true
keep_scroll = true
[tomb]
# Enable Tomb for graceful shutdown
enabled = true
# Kill signal to use for graceful shutdown
signal = "SIGTERM"
# Timeout for graceful shutdown
timeout = "5s"
# Path to the tomb config file
config = "./tomb.yaml"

5
.feature-flags.json Normal file
View File

@ -0,0 +1,5 @@
{
"enable_database": {
"enabled": false
}
}

View File

@ -0,0 +1,14 @@
# <type>: <subject>
# <body>
# <footer>
# Types:
# feat (new feature)
# fix (bug fix)
# docs (documentation changes)
# style (formatting, no code change)
# refactor (refactoring code)
# test (adding tests, refactoring tests)
# chore (updating tasks etc; no production code change)

48
.gitea/hooks/pre-commit Normal file
View File

@ -0,0 +1,48 @@
#!/bin/sh
# Pre-commit hook for ULFlow Golang Starter Kit
# This hook runs linters and checks before allowing a commit
echo "Running pre-commit checks..."
# Check for staged Go files
STAGED_GO_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep "\.go$")
if [[ "$STAGED_GO_FILES" = "" ]]; then
echo "No Go files staged for commit. Skipping Go-specific checks."
else
# Run golangci-lint on staged files
echo "Running golangci-lint..."
golangci-lint run $STAGED_GO_FILES
if [ $? -ne 0 ]; then
echo "golangci-lint failed. Please fix the issues before committing."
exit 1
fi
# Check for formatting issues
echo "Checking Go formatting..."
gofmt -l $STAGED_GO_FILES
if [ $? -ne 0 ]; then
echo "gofmt failed. Please run 'gofmt -w' on your files before committing."
exit 1
fi
# Run tests related to staged files
echo "Running relevant tests..."
go test $(go list ./... | grep -v vendor) -short
if [ $? -ne 0 ]; then
echo "Tests failed. Please fix the tests before committing."
exit 1
fi
fi
# Check for sensitive information in staged files
echo "Checking for sensitive information..."
if git diff --cached | grep -E "(API_KEY|SECRET|PASSWORD|TOKEN).*[A-Za-z0-9_/-]{8,}" > /dev/null; then
echo "WARNING: Possible sensitive information detected in commit."
echo "Please verify you're not committing secrets, credentials or personal data."
echo "Press Enter to continue or Ctrl+C to abort the commit."
read
fi
echo "Pre-commit checks passed!"
exit 0

View File

@ -0,0 +1,34 @@
#!/bin/sh
# prepare-commit-msg hook for ULFlow Golang Starter Kit
# This hook prepares the commit message template
COMMIT_MSG_FILE=$1
COMMIT_SOURCE=$2
SHA1=$3
# Only add template if this is not a merge, amend, or rebase
if [ -z "$COMMIT_SOURCE" ]; then
# Check the current branch name to suggest commit type
BRANCH_NAME=$(git symbolic-ref --short HEAD)
# Extract type from branch name (e.g., feat/123-user-auth -> feat)
BRANCH_TYPE=$(echo $BRANCH_NAME | cut -d '/' -f 1)
# If branch follows naming convention, prepopulate commit message
if [[ "$BRANCH_TYPE" =~ ^(feat|fix|docs|style|refactor|test|chore)$ ]]; then
# Get the first line of the current commit message
first_line=$(head -n 1 $COMMIT_MSG_FILE)
# If the first line doesn't already have the type, prepend it
if [[ ! "$first_line" =~ ^$BRANCH_TYPE ]]; then
# Read existing commit msg
commit_msg=$(cat $COMMIT_MSG_FILE)
# Create new commit msg with suggested type
echo "$BRANCH_TYPE: " > $COMMIT_MSG_FILE
echo "$commit_msg" >> $COMMIT_MSG_FILE
fi
fi
fi
exit 0

126
.gitea/workflows/ci.yml Normal file
View File

@ -0,0 +1,126 @@
name: CI Pipeline
on:
push:
branches-ignore: [ main ]
pull_request:
branches: [ main ]
jobs:
lint:
name: Lint
runs-on: ${{ secrets.RUNNER_LABEL || 'ubuntu-latest' }}
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.23'
cache-dependency-path: go.sum
- name: Lint
uses: golangci/golangci-lint-action@v6
with:
version: latest
args: --timeout=5m
- name: Notify on failure
if: failure()
run: echo "::warning::Linting failed. Please fix code style issues."
security_scan:
name: Security Scan
runs-on: ${{ secrets.RUNNER_LABEL || 'ubuntu-latest' }}
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.23'
cache-dependency-path: go.sum
- name: Run Go Vulnerability Check
uses: golang/govulncheck-action@v1
- name: Notify on security issues
if: failure()
run: echo "::error::Security vulnerabilities detected. Please review dependencies."
test:
name: Test
runs-on: ${{ secrets.RUNNER_LABEL || 'ubuntu-latest' }}
needs: lint
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.23'
cache-dependency-path: go.sum
- name: Install go-junit-report
run: go install github.com/jstemmer/go-junit-report@latest
- name: Test
run: |
go test -v -race -coverprofile=coverage.txt -covermode=atomic ./... | tee test-output.log
go tool cover -func=coverage.txt
- name: Generate test report
if: always()
run: cat test-output.log | go-junit-report > junit-report.xml
- name: Upload coverage
uses: codecov/codecov-action@v3
- name: Upload test report
if: always()
uses: actions/upload-artifact@v3
with:
name: test-reports
path: junit-report.xml
build:
name: Build
runs-on: ${{ secrets.RUNNER_LABEL || 'ubuntu-latest' }}
needs: [test, security_scan]
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.23'
cache-dependency-path: go.sum
- name: Build
run: |
APP_VERSION="dev-${{ gitea.sha }}"
go build -v -ldflags="-s -w -X main.version=${APP_VERSION}" -o ./bin/api ./cmd/api
- name: Upload build artifact
uses: actions/upload-artifact@v3
with:
name: api-binary
path: ./bin/api
- name: Notify on success
if: success()
run: echo "::notice::Build successful. Ready for review and testing."
notify:
name: Notification
runs-on: ${{ secrets.RUNNER_LABEL || 'ubuntu-latest' }}
needs: [lint, test, security_scan, build]
if: always()
steps:
- name: Notify result
run: |
if [[ "${{ needs.build.result }}" == "success" ]]; then
echo "::notice::CI Pipeline completed successfully. Branch is ready for review."
else
echo "::warning::CI Pipeline failed. Please check the logs for details."
fi

135
.gitea/workflows/docker.yml Normal file
View File

@ -0,0 +1,135 @@
name: Docker Build and Deploy
on:
push:
tags: [ 'v*' ]
workflow_run:
#workflows: ["CI Pipeline"]
branches: [main]
types: [completed]
jobs:
docker:
name: Build and Push Docker Image
runs-on: ubuntu-latest
if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') || github.event.workflow_run.conclusion == 'success' }}
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Gitea Container Registry
uses: docker/login-action@v3
with:
registry: ${{ secrets.REGISTRY_URL }}
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ secrets.REGISTRY_URL }}/${{ gitea.repository }}
tags: |
type=semver,pattern={{version}}
type=ref,event=branch
type=sha,format=short
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=${{ secrets.REGISTRY_URL }}/${{ gitea.repository }}:buildcache
cache-to: type=registry,ref=${{ secrets.REGISTRY_URL }}/${{ gitea.repository }}:buildcache,mode=max
- name: Verify image
run: |
echo "Image successfully built and pushed with tags: ${{ steps.meta.outputs.tags }}"
echo "::notice::Docker image built and pushed successfully."
deploy:
name: Deploy to VPS
runs-on: ${{ secrets.DEPLOY_RUNNER || 'ubuntu-latest' }}
needs: docker
if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
steps:
- name: Set image name
run: echo "IMAGE_NAME=${{ secrets.REGISTRY_URL }}/${{ secrets.REPOSITORY_PATH }}:${{ gitea.ref_name || 'latest' }}" >> $GITEA_ENV
- name: Install Docker CLI
run: |
apt-get update
apt-get install -y docker.io curl
docker version
- name: Login to Gitea Container Registry
uses: docker/login-action@v3
with:
registry: ${{ secrets.REGISTRY_URL }}
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Deploy to VPS
run: |
echo "Deploying image: ${{ env.IMAGE_NAME }}"
# Pull latest image
docker pull ${{ env.IMAGE_NAME }}
# Stop and remove existing container
docker stop ulflow-api-container || true
docker rm ulflow-api-container || true
# Run new container
docker run -d \
--name ${{ secrets.CONTAINER_NAME || 'ulflow-api-container' }} \
--network ${{ secrets.DOCKER_NETWORK || 'ulflow-network' }} \
--restart always \
-p ${{ secrets.APP_PORT || '8080' }}:8080 \
-e APP_ENV=${{ secrets.APP_ENV || 'production' }} \
-e DB_HOST=${{ secrets.DB_HOST }} \
-e DB_USER=${{ secrets.DB_USER }} \
-e DB_PASSWORD=${{ secrets.DB_PASSWORD }} \
-e DB_NAME=${{ secrets.DB_NAME }} \
-e JWT_SECRET_KEY=${{ secrets.JWT_SECRET_KEY }} \
-e REFRESH_TOKEN_SECRET=${{ secrets.REFRESH_TOKEN_SECRET }} \
-e API_KEY=${{ secrets.API_KEY }} \
-e ENCRYPTION_KEY=${{ secrets.ENCRYPTION_KEY }} \
--health-cmd "${{ secrets.HEALTH_CMD || 'curl -f http://localhost:8080/health || exit 1' }}" \
--health-interval ${{ secrets.HEALTH_INTERVAL || '30s' }} \
--memory ${{ secrets.CONTAINER_MEMORY || '1g' }} \
--cpus ${{ secrets.CONTAINER_CPU || '1' }} \
${{ env.IMAGE_NAME }}
- name: Verify deployment
run: |
# Wait for container to start
sleep 10
# Check container is running
if docker ps | grep ${{ secrets.CONTAINER_NAME || 'ulflow-api-container' }}; then
echo "Container is running"
else
echo "::error::Container failed to start"
docker logs ${{ secrets.CONTAINER_NAME || 'ulflow-api-container' }}
exit 1
fi
# Check health endpoint
curl -f http://localhost:${{ secrets.APP_PORT || '8080' }}/health || (echo "::error::Health check failed" && exit 1)
echo "::notice::Deployment successful!"
- name: Send notification
if: always()
run: |
if [[ "${{ job.status }}" == "success" ]]; then
echo "::notice::🚀 Deployment to VPS successful! Version: ${{ gitea.ref_name }}"
else
echo "::error::❌ Deployment to VPS failed! Check logs for details."
fi

50
.gitignore vendored Normal file
View File

@ -0,0 +1,50 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
bin/
tmp/
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
vendor/
# Go workspace file
go.work
# IDE specific files
.idea/
.vscode/
*.swp
*.swo
# Environment variables
.env
# Log files
*.log
logs/
# Temporary files
tmp/
temp/
# Air Tomb temporary files
.air.toml.tmp
# Docker volumes
docker/data/
# Build artifacts
dist/
# OS specific files
.DS_Store
Thumbs.db

58
Dockerfile Normal file
View File

@ -0,0 +1,58 @@
# Dockerfile
# Production-ready multi-stage build for server deployment
# Build stage
FROM golang:1.23-alpine AS builder
# Install necessary build tools
RUN apk add --no-cache git make gcc libc-dev
# Set working directory
WORKDIR /build
# Copy go.mod and go.sum files
COPY go.mod go.sum* ./
# Download dependencies
RUN go mod download
# Copy the entire project
COPY . .
# Build the application with optimizations
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /build/bin/api ./cmd/api
# Final stage
FROM alpine:3.19
# Add necessary runtime packages
RUN apk add --no-cache ca-certificates tzdata
# Set timezone
ENV TZ=Asia/Ho_Chi_Minh
# Create non-root user
RUN adduser -D -g '' appuser
# Create app directories
RUN mkdir -p /app/config /app/logs /app/storage
WORKDIR /app
# Copy binary from builder stage
COPY --from=builder /build/bin/api /app/
COPY --from=builder /build/config /app/config
# Set ownership
RUN chown -R appuser:appuser /app
# Switch to non-root user
USER appuser
# Expose port
EXPOSE 8080
# Set environment variable for production
ENV APP_ENV=production
# Command to run the application
ENTRYPOINT ["/app/api"]

32
Dockerfile.local Normal file
View File

@ -0,0 +1,32 @@
# Dockerfile.local
# Optimized for local development with hot reload
# Build stage
FROM golang:1.23-alpine AS builder
# Install necessary tools for development
RUN apk add --no-cache git make gcc libc-dev
# Install Air for hot reload
RUN go install github.com/cosmtrek/air@latest
# Set working directory
WORKDIR /app
# Copy go.mod and go.sum files
COPY go.mod go.sum* ./
# Download dependencies
RUN go mod download
# Copy the entire project
COPY . .
# Expose port
EXPOSE 8080
# Set environment variable for development
ENV APP_ENV=development
# Command to run the application with hot reload
CMD ["air", "-c", ".air.toml"]

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 ULFlow Team
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

171
Makefile Normal file
View File

@ -0,0 +1,171 @@
# ULFlow Golang Starter Kit Makefile
# Provides common commands for development, testing, and deployment
.PHONY: help init dev test lint build clean docker-build docker-run docker-clean docker-prune docker-compose-up docker-compose-down docker-compose-prod-up docker-compose-prod-down ci setup-git all
# Default target executed when no arguments are given to make.
default: help
# Show help message for all Makefile commands
help:
@echo "ULFlow Golang Starter Kit"
@echo ""
@echo "Usage:"
@echo " make <target>"
@echo ""
@echo "Targets:"
@echo " init - Initialize project dependencies and tools"
@echo " dev - Start development server with hot reload (Air Tomb)"
@echo " test - Run all tests"
@echo " lint - Run linters and code quality tools"
@echo " build - Build the application binary"
@echo " clean - Clean temporary files, build artifacts, and cache"
@echo " docker-build - Build Docker image"
@echo " docker-run - Run application in Docker container"
@echo " docker-clean - Remove project Docker containers"
@echo " docker-prune - Remove all unused Docker resources"
@echo " docker-compose-up - Start all services with Docker Compose for local development"
@echo " docker-compose-down - Stop all services started with Docker Compose"
@echo " docker-compose-prod-up - Start all services with Docker Compose for production"
@echo " docker-compose-prod-down - Stop all production services"
@echo " ci - Run CI workflow locally"
@echo " setup-git - Configure Git with commit message template and hooks"
@echo " all - Run lint, test, and build"
# Initialize project dependencies and tools
init:
@echo "Installing project dependencies and tools..."
go mod tidy
go install github.com/cosmtrek/air@latest
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
@echo "Creating necessary directories..."
mkdir -p tmp
mkdir -p logs
@echo "Initialization complete!"
# Start development server with hot reload
dev:
@echo "Starting development server with Air Tomb..."
air
# Run all tests
test:
@echo "Running tests..."
go test -v -cover ./...
# Run linters and code quality tools
lint:
@echo "Running linters..."
golangci-lint run ./...
# Build the application binary
build:
@echo "Building application..."
go build -o bin/api cmd/api/main.go
# Clean temporary files, build artifacts, and cache
clean:
@echo "Cleaning project..."
rm -rf tmp/
rm -rf bin/
rm -rf logs/*.log
go clean -cache -testcache -modcache
@echo "Project cleaned!"
# Build Docker image
docker-build:
@echo "Building Docker image..."
docker build -t ulflow-starter-kit:latest .
# Run application in Docker container
docker-run:
@echo "Running application in Docker container..."
docker run -p 8080:8080 --env-file .env ulflow-starter-kit:latest
# Run Docker Compose for local development
docker-compose-up:
@echo "Starting all services with Docker Compose for local development..."
docker-compose up -d
@echo "Services started! API is available at http://localhost:8080"
# Stop Docker Compose services for local development
docker-compose-down:
@echo "Stopping all development services..."
docker-compose down
# Run Docker Compose for production
docker-compose-prod-up:
@echo "Starting all services with Docker Compose for production..."
docker-compose -f docker-compose.prod.yml up -d
@echo "Production services started! API is available at http://localhost:8080"
# Stop Docker Compose services for production
docker-compose-prod-down:
@echo "Stopping all production services..."
docker-compose -f docker-compose.prod.yml down
# Setup Git configuration
setup-git:
@echo "Setting up Git configuration..."
git config --local commit.template .gitea/commit-template.txt
cp .gitea/hooks/pre-commit .git/hooks/
cp .gitea/hooks/prepare-commit-msg .git/hooks/
chmod +x .git/hooks/pre-commit
chmod +x .git/hooks/prepare-commit-msg
git config --local core.hooksPath .git/hooks
@echo "Git setup complete!"
# Create git message template
setup-git-message:
@echo "Creating Git commit message template..."
mkdir -p .gitea
echo "# <type>: <subject>\n\n# <body>\n\n# <footer>\n\n# Types:\n# feat (new feature)\n# fix (bug fix)\n# docs (documentation changes)\n# style (formatting, no code change)\n# refactor (refactoring code)\n# test (adding tests, refactoring tests)\n# chore (updating tasks etc; no production code change)" > .gitea/commit-template.txt
git config --local commit.template .gitea/commit-template.txt
@echo "Git commit message template created!"
# Clean Docker containers related to this project
docker-clean:
@echo "Cleaning Docker containers..."
docker ps -a | grep ulflow-starter-kit | awk '{print $$1}' | xargs -r docker rm -f
@echo "Docker containers cleaned!"
# Prune all unused Docker resources
docker-prune:
@echo "Pruning unused Docker resources..."
docker system prune -af --volumes
@echo "Docker resources pruned!"
# Run local CI simulation
ci:
@echo "Running CI workflow locally..."
make lint
make test
make build
make docker-build
@echo "CI simulation completed!"
@echo "Cleaning up after CI..."
make docker-clean
# Run everything (lint, test, build)
all: lint test build
# Create a new migration
migrate-create:
@read -p "Enter migration name: " name; \
migrate create -ext sql -dir migrations -seq $$name
# Run migrations up
migrate-up:
migrate -path migrations -database "$(DATABASE_URL)" up
# Run migrations down
migrate-down:
migrate -path migrations -database "$(DATABASE_URL)" down
# Run application (default: without hot reload)
run:
go run ./cmd/app/main.go
# Run application with hot reload
dev:
air -c .air.toml

192
Readme.md Normal file
View File

@ -0,0 +1,192 @@
# ULFlow Golang Starter Kit
<div align="center">
![ULFlow Logo](https://via.placeholder.com/200x80?text=ULFlow)
[![Go Version](https://img.shields.io/badge/Go-1.23-00ADD8.svg)](https://go.dev/)
[![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
</div>
## Tổng quan
ULFlow Golang Starter Kit là nền tảng khởi tạo dự án backend cho Team ULFlow, được thiết kế để giúp khởi tạo các dự án trong thời gian ngắn, nhanh chóng mà vẫn đảm bảo các yếu tố cơ bản về kiến trúc, bảo mật và khả năng mở rộng.
### Đặc điểm chính
- **Kiến trúc DDD (Domain Driven Development)** - Customize cho phù hợp với nhu cầu dự án
- **U-Hierarchy** - Mô hình tổ chức mã nguồn theo các cấp độ logic
- **Hot Reload** - Phát triển nhanh với Air Tomb
- **Docker & Docker Compose** - Containerization và orchestration
- **Gitea Full** - CI/CD, Secret Manager, Registry Image
- **Postgres** - Cơ sở dữ liệu quan hệ
## Cài đặt
### Yêu cầu hệ thống
- Go 1.23
- Docker và Docker Compose
- Git
### Khởi tạo dự án
```bash
# Clone repository
git clone [repo-url] my-project
cd my-project
# Cài đặt dependencies và công cụ
make init
# Cấu hình môi trường
cp .env.example .env
# Chỉnh sửa file .env theo nhu cầu
# Khởi động môi trường phát triển
make dev
```
### Sử dụng Docker
Dự án cung cấp hai cấu hình Docker:
#### Môi trường phát triển (Local)
```bash
# Khởi động tất cả các dịch vụ với Docker Compose
make docker-compose-up
# Dừng tất cả các dịch vụ
make docker-compose-down
# Dọn dẹp container sau khi sử dụng
make docker-clean
```
Môi trường phát triển bao gồm:
- API service với hot reload (Air Tomb)
- PostgreSQL database
- Gitea (Git Server, CI/CD, Registry)
- Gitea Runner
#### Môi trường production
```bash
# Khởi động các dịch vụ cho production
make docker-compose-prod-up
# Dừng các dịch vụ production
make docker-compose-prod-down
```
Môi trường production bao gồm:
- API service (optimized build)
- PostgreSQL database
- Nginx reverse proxy
## Kiến trúc
Dự án sử dụng mô hình DDD (Domain Driven Development) với các thành phần chính:
- **Resource**: Các Aggregate DDD
- **Transaction**: Các Saga điều phối luồng nghiệp vụ phức tạp
- **Adapter**: Xử lý giao tiếp với các hệ thống bên ngoài
- **Helper**: Các thư viện, tiện ích dùng chung
- **UIUX**: Lớp giao diện người dùng
Thành phần kiến trúc chi tiết (U-Hierarchy):
- **ubit**: Đơn vị logic nhỏ nhất (hàm, type, hằng số)
- **ubrick**: Tập hợp các `ubit` liên quan
- **ublock**: Thành phần hoạt động độc lập tương đối
- **ubundle**: Tính năng hoàn chỉnh cho người dùng
## Cấu trúc thư mục
```
├── cmd/ # Entry points
├── pkg/ # Packages
│ ├── resource/ # Domain resources (DDD Aggregates)
│ ├── transaction/ # Business workflows
│ ├── adapter/ # External system interfaces
│ ├── helper/ # Utilities
│ └── uiux/ # User interfaces
├── config/ # Configuration
├── docs/ # Documentation
├── scripts/ # Helper scripts
└── test/ # Tests
```
## Các lệnh Makefile
```bash
# Xem danh sách lệnh có sẵn
make help
# Khởi tạo dự án
make init
# Chạy môi trường phát triển với Air Tomb
make dev
# Chạy tests
make test
# Kiểm tra linting
make lint
# Build ứng dụng
make build
# Dọn dẹp tệp tạm
make clean
# Docker commands
make docker-build # Build Docker image
make docker-run # Run container từ image
make docker-clean # Dọn dẹp container
make docker-prune # Dọn dẹp tất cả tài nguyên Docker không sử dụng
make docker-compose-up # Khởi động môi trường phát triển
make docker-compose-down # Dừng môi trường phát triển
make docker-compose-prod-up # Khởi động môi trường production
make docker-compose-prod-down # Dừng môi trường production
# Cấu hình Git
make setup-git
```
## Quy trình phát triển
Dự án sử dụng Trunk-Based Development với các quy ước đặt tên nhánh:
- `feat`: Tạo tính năng mới
- `fix`: Sửa lỗi
- `docs`: Thay đổi tài liệu
- `style`: Thay đổi không ảnh hưởng đến logic
- `refactor`: Refactor code
- `test`: Thêm hoặc sửa test
- `chore`: Thay đổi cấu hình hoặc các task không liên quan đến code
Xem thêm chi tiết trong [docs/workflow.md](docs/workflow.md).
## Tài liệu
- [Kiến trúc](docs/architecture.md)
- [Thông số kỹ thuật](docs/spec.md)
- [Roadmap](docs/roadmap.md)
- [Quy trình làm việc](docs/workflow.md)
- [CI/CD Workflows](docs/workflows.md)
- [Testing](docs/testing.md)
- [Adapter](docs/adapter.md)
- [UX](docs/ux.md)
- [Changelog](docs/changelog.md)
## Đóng góp
Xem hướng dẫn đóng góp trong [docs/workflow.md](docs/workflow.md).
## Giấy phép
Dự án này được phân phối dưới Giấy phép MIT. Xem file `LICENSE` để biết thêm thông tin.

149
cmd/app/main.go Normal file
View File

@ -0,0 +1,149 @@
package main
import (
"context"
"fmt"
"os"
"time"
"starter-kit/internal/helper/config"
"starter-kit/internal/helper/database"
"starter-kit/internal/helper/feature"
"starter-kit/internal/helper/logger"
"starter-kit/internal/pkg/lifecycle"
"starter-kit/internal/transport/http"
)
// HTTPService implements the lifecycle.Service interface for the HTTP server
type HTTPService struct {
server *http.Server
cfg *config.Config
}
func NewHTTPService(cfg *config.Config) *HTTPService {
return &HTTPService{
server: http.NewServer(cfg),
cfg: cfg,
}
}
func (s *HTTPService) Name() string {
return "HTTP Server"
}
func (s *HTTPService) Start() error {
// Start the server in a goroutine
go func() {
if err := s.server.Start(); err != nil {
logger.WithError(err).Error("HTTP server error")
}
}()
return nil
}
func (s *HTTPService) Shutdown(ctx context.Context) error {
return s.server.Shutdown(ctx)
}
func main() {
// Initialize config loader
configLoader := config.NewConfigLoader()
// Load configuration
cfg, err := configLoader.Load()
if err != nil {
fmt.Printf("Failed to load configuration: %v\n", err)
os.Exit(1)
}
// Initialize logger
logger.Init(&logger.LogConfig{
Level: cfg.Logger.Level,
Format: "json",
EnableCaller: true,
ReportCaller: true,
})
// Initialize feature flags
if err := feature.Init(); err != nil {
logger.WithError(err).Fatal("Failed to initialize feature flags")
}
// Print application info
logger.Infof("Starting %s v%s", cfg.App.Name, cfg.App.Version)
logger.Infof("Environment: %s", cfg.App.Environment)
logger.Infof("Log Level: %s", cfg.Logger.Level)
logger.Infof("Timezone: %s", cfg.App.Timezone)
// Print server config
logger.Infof("Server config: %s:%d", cfg.Server.Host, cfg.Server.Port)
logger.Infof("Read Timeout: %d seconds", cfg.Server.ReadTimeout)
logger.Infof("Write Timeout: %d seconds", cfg.Server.WriteTimeout)
logger.Infof("Shutdown Timeout: %d seconds", cfg.Server.ShutdownTimeout)
// Create a new lifecycle manager
shutdownTimeout := 30 * time.Second // Default shutdown timeout
if cfg.Server.ShutdownTimeout > 0 {
shutdownTimeout = time.Duration(cfg.Server.ShutdownTimeout) * time.Second
}
lifecycleMgr := lifecycle.New(shutdownTimeout)
// Initialize database connection
if feature.IsEnabled(feature.EnableDatabase) {
logger.Info("Database feature is enabled, connecting...")
_, err = database.NewConnection(&cfg.Database)
if err != nil {
logger.WithError(err).Fatal("Failed to connect to database")
}
// Run database migrations
if err := database.Migrate(cfg.Database); err != nil {
logger.WithError(err).Fatal("Failed to migrate database")
}
// Register database cleanup on shutdown
lifecycleMgr.Register(&databaseService{})
} else {
logger.Info("Database feature is disabled")
}
// Register HTTP service with the lifecycle manager
httpService := NewHTTPService(cfg)
lifecycleMgr.Register(httpService)
// Start all services
if err := lifecycleMgr.Start(); err != nil {
logger.WithError(err).Fatal("Failed to start services")
}
// Handle OS signals for graceful shutdown
lifecycleMgr.ShutdownOnSignal()
// Wait for all services to complete
if err := lifecycleMgr.Wait(); err != nil {
logger.WithError(err).Error("Service error")
}
// Close feature flags
if err := feature.Close(); err != nil {
logger.WithError(err).Error("Failed to close feature flags")
}
logger.Info("Application stopped")
}
// databaseService implements the lifecycle.Service interface for database operations
type databaseService struct{}
func (s *databaseService) Name() string {
return "Database Service"
}
func (s *databaseService) Start() error {
// Database connection is initialized in main
return nil
}
func (s *databaseService) Shutdown(ctx context.Context) error {
return database.Close()
}

31
configs/config.yaml Normal file
View File

@ -0,0 +1,31 @@
app:
name: "ULFlow Starter Kit"
version: "0.1.0"
environment: "development"
timezone: "Asia/Ho_Chi_Minh"
logger:
level: "info" # debug, info, warn, error
server:
host: "localhost"
port: 8080
read_timeout: 15
write_timeout: 15
shutdown_timeout: 30
trusted_proxies: []
allow_origins:
- "*"
database:
driver: "postgres"
host: "localhost"
port: 5432
username: "postgres"
password: "postgres"
database: "ulflow"
ssl_mode: "disable"
max_open_conns: 25
max_idle_conns: 5
conn_max_lifetime: 300
migration_path: "migrations"

83
docker-compose.prod.yml Normal file
View File

@ -0,0 +1,83 @@
version: '3.8'
services:
# API Service - Production Configuration
api:
build:
context: .
dockerfile: Dockerfile
container_name: ulflow-api
restart: always
ports:
- "8080:8080"
env_file:
- .env
environment:
- APP_ENV=production
depends_on:
- postgres
networks:
- ulflow-network
deploy:
resources:
limits:
cpus: '1'
memory: 1G
restart_policy:
condition: on-failure
max_attempts: 3
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
# PostgreSQL Database - Production Configuration
postgres:
image: postgres:15-alpine
container_name: ulflow-postgres
restart: always
environment:
POSTGRES_USER: ${DB_USER:-user}
POSTGRES_PASSWORD: ${DB_PASSWORD:-password}
POSTGRES_DB: ${DB_NAME:-ulflow_db}
volumes:
- postgres-data:/var/lib/postgresql/data
networks:
- ulflow-network
deploy:
resources:
limits:
cpus: '1'
memory: 1G
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-user} -d ${DB_NAME:-ulflow_db}"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
# Nginx Reverse Proxy
nginx:
image: nginx:alpine
container_name: ulflow-nginx
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d
- ./nginx/ssl:/etc/nginx/ssl
- ./nginx/logs:/var/log/nginx
depends_on:
- api
networks:
- ulflow-network
volumes:
postgres-data:
networks:
ulflow-network:
driver: bridge

85
docker-compose.yml Normal file
View File

@ -0,0 +1,85 @@
version: '3.8'
services:
# API Service
api:
build:
context: .
dockerfile: Dockerfile.local
container_name: ulflow-api
restart: unless-stopped
ports:
- "8080:8080"
volumes:
- .:/app
- go-modules:/go/pkg/mod
env_file:
- .env
depends_on:
- postgres
networks:
- ulflow-network
# PostgreSQL Database
postgres:
image: postgres:15-alpine
container_name: ulflow-postgres
restart: unless-stopped
environment:
POSTGRES_USER: ${DB_USER:-user}
POSTGRES_PASSWORD: ${DB_PASSWORD:-password}
POSTGRES_DB: ${DB_NAME:-ulflow_db}
ports:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
networks:
- ulflow-network
# Gitea (Git Server, CI/CD, Registry)
gitea:
image: gitea/gitea:1.21
container_name: ulflow-gitea
restart: unless-stopped
environment:
- USER_UID=1000
- USER_GID=1000
- GITEA__database__DB_TYPE=postgres
- GITEA__database__HOST=postgres:5432
- GITEA__database__NAME=gitea
- GITEA__database__USER=${DB_USER:-user}
- GITEA__database__PASSWD=${DB_PASSWORD:-password}
ports:
- "3000:3000"
- "2222:22"
volumes:
- gitea-data:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
depends_on:
- postgres
networks:
- ulflow-network
# Gitea Runner for CI/CD
gitea-runner:
image: gitea/act_runner:latest
container_name: ulflow-gitea-runner
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- gitea-runner-data:/data
depends_on:
- gitea
networks:
- ulflow-network
volumes:
postgres-data:
gitea-data:
gitea-runner-data:
go-modules:
networks:
ulflow-network:
driver: bridge

1
docs/.obsidian/app.json vendored Normal file
View File

@ -0,0 +1 @@
{}

1
docs/.obsidian/appearance.json vendored Normal file
View File

@ -0,0 +1 @@
{}

31
docs/.obsidian/core-plugins.json vendored Normal file
View File

@ -0,0 +1,31 @@
{
"file-explorer": true,
"global-search": true,
"switcher": true,
"graph": true,
"backlink": true,
"canvas": true,
"outgoing-link": true,
"tag-pane": true,
"properties": false,
"page-preview": true,
"daily-notes": true,
"templates": true,
"note-composer": true,
"command-palette": true,
"slash-command": false,
"editor-status": true,
"bookmarks": true,
"markdown-importer": false,
"zk-prefixer": false,
"random-note": false,
"outline": true,
"word-count": true,
"slides": false,
"audio-recorder": false,
"workspaces": false,
"file-recovery": true,
"publish": false,
"sync": true,
"webviewer": false
}

184
docs/.obsidian/workspace.json vendored Normal file
View File

@ -0,0 +1,184 @@
{
"main": {
"id": "261ce7313e6dd2d1",
"type": "split",
"children": [
{
"id": "8cfa998b40f0304e",
"type": "tabs",
"children": [
{
"id": "6d1019fb7e553425",
"type": "leaf",
"state": {
"type": "markdown",
"state": {
"file": "adapter.md",
"mode": "source",
"source": false
},
"icon": "lucide-file",
"title": "adapter"
}
}
]
}
],
"direction": "vertical"
},
"left": {
"id": "f87c78d3a5a321a8",
"type": "split",
"children": [
{
"id": "015ce6e570ac0c70",
"type": "tabs",
"children": [
{
"id": "6172be51a41d7dc9",
"type": "leaf",
"state": {
"type": "file-explorer",
"state": {
"sortOrder": "alphabetical",
"autoReveal": false
},
"icon": "lucide-folder-closed",
"title": "Trình duyệt tệp"
}
},
{
"id": "0564e6c5e875c5e5",
"type": "leaf",
"state": {
"type": "search",
"state": {
"query": "",
"matchingCase": false,
"explainSearch": false,
"collapseAll": false,
"extraContext": false,
"sortOrder": "alphabetical"
},
"icon": "lucide-search",
"title": "Tìm kiếm"
}
},
{
"id": "d2af93c860305d9b",
"type": "leaf",
"state": {
"type": "bookmarks",
"state": {},
"icon": "lucide-bookmark",
"title": "Đánh dấu"
}
}
]
}
],
"direction": "horizontal",
"width": 300
},
"right": {
"id": "f08c8c30bdb753d9",
"type": "split",
"children": [
{
"id": "a243e45a9db5439d",
"type": "tabs",
"children": [
{
"id": "3edc5198b4139609",
"type": "leaf",
"state": {
"type": "backlink",
"state": {
"file": "general.md",
"collapseAll": false,
"extraContext": false,
"sortOrder": "alphabetical",
"showSearch": false,
"searchQuery": "",
"backlinkCollapsed": false,
"unlinkedCollapsed": true
},
"icon": "links-coming-in",
"title": "Liên kết đến của general"
}
},
{
"id": "bd56178c8119e62e",
"type": "leaf",
"state": {
"type": "outgoing-link",
"state": {
"file": "general.md",
"linksCollapsed": false,
"unlinkedCollapsed": true
},
"icon": "links-going-out",
"title": "Liên kết đi ra từ general"
}
},
{
"id": "bc29a86f17a39689",
"type": "leaf",
"state": {
"type": "tag",
"state": {
"sortOrder": "frequency",
"useHierarchy": true,
"showSearch": false,
"searchQuery": ""
},
"icon": "lucide-tags",
"title": "Tags"
}
},
{
"id": "8b998447c8dac66b",
"type": "leaf",
"state": {
"type": "outline",
"state": {
"file": "general.md",
"followCursor": false,
"showSearch": false,
"searchQuery": ""
},
"icon": "lucide-list",
"title": "Đề cương của general"
}
}
]
}
],
"direction": "horizontal",
"width": 300,
"collapsed": true
},
"left-ribbon": {
"hiddenItems": {
"switcher:Mở bộ chuyển đổi nhanh": false,
"graph:Mở xem biểu đồ": false,
"canvas:Tạo bảng mới": false,
"daily-notes:Mở ghi chú hôm nay": false,
"templates:Chèn mẫu": false,
"command-palette:Mở khay lệnh": false
}
},
"active": "6d1019fb7e553425",
"lastOpenFiles": [
"workflow.md",
"ux.md",
"testing.md",
"spec.md",
"secrets.md",
"roadmap.md",
"general.md",
"adapter.md",
"architecture.md",
"changelog.md"
]
}

149
docs/adapter.md Normal file
View File

@ -0,0 +1,149 @@
# Adapter Layer
## Tổng quan
Adapter là lớp giao tiếp giữa ứng dụng và các hệ thống bên ngoài. Layer này cung cấp khả năng tích hợp với bên thứ ba và sự độc lập của core business logic.
## Nguyên tắc thiết kế
- Tách biệt domain logic với chi tiết triển khai bên ngoài
- Dễ dàng thay thế các implementation mà không ảnh hưởng tới business logic
- Dependency inversion - core logic không phụ thuộc vào chi tiết triển khai
- Interface-driven design cho các external services
## Cấu trúc thư mục
```
internal/adapter/
│── persistence/ # Database adapters
│ │── postgres/ # PostgreSQL implementation
│ │── mysql/ # MySQL implementation (optional)
│ └── sqlite/ # SQLite implementation (optional)
│── messaging/ # Messaging system adapters
│── storage/ # File storage adapters
│── externalapi/ # Third-party service adapters
│── notification/ # Notification service adapters
└── cache/ # Cache adapters
```
## Database Adapter
Adapter database được triển khai với tính năng:
- Kết nối tự động với PostgreSQL
- Cấu hình connection pool
- Xử lý migration tự động
- Triển khai repository pattern
### Cấu hình
Cấu hình database được định nghĩa trong file `configs/config.yaml`:
```yaml
database:
driver: "postgres"
host: "localhost"
port: 5432
username: "postgres"
password: "postgres"
database: "ulflow"
ssl_mode: "disable"
max_open_conns: 25
max_idle_conns: 5
conn_max_lifetime: 300
migration_path: "migrations"
```
## Triển khai adapter
### 1. Định nghĩa interface
```go
// pkg/adapter/payment/interface.go
package payment
type PaymentProvider interface {
ProcessPayment(amount float64, currency string, metadata map[string]string) (string, error)
RefundPayment(paymentId string, amount float64) error
GetPaymentStatus(paymentId string) (string, error)
}
```
### 2. Triển khai cụ thể
```go
// pkg/adapter/payment/stripe/stripe.go
package stripe
import "github.com/your-project/pkg/adapter/payment"
type StripeAdapter struct {
apiKey string
client *stripe.Client
}
func NewStripeAdapter(apiKey string) *StripeAdapter {
return &StripeAdapter{
apiKey: apiKey,
client: stripe.NewClient(apiKey),
}
}
func (s *StripeAdapter) ProcessPayment(amount float64, currency string, metadata map[string]string) (string, error) {
// Triển khai với Stripe API
}
// Triển khai các phương thức khác...
```
### 3. Sử dụng mock để test
```go
// pkg/adapter/payment/mock/mock.go
package mock
import "github.com/your-project/pkg/adapter/payment"
type MockPaymentAdapter struct {
// Fields to store test data
}
func (m *MockPaymentAdapter) ProcessPayment(amount float64, currency string, metadata map[string]string) (string, error) {
// Mock implementation for testing
}
// Triển khai các phương thức khác...
```
## Dependency Injection
Sử dụng dependency injection để đưa adapter vào business logic:
```go
// pkg/transaction/order/service.go
package order
type OrderService struct {
paymentProvider payment.PaymentProvider
// Other dependencies...
}
func NewOrderService(paymentProvider payment.PaymentProvider) *OrderService {
return &OrderService{
paymentProvider: paymentProvider,
}
}
func (s *OrderService) PlaceOrder(order *resource.Order) error {
// Sử dụng payment provider thông qua interface
paymentId, err := s.paymentProvider.ProcessPayment(
order.TotalAmount,
order.Currency,
map[string]string{"orderId": order.ID},
)
// Xử lý kết quả...
}
```
## Best Practices
1. **Mỗi adapter chỉ giao tiếp với một dịch vụ bên ngoài**
2. **Xử lý lỗi phù hợp và mapping sang domain errors**
3. **Cung cấp logging và metrics cho mỗi adapter**
4. **Retry mechanisms cho các dịch vụ không ổn định**
5. **Circuit breaking để tránh cascading failures**
6. **Feature toggles để dễ dàng chuyển đổi giữa các provider**

234
docs/architecture.md Normal file
View File

@ -0,0 +1,234 @@
# Kiến trúc hệ thống
## Project Overview
- Đây là dự án tạo ra Starter Kit Golang Backend cho Team ULFlow để có thể khởi tạo các dự án trong thời gian ngắn, nhanh chóng mà vẫn đảm bảo các yếu tố cơ bản
- Sử dụng Mô hình DDD (Domain Driven Development) - Customize
- `Resource`: Các Aggregate DDD
- `Transaction`: Các Saga điều phối luồng nghiệp vụ phức tạp
- `Adapter`: Xử lý giao tiếp với các hệ thống bên ngoài
- `Helper`: Các thư viện, tiện ích dùng chung
- `Transport`: Lớp giao diện người dùng
- Thành phần kiến trúc chi tiết (U-Hierarchy)
- `ubit`: Đơn vị logic nhỏ nhất (hàm, type, hằng số)
- `ubrick`: Tập hợp các `ubit` liên quan
- `ublock`: Thành phần hoạt động độc lập tương đối
- `ubundle`: Tính năng hoàn chỉnh cho người dùng
## Nguyên tắc thiết kế
- **User-Centric**: Ưu tiên trải nghiệm người dùng, giảm thiểu sự phức tạp
- **Data-Oriented Programming (DOP)**: Thiết kế xoay quanh luồng dữ liệu và các biến đổi dữ liệu
- **Domain-Driven Design (DDD)**: Áp dụng các khái niệm cốt lõi (Bounded Context, Aggregates, Domain Events)
## Áp dụng DDD trong Dự án
### Resource
- Tuân theo nguyên tắc Aggregate trong DDD
- Mỗi Resource đại diện cho một thực thể nghiệp vụ
- Bao gồm các thuộc tính và hành vi liên quan
- Đảm bảo tính nhất quán và logic nghiệp vụ
### Transaction
- Triển khai mẫu Saga để điều phối các hoạt động phức tạp
- Đảm bảo tính toàn vẹn dữ liệu xuyên suốt các Resource
- Xử lý các trường hợp lỗi và rollback khi cần thiết
### Adapter
- Cung cấp giao diện giao tiếp với hệ thống bên ngoài
- Triển khai mẫu Adapter để đảm bảo tính linh hoạt
- Dễ dàng thay thế các thành phần khi cần thiết
### Helper
- Cung cấp các tiện ích dùng chung
- Triển khai các công cụ hỗ trợ phát triển
- Tối ưu hóa mã lệnh và tăng khả năng tái sử dụng
### UIUX
- Lớp giao diện người dùng (API, Web Interface)
- Triển khai theo nguyên tắc thiết kế hướng người dùng
- Tách biệt với logic nghiệp vụ
## U-Hierarchy
Hệ thống phân cấp U là một cách tiếp cận duy nhất cho tổ chức mã:
### ubit
- Đơn vị nhỏ nhất của mã
- Ví dụ: một hàm, một constant, một type
- Mục đích đơn lẻ, dễ test
### ubrick
- Tập hợp các ubit liên quan đến nhau
- Cung cấp một chức năng cụ thể
- Ví dụ: một package nhỏ, một nhóm hàm liên quan
### ublock
- Thành phần có thể hoạt động độc lập
- Bao gồm nhiều ubrick làm việc cùng nhau
- Ví dụ: một module hoặc service
### ubundle
- Một tính năng hoàn chỉnh cho người dùng
- Kết hợp nhiều ublock để tạo ra trải nghiệm người dùng
- Ví dụ: một tính năng end-to-end
### Directory Structure
starter-kit/
├── .gitea/
│ ├── workflows/
│ │ ├── ci.yml
│ │ └── docker.yml
│ ├── .hooks/
│ │ ├──pre-commit
│ │ └──prepare-commit-msg
│ └── commit-template.txt
├── cmd/
│ └── app/ # Hoặc 'server/', 'api/' - Entrypoint chính của ứng dụng
│ └── main.go
├── internal/
│ ├── resource/ # Lớp chứa các Aggregates/Entities (DDD)
│ │ └── example/ # Một module ví dụ rất cơ bản
│ │ ├── example_aggregate.go
│ │ ├── example_types.go
│ │ └── example_repository_interface.go # Interface cho repository
│ └── transaction/ # Lớp chứa các Use Cases/Application Services/Sagas (DDD)
│ │ └── example/ # Một module ví dụ rất cơ bản
│ │ └── example_transaction.go
│ ├── adapter/ # Lớp giao tiếp với các hệ thống bên ngoài hoặc hạ tầng
│ │ ├── persistence/ # Hoặc 'repository/', 'storage/' - Triển khai repositories
│ │ │ └── postgres/ # Ví dụ với PostgreSQL
│ │ │ ├── connection.go # Thiết lập kết nối DB
│ │ │ ├── example_postgres_repository.go # Triển khai repository cho example_aggregate
│ │ │ └── models.go # (Tùy chọn) GORM models hoặc struct cho DB mapping
│ │ |
│ │ └── externalapi/ # Giao tiếp với các dịch vụ bên ngoài
│ │ └── # example_service_client.go
│ ├── helper/ # Các thư viện, tiện ích dùng chung, không có business logic
│ │ ├── config/ # Load cấu hình (ví dụ: Gin)
│ │ │ └── load.go
│ │ ├── logger/ # Thiết lập logger (ví dụ: Logrus)
│ │ │ └── log.go
│ │ ├── validation/ # Tiện ích validation chung
│ │ │ └── common.go
│ │ ├── security/ # Tiện ích bảo mật (JWT, hashing)
│ │ │ ├── jwt_helper.go
│ │ │ └── password_helper.go
│ │ └── # ... (ví dụ: datetime, string_utils)
│ └── transport/ # Lớp giao tiếp với thế giới bên ngoài (ví dụ: HTTP)
│ └── http/ # Cụ thể cho HTTP transport
│ ├── handler/ # HTTP request handlers
│ │ ├── example_handler.go
│ │ ├── health_handler.go # Endpoint kiểm tra sức khỏe ứng dụng
│ │ └── middleware/ # HTTP middlewares (auth, logging, cors, recovery)
│ │ ├── auth_middleware.go
│ │ ├── request_logger_middleware.go
│ │ └── error_handling_middleware.go
│ ├── router.go # Định nghĩa các routes (ví dụ: sử dụng Gin, Chi)
│ └── dto/ # Data Transfer Objects cho request/response
│ ├── example_dto.go
│ └── common_dto.go
├── docs/ # Tài liệu của starter-kit
├── templates/ # Chứa các file cấu hình mẫu
│ ├── config.example.yaml # Đổi tên để rõ là file mẫu
│ └── .env.example # Các biến môi trường mẫu
├── migrations/ # Chứa các file SQL migration
│ └── 000001_init_schema.example.sql # Migration mẫu
├── api/ # (Tùy chọn) Định nghĩa API (ví dụ: OpenAPI/Swagger specs)
│ └── openapi.yaml
├── scripts/ # Các shell script tiện ích (nếu Makefile không đủ)
│ ├── # setup_env.sh
│ └── # run_checks.sh
├── Makefile # Các lệnh tiện ích (build, test, lint, run, docker, ...)
├── go.mod # Tên module nên là tên của starter-kit,
├── .gitignore
└── README.md # Hướng dẫn nhanh và tổng quan về starter-kit
### Checklist cơ bản của vòng đời App
I. Thiết lập Cơ bản và Cấu trúc (Basic Setup & Structure)
[ ] Tách biệt Logic Khởi tạo:
Yêu cầu: main.go chỉ chứa hàm main() và các lệnh gọi ở mức cao nhất. Logic chi tiết cho việc khởi tạo từng thành phần (config, logger, server, DB) phải nằm trong các package/hàm riêng biệt (ví dụ: internal/common/config, internal/common/logger, internal/transport/http/server, internal/infrastructure/db).
Mục đích: Giữ main.go ngắn gọn, dễ đọc, dễ hiểu vai trò điều phối của nó.
[ ] Hàm main() rõ ràng:
Yêu cầu: Hàm main() nên thực hiện các bước khởi tạo một cách tuần tự, logic.
Mục đích: Dễ dàng theo dõi luồng khởi động của ứng dụng.
[ ] Xử lý lỗi khởi tạo nghiêm trọng:
Yêu cầu: Nếu một bước khởi tạo thiết yếu (ví dụ: load config, kết nối DB) thất bại, ứng dụng nên log lỗi rõ ràng và thoát (ví dụ: log.Fatalf hoặc logger.Fatal).
Mục đích: Tránh việc ứng dụng chạy trong trạng thái không ổn định hoặc không đầy đủ.
II. Quản lý Cấu hình (Configuration Management)
[ ] Load Cấu hình:
Yêu cầu: Gọi một hàm/package chuyên biệt để đọc cấu hình từ các nguồn (file YAML/JSON/.env, biến môi trường).
Ví dụ: cfg, err := config.Load("configs/", ".env.example")
Mục đích: Tập trung logic load config, dễ dàng thay đổi nguồn hoặc định dạng config.
[ ] Validate Cấu hình Cơ bản (Đề xuất):
Yêu cầu (Đề xuất): Sau khi load, có một bước kiểm tra sơ bộ các giá trị cấu hình thiết yếu (ví dụ: port server có hợp lệ, thông tin kết nối DB có đủ).
Mục đích: Phát hiện lỗi cấu hình sớm.
III. Logging
[ ] Structured Logging với Logrus:
- Sử dụng Logrus cho structured logging với JSON format
- Hỗ trợ các log level: debug, info, warn, error
- Tích hợp request ID tracking cho HTTP requests
- Cấu hình logging được quản lý tập trung trong section `logger`
[ ] Middleware Logging:
- Tự động log các HTTP request với thông tin chi tiết:
- Request ID
- Method
- Path
- Status code
- Latency
- Client IP
- User agent
[ ] Error Handling và Context:
- Tích hợp error context vào logs
- Structured fields cho phép phân tích và tìm kiếm hiệu quả
- Theo dõi chuỗi lỗi với error wrapping
IV. Khởi tạo Dependencies Cơ sở hạ tầng (Infrastructure Dependencies)
[ ] Khởi tạo Kết nối Database (nếu starter kit có sẵn):
Yêu cầu: Gọi một hàm/package để thiết lập kết nối tới DB (ví dụ: PostgreSQL) và quản lý connection pool.
Ví dụ: dbConn, err := database.New(cfg.DBConfig, appLogger)
Mục đích: Chuẩn bị sẵn sàng cho việc tương tác với DB.
[ ] Đóng Kết nối Database khi Shutdown:
Yêu cầu: Đảm bảo kết nối DB được đóng một cách an toàn khi ứng dụng tắt (sử dụng defer hoặc trong quá trình graceful shutdown).
Mục đích: Tránh rò rỉ tài nguyên.
V. Thiết lập Server (ví dụ: HTTP Server)
[ ] Khởi tạo Router (từ package riêng):
Yêu cầu: Logic định nghĩa routes và gắn handlers phải nằm trong một package riêng (ví dụ: internal/transport/http/router). main.go chỉ khởi tạo nó.
Ví dụ: httpRouter := router.New(appLogger /*, dbConn, otherServices */)
Mục đích: Tách biệt rõ ràng logic routing và business.
[ ] Khởi tạo HTTP Server:
Yêu cầu: Tạo instance http.Server với các cấu hình cơ bản (địa chỉ, port, handler là router đã khởi tạo, có thể cả timeouts cơ bản).
[ ] Đăng ký Middleware Phi tính năng Cơ bản (thường trong package router hoặc server setup):
Yêu cầu: Đảm bảo các middleware thiết yếu như request logging, panic recovery, CORS (nếu cần) được áp dụng.
Mục đích: Tăng cường độ tin cậy và khả năng giám sát cho API.
[ ] Đăng ký Health Check Endpoint (trong router):
Yêu cầu: Cung cấp một endpoint (/healthz, /status) để kiểm tra tình trạng hoạt động của ứng dụng.
Mục đích: Hỗ trợ giám sát và tự động hóa vận hành.
VI. Quản lý Vòng đời Ứng dụng (Application Lifecycle Management)
[ ] Chạy Server trong Goroutine riêng:
Yêu cầu: httpServer.ListenAndServe() (hoặc tương tự) được chạy trong một goroutine để không block hàm main().
[ ] Xử lý Graceful Shutdown:
Yêu cầu: Lắng nghe các tín hiệu OS (SIGINT, SIGTERM) để thực hiện shutdown một cách an toàn.
Bao gồm:
Tạo channel để nhận tín hiệu: quit := make(chan os.Signal, 1)
Đăng ký tín hiệu: signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
Block cho đến khi nhận tín hiệu: <-quit
Gọi httpServer.Shutdown(ctx) với một context có timeout.
Log các bước shutdown.
Mục đích: Đảm bảo ứng dụng hoàn thành các request đang xử lý, đóng kết nối an toàn, tránh mất dữ liệu.
[ ] Xử lý Error và Log:
Yêu cầu: Tất cả các error từ các thành phần (DB, HTTP, middleware) nên được log rõ ràng, bao gồm thông tin context (ví dụ: request ID, method, URL).
Mục đích: Hỗ trợ debug và giám sát vấn đề khi xảy ra.
VII. Quản lý tài nguyên (Resource Management)
[ ] Đóng tài nguyên khi không cần thiết:
Yêu cầu: Đảm bảo tất cả các tài nguyên (connection, file, mutex, channel) được đóng hoặc giải phóng khi không còn sử dụng.
Mục đích: Tránh rò rỉ tài nguyên và đảm bảo hiệu quả sử dụng bộ nhớ.

63
docs/changelog.md Normal file
View File

@ -0,0 +1,63 @@
# Changelog
Tất cả những thay đổi đáng chú ý trong dự án sẽ được ghi lại ở đây.
Định dạng dựa trên [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
và dự án này tuân theo [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- **Logger Improvements**:
- Hỗ trợ phân biệt stdout/stderr cho các mức log khác nhau
- Tự động thêm thông tin người gọi (caller) vào log
- Hỗ trợ nhiều định dạng log (JSON, Text)
- Tự động thêm các trường mặc định vào mỗi log entry
- Tối ưu hiệu năng với buffer và sync.Pool
- Hỗ trợ log rotation thông qua các hooks
- Tài liệu chi tiết về cách sử dụng và cấu hình
- Thread-safe implementation
- Hỗ trợ context và request-scoped fields
- Tích hợp với cấu hình ứng dụng
### Changed
- **Logger Refactor**:
- Thay đổi cấu trúc package logger để dễ mở rộng
- Cải thiện hiệu suất với ít cấp phát bộ nhớ hơn
- Chuẩn hóa định dạng log đầu ra
- Cập nhật middleware HTTP để sử dụng logger mới
### Fixed
- **Logger**:
- Sửa lỗi race condition khi khởi tạo logger
- Đảm bảo tất cả log đều có đầy đủ context
- Cải thiện xử lý lỗi khi cấu hình không hợp lệ
### Changed
- Thay thế standard log package bằng Logrus trong toàn bộ ứng dụng
- Di chuyển cấu hình logging từ `app.log_level` sang section `logger` riêng biệt
- Cập nhật HTTP server để sử dụng structured logging
- Cải thiện validation cho database config với required_if conditions
- Nâng cấp cấu hình logger để hỗ trợ nhiều tùy chọn hơn
- Tối ưu hiệu năng của hệ thống logging
## [0.1.1] - 2025-05-14
### Added
- Triển khai module config với các chức năng:
- Đọc cấu hình từ file YAML
- Hỗ trợ biến môi trường
- Validation tự động các giá trị cấu hình
- Giá trị mặc định cho các tham số
- Thiết lập cấu trúc thư mục theo mô hình DDD (Domain-Driven Design)
- Cấu hình CI/CD với Gitea Workflows
- Cấu hình Docker cho môi trường development và production
- Tích hợp các Git hooks để đảm bảo chất lượng mã nguồn
## [0.1.0] - 2025-05-12
### Added
- Khởi tạo repository
- Tạo roadmap ban đầu
- Thiết lập kiến trúc hệ thống theo mô hình DDD
- Định nghĩa U-Hierarchy cho tổ chức mã nguồn

79
docs/general.md Normal file
View File

@ -0,0 +1,79 @@
# Giới thiệu chung
## Tổng quan
- ULFlow Golang Starter Kit là một khung phát triển backend theo mô hình Domain-Driven Design (DDD)
- Được thiết kế để tạo ra các ứng dụng có thể bảo trì và mở rộng dễ dàng
- Mục tiêu: Cung cấp một cấu trúc dự án chuẩn, các thành phần cơ bản và công cụ phát triển hiệu quả
## Nguyên tắc thiết kế
- **User-Centric**: Ưu tiên trải nghiệm người dùng, giảm thiểu sự phức tạp
- **Data-Oriented Programming (DOP)**: Thiết kế xoay quanh luồng dữ liệu và các biến đổi dữ liệu
- **Domain-Driven Design (DDD)**: Áp dụng các khái niệm cốt lõi (Bounded Context, Aggregates, Domain Events)
## Tầm nhìn
- Tạo ra nền tảng phát triển linh hoạt, dễ mở rộng cho các dự án backend Golang
- Tích hợp các công nghệ hiện đại, best practices, và quy trình phát triển chuẩn
- Cung cấp môi trường phát triển thống nhất cho team
## Hướng dẫn cài đặt
### Yêu cầu hệ thống
- Go 1.23
- Docker và Docker Compose
- Git
### Cài đặt
1. Clone repository
```bash
git clone [repo-url] my-project
cd my-project
```
2. Thiết lập môi trường
```bash
cp .env.example .env
# Chỉnh sửa file .env theo nhu cầu
```
3. Khởi động dev server
```bash
make dev
```
## Cấu trúc thư mục
Dự án được tổ chức theo cấu trúc DDD chuẩn, với các thành phần chính:
```
│── .gitea/ # Gitea workflows & hooks
│── cmd/ # Điểm vào của ứng dụng
│ └── app/ # Main application
│── internal/ # Mã nguồn chính của ứng dụng
│ │── domain/ # Các đối tượng nghiệp vụ cốt lõi
│ │ │── resource/ # Resource (Aggregates/Entities)
│ │ └── transaction/ # Transaction (Use Cases)
│ │── adapter/ # Giao tiếp với bên ngoài
│ │ │── persistence/ # Repository implementations
│ │ └── externalapi/ # External API clients
│ │── helper/ # Tiện ích chung
│ │ │── config/ # Configuration
│ │ │── logger/ # Logging
│ │ │── security/ # Authentication/Authorization
│ │ └── validation/ # Input validation
│ └── transport/ # Giao tiếp với client
│ └── http/ # HTTP handlers & middleware
│── docs/ # Tài liệu
│── templates/ # Các file mẫu
│── configs/ # File cấu hình
│── migrations/ # Database migrations
│── api/ # API definitions (OpenAPI/Swagger)
│── scripts/ # Các tiện ích
│── test/ # End-to-end tests
└── tools/ # Development tools
## Hỗ trợ và đóng góp
- Báo lỗi và feature request: Tạo issue trên repository
- Đóng góp: Tạo pull request theo quy trình được mô tả trong workflow.md
- Liên hệ: [email/contact-info]

48
docs/roadmap.md Normal file
View File

@ -0,0 +1,48 @@
# Roadmap phát triển
## Roadmap cơ bản
- [ ] Read Config from env file
- [ ] HTTP Server with gin framework
- [ ] JWT Authentication
- [ ] Database with GORM + Postgres
- [ ] Health Check
- [ ] Unit Test with testify (Template)
- [ ] CI/CD with Gitea for Dev Team
- [ ] Build and Deploy with Docker + Docker Compose on Local
## Giai đoạn 1: Cơ sở hạ tầng cơ bản
- [ ] Thiết lập cấu trúc dự án theo mô hình DDD
- [ ] Cấu hình cơ bản: env, logging, error handling
- [ ] Cấu hình Docker và Docker Compose
- [ ] HTTP server với Gin
- [ ] Database setup với GORM và Postgres
- [ ] Health check API endpoints
- Timeline: Q2/2025
## Giai đoạn 2: Bảo mật và xác thực
- [ ] JWT Authentication
- [ ] Role-based access control
- [ ] API rate limiting
- [ ] Secure headers và middleware
- Timeline: Q2/2025
## Giai đoạn 3: Tự động hóa
- [ ] Unit Test templates và mocks
- [ ] CI/CD với Gitea
- [ ] Automated deployment
- [ ] Linting và code quality checks
- Timeline: Q3/2025
## Giai đoạn 4: Mở rộng tính năng
- [ ] Go Feature Flag implementation
- [ ] Notification system
- [ ] Background job processing
- [ ] API documentation
- Timeline: Q3/2025
## Giai đoạn 5: Production readiness
- [ ] Performance optimization
- [ ] Monitoring và observability
- [ ] Backup và disaster recovery
- [ ] Security hardening
- Timeline: Q4/2025

136
docs/secrets.md Normal file
View File

@ -0,0 +1,136 @@
# CI/CD Workflows
ULFlow Starter Kit sử dụng Gitea Actions để tự động hóa quy trình CI/CD. Tài liệu này mô tả các workflow được cấu hình và các biến cần thiết.
## Tổng quan về quy trình
Quy trình CI/CD được thiết kế để hoạt động như sau:
1. **CI Pipeline** chạy trên tất cả các nhánh ngoại trừ `main`
2. **Docker Build** chạy khi đánh tag hoặc sau khi merge vào `main`
3. **Deploy to VPS** chỉ chạy khi đánh tag phiên bản (format `v*`)
## Workflow Files
### 1. CI Pipeline (`.gitea/workflows/ci.yml`)
CI Pipeline chạy trên tất cả các nhánh ngoại trừ `main` và bao gồm các job:
- **Lint**: Kiểm tra chất lượng mã với golangci-lint
- **Security Scan**: Quét lỗ hổng bảo mật với govulncheck
- **Test**: Chạy unit tests và tạo báo cáo coverage
- **Build**: Build ứng dụng và tạo artifact
- **Notify**: Thông báo kết quả
### 2. Docker Build & Deploy (`.gitea/workflows/docker.yml`)
Workflow này chạy khi:
- Đánh tag phiên bản (format `v*`)
- Sau khi CI Pipeline thành công trên nhánh `main`
Bao gồm các job:
- **Docker Build**: Build và push Docker image lên Gitea Container Registry
- **Deploy to VPS**: Triển khai ứng dụng lên VPS (chỉ khi đánh tag)
## Cấu hình Secrets
Để các workflow hoạt động đúng, bạn cần cấu hình các secrets sau trong Gitea:
### Secrets cho Runner
| Secret | Mô tả | Mặc định |
|--------|-------|----------|
| `RUNNER_LABEL` | Label của runner cho các job CI | `ubuntu-latest` |
| `DEPLOY_RUNNER` | Label của runner cho job deploy | `ubuntu-latest` |
### Secrets cho Docker Registry
| Secret | Mô tả | Bắt buộc |
|--------|-------|----------|
| `REGISTRY_URL` | URL của Gitea Container Registry | ✅ |
| `REGISTRY_USERNAME` | Username để đăng nhập vào registry | ✅ |
| `REGISTRY_PASSWORD` | Password để đăng nhập vào registry | ✅ |
| `REPOSITORY_PATH` | Đường dẫn repository trong registry | ✅ |
### Secrets cho Deployment
| Secret | Mô tả | Mặc định |
|--------|-------|----------|
| `CONTAINER_NAME` | Tên container | `ulflow-api-container` |
| `DOCKER_NETWORK` | Tên network Docker | `ulflow-network` |
| `APP_PORT` | Port để expose | `8080` |
| `APP_ENV` | Môi trường ứng dụng | `production` |
| `CONTAINER_MEMORY` | Giới hạn bộ nhớ | `1g` |
| `CONTAINER_CPU` | Giới hạn CPU | `1` |
| `HEALTH_CMD` | Command kiểm tra health | `curl -f http://localhost:8080/health || exit 1` |
| `HEALTH_INTERVAL` | Khoảng thời gian kiểm tra health | `30s` |
### Secrets cho Database
| Secret | Mô tả | Bắt buộc |
|--------|-------|----------|
| `DB_HOST` | Hostname của database | ✅ |
| `DB_USER` | Username database | ✅ |
| `DB_PASSWORD` | Password database | ✅ |
| `DB_NAME` | Tên database | ✅ |
### Secrets cho Security
| Secret | Mô tả | Bắt buộc |
|--------|-------|----------|
| `JWT_SECRET_KEY` | Secret key cho JWT | ✅ |
| `REFRESH_TOKEN_SECRET` | Secret key cho refresh token | ✅ |
| `API_KEY` | API key | ✅ |
| `ENCRYPTION_KEY` | Key mã hóa dữ liệu | ✅ |
## Cấu hình Gitea
### Tạo Secrets trong Gitea
1. Truy cập repository trong Gitea
2. Vào **Settings > Secrets**
3. Thêm từng secret với tên và giá trị tương ứng
### Cấu hình Runner
1. Đảm bảo Gitea Runner đã được cài đặt và kết nối với Gitea
2. Nếu sử dụng custom runner, cập nhật `RUNNER_LABEL``DEPLOY_RUNNER` tương ứng
## Ví dụ quy trình làm việc
1. **Phát triển tính năng:**
```bash
git checkout -b feat/new-feature
# Làm việc và commit
git push origin feat/new-feature
```
→ CI Pipeline tự động chạy
2. **Merge vào main:**
- Tạo Pull Request từ `feat/new-feature` vào `main`
- Sau khi merge, Docker Build tự động chạy
3. **Release phiên bản:**
```bash
git tag v1.0.0
git push origin v1.0.0
```
→ Docker Build và Deploy tự động chạy
## Troubleshooting
### Workflow không chạy
- Kiểm tra Gitea Runner đã được cấu hình đúng
- Kiểm tra quyền của repository và runner
### Docker Build thất bại
- Kiểm tra thông tin đăng nhập registry
- Kiểm tra quyền truy cập vào registry
### Deploy thất bại
- Kiểm tra kết nối đến VPS
- Kiểm tra Docker đã được cài đặt trên VPS
- Kiểm tra các biến môi trường đã được cấu hình đúng

130
docs/spec.md Normal file
View File

@ -0,0 +1,130 @@
# Thông số kỹ thuật
## Tech Stack
- **Golang 1.23.6**: Ngôn ngữ chính dùng phát triển backend
- **Docker + Docker Compose**: Containerization và orchestration
- **Air Tomb v1.49.0**: Hot reload cho phát triển
- **Gitea v1.21.0+**: Server, Runner, Action, Secret Manager, Registry Image
- **PostgreSQL 15+**: Cơ sở dữ liệu quan hệ
- **Ansible 2.14+**: Tự động hóa quy trình triển khai
- **Go Feature Flag v1.9.0+**: Quản lý tính năng
## Yêu cầu hệ thống
- Go 1.23.6+
- Docker Engine 24.0.0+
- Docker Compose v2.20.0+
- Git 2.40.0+
- PostgreSQL 15.0+
- Make (cho các tác vụ phát triển)
## Thư viện và frameworks chính
### Web Framework
- **Gin v1.10.0**: HTTP framework nhẹ, hiệu suất cao
- Middleware hỗ trợ: CORS, JWT, Rate limiting
- Tích hợp sẵn validator
### Database
- **GORM v1.26.1**: ORM cho Go
- Hỗ trợ nhiều database: PostgreSQL, MySQL, SQLite
- Migration tools tích hợp
- Connection pooling và retry mechanisms
### Authentication & Security
- **JWT (JSON Web Tokens)**
- **UUID v1.6.0**: Định danh duy nhất
- **bcrypt**: Mã hóa mật khẩu
### Configuration
- **Viper v1.17.0**: Quản lý cấu hình linh hoạt
- Hỗ trợ nhiều định dạng (JSON, YAML, TOML, ENV)
- Tích hợp với biến môi trường
### Logging
- **Logrus v1.9.3**: Thư viện logging cấu trúc
```yaml
logger:
level: "info" # debug, info, warn, error, fatal, panic
format: "json" # json hoặc text
enable_caller: true # Hiển thị thông tin người gọi
```
- Hỗ trợ nhiều mức độ log
- Tích hợp với các hệ thống log tập trung
- JSON format cho dễ phân tích
### Testing
- **Testify v1.10.0**: Framework testing cho Go
- Assertions mạnh mẽ
- Mock generation
- Test suites
- **go-sqlmock v1.5.0+**: Mock database
- **testcontainers-go**: Integration testing
### Monitoring & Observability
- **Prometheus**: Metrics collection
- **OpenTelemetry**: Distributed tracing
- **Health check endpoints**
- **Pprof**: Profiling
### Development Tools
- **GolangCI-Lint**: Static code analysis
- **Air Tomb**: Hot reload
- **Go Mod**: Quản lý dependencies
- **Makefile**: Tự động hóa tác vụ
## Cấu hình môi trường
### Development
- Hot reload với Air Tomb
- Local Docker environment
- Sample data và fixtures
### Testing
- CI/CD với Gitea Actions
- Automated test suites
- Lint và code quality checks
### Staging
- Triển khai tự động
- Môi trường gần với production
- Performance testing
### Production
- High availability setup
- Backup và disaster recovery
- Security hardening
## Quản lý cấu hình
ULFlow Starter Kit sử dụng một hệ thống cấu hình linh hoạt dựa trên Viper:
### Đặc điểm
- Đọc cấu hình từ nhiều nguồn (file, biến môi trường)
- Xác thực tự động các giá trị
- Giá trị mặc định thông minh
- Cấu trúc mạnh (strongly typed configuration)
### Sử dụng
```go
// Khởi tạo config loader
configLoader := config.NewConfigLoader()
// Load cấu hình
cfg, err := configLoader.Load()
if err != nil {
log.Fatalf("Failed to load configuration: %v", err)
}
// Truy cập cấu hình
appName := cfg.App.Name
dbHost := cfg.Database.Host
```
## Các tệp cấu hình
- `.env.example`: Template biến môi trường
- `configs/config.yaml`: Cấu hình ứng dụng trong môi trường dev
- `templates/config.example.yaml`: Template cấu hình ứng dụng
- `docker-compose.yml`: Cấu hình container
- `go.mod`, `go.sum`: Quản lý dependencies

119
docs/testing.md Normal file
View File

@ -0,0 +1,119 @@
# Testing
## Tổng quan
Testing là một phần quan trọng trong quy trình phát triển, đảm bảo chất lượng mã nguồn và giảm thiểu bugs. Dự án này sử dụng các công cụ và phương pháp testing tiêu chuẩn trong Golang.
## Unit Testing
### Công cụ sử dụng
- Testify: Framework unit testing cho Go
- Table-driven tests: Thiết kế test cases linh hoạt
- Mocking: Giả lập dependencies
### Quy ước và cấu trúc
- Mỗi package cần có file `*_test.go` tương ứng
- Các test functions có format `Test{FunctionName}`
- Test cases nên bao gồm cả happy path và error cases
- Coverage yêu cầu tối thiểu: 80%
- Sử dụng t.Run() để chạy các subtest
### Mẫu Unit Test
```go
// user_service_test.go
package user
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// Mocking repository
type MockUserRepository struct {
mock.Mock
}
func (m *MockUserRepository) FindByID(id string) (*User, error) {
args := m.Called(id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*User), args.Error(1)
}
func TestGetUser(t *testing.T) {
// Test cases
testCases := []struct {
name string
userID string
mockUser *User
mockError error
expectedUser *User
expectedError error
}{
{
name: "successful_get",
userID: "123",
mockUser: &User{ID: "123", Name: "Test User"},
mockError: nil,
expectedUser: &User{ID: "123", Name: "Test User"},
expectedError: nil,
},
{
name: "user_not_found",
userID: "456",
mockUser: nil,
mockError: ErrUserNotFound,
expectedUser: nil,
expectedError: ErrUserNotFound,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Setup mock
mockRepo := new(MockUserRepository)
mockRepo.On("FindByID", tc.userID).Return(tc.mockUser, tc.mockError)
// Create service with mock dependency
service := NewUserService(mockRepo)
// Call the method
user, err := service.GetUser(tc.userID)
// Assert results
assert.Equal(t, tc.expectedUser, user)
assert.Equal(t, tc.expectedError, err)
// Verify expectations
mockRepo.AssertExpectations(t)
})
}
}
```
## Integration Testing
### Approach
- Test containers: Chạy dependent services (database, caching, etc) trong Docker containers
- API testing: Kiểm tra endpoints và responses
- DB testing: Kiểm tra queries và migrations
### Setup và Teardown
- Sử dụng `TestMain` để setup và teardown test environment
- Cấu hình Docker containers cho testing
- Cleanup sau khi chạy tests
## E2E Testing
- API black-box testing
- Sequence testing cho business flows
- Performance testing
## CI/CD Integration
- Chạy tests trên mỗi commit và PR
- Lưu test results và coverage
- Chỉ merge khi tests pass
## Best Practices
1. **Write tests first (TDD approach)**
2. **Keep tests independent và idempotent**
3. **Sử dụng fixtures cho test data**
4. **Tránh hard-coding external dependencies**
5. **Tách common test code thành helper functions**

57
docs/ux.md Normal file
View File

@ -0,0 +1,57 @@
# Thiết kế trải nghiệm người dùng
## Nguyên tắc thiết kế API
### API Design
- RESTful APIs là mặc định
- Sử dụng HTTP verbs phù hợp (GET, POST, PUT, DELETE)
- Định dạng URL: `/api/v{version}/{resource}/{id}`
- Versions được chỉ định trong URL path
- Query parameters cho filtering, sorting, pagination
### Responses
- Sử dụng HTTP status codes một cách nhất quán
- Cấu trúc JSON responses:
```json
{
"status": "success",
"data": { ... },
"meta": { "pagination": { ... } }
}
```
- Cấu trúc error responses:
```json
{
"status": "error",
"error": {
"code": "ERROR_CODE",
"message": "Human readable message",
"details": { ... }
}
}
```
### Authentication
- Bearer token trong Authorization header
- API key cho các dịch vụ được tin cậy
- OAuth 2.0 cho third-party applications
## Documentation
- OpenAPI/Swagger spec được tự động sinh
- API documentation tích hợp với code
- Ví dụ request/response cho mọi endpoint
## Rate Limiting
- Rate limits dựa trên IP hoặc user
- Headers thông báo limits và remaining requests
- Graceful handling khi vượt giới hạn
## Monitoring
- Tracking API usage và performance
- User behavior analytics
- Error tracking và alerting
## Testing
- End-to-end API testing
- Performance và load testing
- Security testing

84
docs/workflow.md Normal file
View File

@ -0,0 +1,84 @@
# Quy trình phát triển
## Branch Strategy
- Trunk-Based Development là phương pháp quản lý nhánh trong Git, nơi tất cả thay đổi được tích hợp thường xuyên vào nhánh chính (`main`), còn gọi là "trunk".
- Tên nhánh trùng với tiêu chuẩn của Git Commit Message
- `feat`: Tạo tính năng mới
- `fix`: Sửa lỗi
- `docs`: Thay đổi tài liệu
- `style`: Thay đổi không ảnh hưởng đến logic
- `refactor`: Refactor code
- `test`: Thêm hoặc sửa test
- `chore`: Thay đổi cấu hình hoặc các task không liên quan đến code
## Quy trình làm việc
### 1. Khởi tạo công việc
- Tạo issue mô tả công việc cần thực hiện
- Gán assignee và labels phù hợp
- Ước lượng effort và deadline
### 2. Phát triển
- Tạo nhánh từ `main` với quy ước đặt tên:
```
<type>/<issue-number>-<short-description>
```
Ví dụ: `feat/123-user-authentication`
- Thực hiện thay đổi, tuân thủ coding standards
- Commit thường xuyên với commit message rõ ràng
```
<type>(<scope>): <subject>
```
Ví dụ: `feat(auth): implement JWT token validation`
### 3. Kiểm thử
- Viết unit tests cho mọi thay đổi
- Đảm bảo test coverage đạt yêu cầu
- Chạy linting và static code analysis
### 4. Review
- Tạo pull request (PR) vào nhánh `main`
- Mô tả chi tiết các thay đổi
- Request review từ ít nhất 1 team member
- Khi được approve, resolve tất cả comments
### 5. Triển khai
- Merge PR vào `main` (squash và fast-forward)
- CI/CD pipeline sẽ tự động build và deploy
- Theo dõi logs và metrics sau khi deploy
## Gitea Workflow
### Gitea Actions
- Cấu hình trong `.gitea/workflows/`
- Định nghĩa các jobs: build, test, lint, deploy
- Trigger dựa trên events: push, PR, tag
### Gitea Secrets
- Sử dụng Secret Manager để lưu trữ credentials
- Truy cập secrets trong workflows
### Gitea Registry
- Build và push Docker images
- Versioning theo semantic versioning
## Conventions
### Commit Messages
- Format: `<type>(<scope>): <subject>`
- Types: feat, fix, docs, style, refactor, test, chore
- Scope: module/component tương ứng
- Subject: mô tả ngắn gọn, rõ ràng
### Versioning
- Theo Semantic Versioning: MAJOR.MINOR.PATCH
- MAJOR: breaking changes
- MINOR: backwards-compatible features
- PATCH: backwards-compatible bug fixes
### Release Process
- Tạo tag với version mới
- Cập nhật changelog
- Deploy lên môi trường production

72
go.mod Normal file
View File

@ -0,0 +1,72 @@
module starter-kit
go 1.23.6
require (
github.com/gin-gonic/gin v1.10.0
github.com/go-playground/validator/v10 v10.20.0
github.com/google/uuid v1.6.0
github.com/sirupsen/logrus v1.9.3
github.com/spf13/viper v1.17.0
github.com/stretchr/testify v1.10.0
gorm.io/driver/mysql v1.5.7
gorm.io/driver/postgres v1.5.11
gorm.io/driver/sqlite v1.5.7
gorm.io/gorm v1.26.1
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-sql-driver/mysql v1.9.2 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.4 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/magiconair/properties v1.8.9 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.28 // indirect
github.com/mitchellh/mapstructure v1.5.0 // 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.2 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/sagikazarmark/locafero v0.3.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.10.0 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/sync v0.14.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.25.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

594
go.sum Normal file
View File

@ -0,0 +1,594 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
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.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
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.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/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/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
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.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg=
github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
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/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
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/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
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.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM=
github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
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/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
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.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/sagikazarmark/locafero v0.3.0 h1:zT7VEGWC2DTflmccN/5T1etyKvxSxpHsjb9cJvm4SvQ=
github.com/sagikazarmark/locafero v0.3.0/go.mod h1:w+v7UsPNFwzF1cHuOajOOzoq4U7v/ig1mpRjqV+Bu1U=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY=
github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.17.0 h1:I5txKw7MJasPL/BrfkbA0Jyo/oELqVmux4pR/UxOMfI=
github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0+yVI=
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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
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.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 h1:yiW+nvdHb9LVqSHQBXfZCieqV4fzYhNBql77zY0ykqs=
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637/go.mod h1:BHsqpu/nsuzkT5BpiH1EMZPLyqSMM8JbIavyFACoFNk=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
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=
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314=
gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.26.1 h1:ghB2gUI9FkS46luZtn6DLZ0f6ooBJ5IbVej2ENFDjRw=
gorm.io/gorm v1.26.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View File

@ -0,0 +1,117 @@
package config
import (
"fmt"
"os"
"strings"
"github.com/go-playground/validator/v10"
"github.com/spf13/viper"
)
// ConfigLoader định nghĩa interface để load cấu hình
type ConfigLoader interface {
Load() (*Config, error)
}
// ViperConfigLoader triển khai ConfigLoader với Viper
type ViperConfigLoader struct {
configPaths []string
configName string
configType string
envPrefix string
}
// NewConfigLoader tạo ConfigLoader mới với các giá trị mặc định
func NewConfigLoader() ConfigLoader {
return &ViperConfigLoader{
configPaths: []string{"./configs", ".", "./templates"},
configName: "config",
configType: "yaml",
envPrefix: "APP",
}
}
// Load đọc cấu hình từ file và biến môi trường
func (l *ViperConfigLoader) Load() (*Config, error) {
// Khởi tạo viper
v := viper.New()
// Thiết lập tên config và loại
v.SetConfigName(l.configName)
v.SetConfigType(l.configType)
// Thêm các paths để tìm config
for _, path := range l.configPaths {
v.AddConfigPath(path)
}
// Tự động đọc biến môi trường
v.AutomaticEnv()
v.SetEnvPrefix(l.envPrefix)
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
// Thiết lập giá trị mặc định
setDefaultValues(v)
// Đọc cấu hình
if err := v.ReadInConfig(); err != nil {
// Chỉ cảnh báo nếu không tìm thấy file, không gây lỗi
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
return nil, fmt.Errorf("error reading config file: %v", err)
}
}
// Set default logger level
if os.Getenv("LOG_LEVEL") == "" {
os.Setenv("LOG_LEVEL", "info")
}
// Bind cấu hình vào struct
var config Config
if err := v.Unmarshal(&config); err != nil {
return nil, fmt.Errorf("unable to decode config into struct: %v", err)
}
// Validate cấu hình
if err := validateConfig(&config); err != nil {
return nil, fmt.Errorf("config validation error: %v", err)
}
return &config, nil
}
// setDefaultValues thiết lập các giá trị mặc định cho config
func setDefaultValues(v *viper.Viper) {
// App defaults
v.SetDefault("app.name", "GoStarter")
v.SetDefault("app.version", "0.1.0")
v.SetDefault("app.environment", "development")
v.SetDefault("app.log_level", "info")
v.SetDefault("app.timezone", "UTC")
// Server defaults
v.SetDefault("server.host", "0.0.0.0")
v.SetDefault("server.port", 8080)
v.SetDefault("server.read_timeout", 15) // seconds
v.SetDefault("server.write_timeout", 15) // seconds
v.SetDefault("server.shutdown_timeout", 30) // seconds
v.SetDefault("server.trusted_proxies", []string{})
v.SetDefault("server.allow_origins", []string{"*"})
// Database defaults
v.SetDefault("database.driver", "postgres")
v.SetDefault("database.host", "localhost")
v.SetDefault("database.port", 5432)
v.SetDefault("database.max_open_conns", 25)
v.SetDefault("database.max_idle_conns", 5)
v.SetDefault("database.conn_max_lifetime", 300) // seconds
v.SetDefault("database.ssl_mode", "disable")
v.SetDefault("database.migration_path", "migrations")
}
// validateConfig xác thực cấu hình
func validateConfig(config *Config) error {
validate := validator.New()
return validate.Struct(config)
}

View File

@ -0,0 +1,48 @@
package config
// AppConfig chứa thông tin cấu hình của ứng dụng
type AppConfig struct {
Name string `mapstructure:"name" validate:"required"`
Version string `mapstructure:"version" validate:"required"`
Environment string `mapstructure:"environment" validate:"required,oneof=development staging production"`
Timezone string `mapstructure:"timezone" validate:"required"`
}
// ServerConfig chứa thông tin cấu hình server
type ServerConfig struct {
Host string `mapstructure:"host" validate:"required"`
Port int `mapstructure:"port" validate:"required,min=1,max=65535"`
ReadTimeout int `mapstructure:"read_timeout" validate:"required,min=1"`
WriteTimeout int `mapstructure:"write_timeout" validate:"required,min=1"`
ShutdownTimeout int `mapstructure:"shutdown_timeout" validate:"required,min=1"`
TrustedProxies []string `mapstructure:"trusted_proxies"`
AllowOrigins []string `mapstructure:"allow_origins"`
}
// DatabaseConfig chứa thông tin cấu hình database
type DatabaseConfig struct {
Driver string `mapstructure:"driver" validate:"required,oneof=postgres mysql sqlite"`
Host string `mapstructure:"host" validate:"required_if=Driver postgres,required_if=Driver mysql"`
Port int `mapstructure:"port" validate:"required_if=Driver postgres,required_if=Driver mysql,min=1,max=65535"`
Username string `mapstructure:"username" validate:"required_if=Driver postgres,required_if=Driver mysql"`
Password string `mapstructure:"password" validate:"required_if=Driver postgres,required_if=Driver mysql"`
Database string `mapstructure:"database" validate:"required"`
SSLMode string `mapstructure:"ssl_mode" validate:"omitempty,oneof=disable prefer require verify-ca verify-full"`
MaxOpenConns int `mapstructure:"max_open_conns" validate:"min=1"`
MaxIdleConns int `mapstructure:"max_idle_conns" validate:"min=1"`
ConnMaxLifetime int `mapstructure:"conn_max_lifetime" validate:"min=1"`
MigrationPath string `mapstructure:"migration_path" validate:"required"`
}
// Config là struct tổng thể chứa tất cả các cấu hình
type Config struct {
App AppConfig `mapstructure:"app" validate:"required"`
Server ServerConfig `mapstructure:"server" validate:"required"`
Database DatabaseConfig `mapstructure:"database" validate:"required"`
Logger LoggerConfig `mapstructure:"logger" validate:"required"`
}
// LoggerConfig chứa cấu hình cho logger
type LoggerConfig struct {
Level string `mapstructure:"level" validate:"required,oneof=debug info warn error"`
}

View File

@ -0,0 +1,154 @@
package database
import (
"context"
"database/sql"
"fmt"
"sync"
"time"
"starter-kit/internal/helper/config"
"starter-kit/internal/helper/logger"
"github.com/sirupsen/logrus"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
var (
// dbInstance is the singleton database instance
dbInstance *gorm.DB
dbOnce sync.Once
dbErr error
)
// Database wraps the database connection
type Database struct {
DB *gorm.DB
Config *config.DatabaseConfig
Logger *logrus.Logger
}
// NewConnection creates a new database connection based on configuration
func NewConnection(cfg *config.DatabaseConfig) (*gorm.DB, error) {
dbOnce.Do(func() {
switch cfg.Driver {
case "postgres":
dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
cfg.Host, cfg.Port, cfg.Username, cfg.Password, cfg.Database, cfg.SSLMode)
dbInstance, dbErr = gorm.Open(postgres.Open(dsn), &gorm.Config{})
case "mysql":
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
cfg.Username, cfg.Password, cfg.Host, cfg.Port, cfg.Database)
dbInstance, dbErr = gorm.Open(mysql.Open(dsn), &gorm.Config{})
case "sqlite":
dbInstance, dbErr = gorm.Open(sqlite.Open(cfg.Database), &gorm.Config{})
default:
dbErr = fmt.Errorf("unsupported database driver: %s", cfg.Driver)
return
}
if dbErr != nil {
dbErr = fmt.Errorf("failed to connect to database: %w", dbErr)
return
}
// Set connection pool settings
var sqlDB *sql.DB
sqlDB, dbErr = dbInstance.DB()
if dbErr != nil {
dbErr = fmt.Errorf("failed to get database instance: %w", dbErr)
return
}
sqlDB.SetMaxOpenConns(cfg.MaxOpenConns)
sqlDB.SetMaxIdleConns(cfg.MaxIdleConns)
sqlDB.SetConnMaxLifetime(time.Duration(cfg.ConnMaxLifetime) * time.Second)
logger.Info("Database connection established successfully")
})
return dbInstance, dbErr
}
// Close closes the database connection
func Close() error {
if dbInstance == nil {
return nil
}
sqlDB, err := dbInstance.DB()
if err != nil {
return fmt.Errorf("failed to get database instance: %w", err)
}
if err := sqlDB.Close(); err != nil {
return fmt.Errorf("error closing database: %w", err)
}
logger.Info("Database connection closed")
return nil
}
// Migrate runs database migrations
func Migrate(cfg config.DatabaseConfig) error {
_, err := NewConnection(&cfg)
if err != nil {
return fmt.Errorf("failed to connect to database: %w", err)
}
// TODO: Add your database migrations here
// Example: return dbInstance.AutoMigrate(&models.User{}, &models.Post{})
return nil
}
// GetDB returns the database instance
func GetDB() *gorm.DB {
return dbInstance
}
// WithTransaction executes a function within a database transaction
func WithTransaction(fn func(tx *gorm.DB) error) error {
tx := dbInstance.Begin()
if tx.Error != nil {
return tx.Error
}
defer func() {
if r := recover(); r != nil {
tx.Rollback()
panic(r)
}
}()
if err := fn(tx); err != nil {
if rbErr := tx.Rollback().Error; rbErr != nil {
return fmt.Errorf("tx: %v, rb err: %v", err, rbErr)
}
return err
}
return tx.Commit().Error
}
// HealthCheck checks if the database is reachable
func HealthCheck() error {
if dbInstance == nil {
return fmt.Errorf("database not initialized")
}
sqlDB, err := dbInstance.DB()
if err != nil {
return fmt.Errorf("failed to get database instance: %w", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return sqlDB.PingContext(ctx)
}

View File

@ -0,0 +1,75 @@
package feature
import (
"encoding/json"
"fmt"
"os"
"sync"
)
// FeatureFlag represents a feature flag configuration
type FeatureFlag struct {
Enabled bool `json:"enabled"`
}
// featureFlags stores the feature flags
var (
featureFlags = make(map[string]FeatureFlag)
mu sync.RWMutex
)
// Feature flags
const (
EnableDatabase = "enable_database"
)
// Init initializes feature flags
func Init() error {
// Check if config file exists
configFile := ".feature-flags.json"
config, err := os.ReadFile(configFile)
if err != nil {
// File doesn't exist, create with default config
config = []byte(`{
"enable_database": {
"enabled": false
}
}`)
if err := os.WriteFile(configFile, config, 0644); err != nil {
return fmt.Errorf("failed to write default feature flags config: %w", err)
}
}
// Parse feature flags from JSON
var flags map[string]FeatureFlag
if err := json.Unmarshal(config, &flags); err != nil {
return fmt.Errorf("failed to parse feature flags: %w", err)
}
// Store feature flags
mu.Lock()
featureFlags = flags
mu.Unlock()
return nil
}
// IsEnabled checks if a feature flag is enabled
func IsEnabled(flagKey string) bool {
mu.RLock()
defer mu.RUnlock()
flag, exists := featureFlags[flagKey]
if !exists {
return false
}
return flag.Enabled
}
// Close cleans up feature flag resources
func Close() error {
mu.Lock()
defer mu.Unlock()
featureFlags = make(map[string]FeatureFlag)
return nil
}

View File

@ -0,0 +1,118 @@
# Logger Package
A structured, high-performance logging package built on top of Logrus with the following features:
## Features
- **Structured Logging**: JSON and Text formats supported
- **Level-based Logging**: Debug, Info, Warn, Error, Fatal, Panic, Trace
- **Performance Optimized**: Low allocation design with sync.Pool
- **Context Support**: Add request-scoped fields
- **Caller Information**: Automatically include file and line numbers
- **Multiple Outputs**: Stdout/Stderr separation
- **Thread-safe**: Safe for concurrent use
- **Customizable**: Various configuration options
## Installation
```go
import "starter-kit/internal/helper/logger"
```
## Basic Usage
### Initialization
```go
// Initialize with default configuration
logger.Init(&logger.LogConfig{
Level: "info",
Format: "json",
EnableCaller: true,
ReportCaller: true,
})
```
### Logging Messages
```go
// Simple logging
logger.Info("Application started")
logger.Debug("Debug information")
logger.Warn("Warning message")
logger.Error("Error occurred")
// With fields
logger.WithFields(logger.Fields{
"key": "value",
"num": 42,
}).Info("Log with fields")
// With error
if err != nil {
logger.WithError(err).Error("Operation failed")
}
```
## Configuration Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| Level | string | "info" | Log level (debug, info, warn, error, fatal, panic) |
| Format | string | "json" | Output format (json, text) |
| EnableCaller | bool | true | Include caller information |
| BufferSize | int | 256 | Buffer size for async logging |
| DisableColor | bool | false | Disable colored output (text format only) |
| ReportCaller | bool | true | Report the calling method |
## Best Practices
1. **Use Appropriate Log Levels**:
- ERROR: Something failed, needs attention
- WARN: Unexpected but handled situation
- INFO: Important business process has started/ended
- DEBUG: Detailed information for debugging
2. **Structured Logging**:
- Always use fields for structured data
- Keep log messages concise and consistent
3. **Performance**:
- Use `WithFields` when logging multiple key-value pairs
- Avoid expensive operations in log statements
## Example Output
### JSON Format
```json
{
"@timestamp": "2023-04-01T12:00:00Z",
"level": "info",
"message": "Request processed",
"caller": "handler/user.go:42",
"method": "GET",
"path": "/api/users",
"status": 200,
"duration": 42.5,
"request_id": "abc123"
}
```
### Text Format
```
INFO[0000] Request processed caller=handler/user.go:42 method=GET path=/api/users status=200 duration=42.5 request_id=abc123
```
## Testing
```bash
# Run tests
go test -v ./...
# Run with race detector
go test -race ./...
```
## License
[MIT](LICENSE)

View File

@ -0,0 +1,291 @@
package logger
import (
"fmt"
"io"
"os"
"runtime"
"strings"
"sync"
"time"
"github.com/sirupsen/logrus"
)
var (
// log is the global logger instance
log *logrus.Logger
// logMutex ensures thread-safe logger configuration
logMutex sync.RWMutex
// defaultFields are included in every log entry
defaultFields = logrus.Fields{
"app_name": "starter-kit",
"env": os.Getenv("APP_ENV"),
}
)
// Buffer size for async logging
const defaultBufferSize = 256
// Fields is a type alias for logrus.Fields
type Fields = logrus.Fields
// LogConfig holds configuration for the logger
type LogConfig struct {
Level string `json:"level"`
Format string `json:"format"`
EnableCaller bool `json:"enable_caller"`
BufferSize int `json:"buffer_size"`
DisableColor bool `json:"disable_color"`
ReportCaller bool `json:"report_caller"`
}
// Init initializes the logger with the given configuration
func Init(config *LogConfig) {
logMutex.Lock()
defer logMutex.Unlock()
// Create new logger instance
log = logrus.New()
// Set default values if config is nil
if config == nil {
config = &LogConfig{
Level: "info",
Format: "json",
EnableCaller: true,
BufferSize: defaultBufferSize,
DisableColor: false,
ReportCaller: true,
}
}
// Set log level
setLogLevel(config.Level)
// Set log formatter
setLogFormatter(config)
// Configure output
configureOutput()
// Add hooks
addHooks(config)
}
// configureOutput sets up the output writers
func configureOutput() {
// Use async writer if buffer size > 0
log.SetOutput(io.Discard) // Discard logs by default
// Create multi-writer for stdout/stderr
log.AddHook(&writerHook{
Writer: os.Stdout,
LogLevels: []logrus.Level{logrus.InfoLevel, logrus.DebugLevel},
})
log.AddHook(&writerHook{
Writer: os.Stderr,
LogLevels: []logrus.Level{logrus.PanicLevel, logrus.FatalLevel, logrus.ErrorLevel, logrus.WarnLevel},
})
}
// setLogLevel configures the log level
func setLogLevel(level string) {
lvl, err := logrus.ParseLevel(strings.ToLower(level))
if err != nil {
logrus.Warnf("Invalid log level '%s', defaulting to 'info'", level)
lvl = logrus.InfoLevel
}
log.SetLevel(lvl)
}
// setLogFormatter configures the log formatter
func setLogFormatter(config *LogConfig) {
switch strings.ToLower(config.Format) {
case "text":
log.SetFormatter(&logrus.TextFormatter{
FullTimestamp: true,
TimestampFormat: time.RFC3339Nano,
DisableColors: config.DisableColor,
CallerPrettyfier: callerPrettyfier,
})
default: // JSON format
log.SetFormatter(&logrus.JSONFormatter{
TimestampFormat: time.RFC3339Nano,
DisableTimestamp: false,
DisableHTMLEscape: true,
CallerPrettyfier: callerPrettyfier,
FieldMap: logrus.FieldMap{
logrus.FieldKeyTime: "@timestamp",
logrus.FieldKeyLevel: "level",
logrus.FieldKeyMsg: "message",
logrus.FieldKeyFunc: "caller",
},
})
}
}
// addHooks adds additional hooks to the logger
func addHooks(config *LogConfig) {
// Add caller info hook if enabled
if config.EnableCaller || config.ReportCaller {
log.AddHook(&callerHook{})
}
// Add default fields hook
log.AddHook(&defaultFieldsHook{fields: defaultFields})
}
// writerHook sends logs to different writers based on level
type writerHook struct {
Writer io.Writer
LogLevels []logrus.Level
}
func (hook *writerHook) Fire(entry *logrus.Entry) error {
line, err := entry.Logger.Formatter.Format(entry)
if err != nil {
return err
}
_, err = hook.Writer.Write(line)
return err
}
func (hook *writerHook) Levels() []logrus.Level {
return hook.LogLevels
}
// callerHook adds caller information to the log entry
type callerHook struct{}
func (hook *callerHook) Fire(entry *logrus.Entry) error {
entry.Data["caller"] = getCaller()
return nil
}
func (hook *callerHook) Levels() []logrus.Level {
return logrus.AllLevels
}
// defaultFieldsHook adds default fields to each log entry
type defaultFieldsHook struct {
fields logrus.Fields
}
func (hook *defaultFieldsHook) Fire(entry *logrus.Entry) error {
for k, v := range hook.fields {
if _, exists := entry.Data[k]; !exists {
entry.Data[k] = v
}
}
return nil
}
func (hook *defaultFieldsHook) Levels() []logrus.Level {
return logrus.AllLevels
}
// getCaller returns the caller's file and line number
func getCaller() string {
_, file, line, ok := runtime.Caller(5) // Adjust the skip to get the right caller
if !ok {
return ""
}
return fmt.Sprintf("%s:%d", file, line)
}
// callerPrettyfier formats the caller info
func callerPrettyfier(f *runtime.Frame) (string, string) {
// Get the relative path of the file
file := ""
if f != nil {
file = f.File
// Get the last 2 segments of the path
parts := strings.Split(file, "/")
if len(parts) > 2 {
file = strings.Join(parts[len(parts)-2:], "/")
}
file = fmt.Sprintf("%s:%d", file, f.Line)
}
// Return empty function name to hide it
return "", file
}
// GetLogger returns the global logger instance
func GetLogger() *logrus.Logger {
logMutex.RLock()
defer logMutex.RUnlock()
return log
}
// SetLevel sets the logging level
func SetLevel(level string) {
logMutex.Lock()
defer logMutex.Unlock()
setLogLevel(level)
}
// WithFields creates an entry with the given fields and includes the caller
func WithFields(fields Fields) *logrus.Entry {
return log.WithFields(logrus.Fields(fields)).WithField("caller", getCaller())
}
// WithError adds an error as a single field and includes the caller
func WithError(err error) *logrus.Entry {
return log.WithError(err).WithField("caller", getCaller())
}
// Debug logs a message at level Debug
func Debug(args ...interface{}) {
log.WithField("caller", getCaller()).Debug(args...)
}
// Info logs a message at level Info
func Info(args ...interface{}) {
log.WithField("caller", getCaller()).Info(args...)
}
// Infof logs a formatted message at level Info
func Infof(format string, args ...interface{}) {
log.WithField("caller", getCaller()).Infof(format, args...)
}
// Warn logs a message at level Warn
func Warn(args ...interface{}) {
log.WithField("caller", getCaller()).Warn(args...)
}
// Error logs a message at level Error
func Error(args ...interface{}) {
log.WithField("caller", getCaller()).Error(args...)
}
// Errorf logs a formatted message at level Error
func Errorf(format string, args ...interface{}) {
log.WithField("caller", getCaller()).Errorf(format, args...)
}
// Fatal logs a message at level Fatal then the process will exit with status set to 1
func Fatal(args ...interface{}) {
log.WithField("caller", getCaller()).Fatal(args...)
}
// Panic logs a message at level Panic and then panics
func Panic(args ...interface{}) {
log.WithField("caller", getCaller()).Panic(args...)
}
// Trace logs a message at level Trace
func Trace(args ...interface{}) {
log.WithField("caller", getCaller()).Trace(args...)
}
// Initialize logger with default configuration
func init() {
Init(&LogConfig{
Level: "info",
Format: "json",
EnableCaller: true,
BufferSize: defaultBufferSize,
ReportCaller: true,
})
}

View File

@ -0,0 +1,249 @@
package logger
import (
"bytes"
"encoding/json"
"io"
"os"
"strings"
"sync"
"testing"
"time"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// captureOutput captures log output for testing
func captureOutput(f func()) string {
r, w, err := os.Pipe()
if err != nil {
panic(err)
}
// Replace stdout/stderr
oldStdout := os.Stdout
oldStderr := os.Stderr
os.Stdout = w
os.Stderr = w
// Reset the logger after the test
oldLogger := log
defer func() {
log = oldLogger
os.Stdout = oldStdout
os.Stderr = oldStderr
}()
// Create a new logger for testing
log = logrus.New()
log.SetOutput(w)
log.SetFormatter(&logrus.JSONFormatter{
TimestampFormat: time.RFC3339Nano,
})
// Run the function
f()
// Close the writer
w.Close()
// Read the output
var buf bytes.Buffer
_, err = io.Copy(&buf, r)
if err != nil {
panic(err)
}
return buf.String()
}
func TestLogger_Levels(t *testing.T) {
tests := []struct {
name string
setLevel string
logFunc func()
expected bool
}{
{
name: "debug level shows debug logs",
setLevel: "debug",
logFunc: func() { Debug("test debug") },
expected: true,
},
{
name: "info level hides debug logs",
setLevel: "info",
logFunc: func() { Debug("test debug") },
expected: false,
},
{
name: "error level shows error logs",
setLevel: "error",
logFunc: func() { Error("test error") },
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
output := captureOutput(func() {
Init(&LogConfig{Level: tt.setLevel, Format: "json"})
tt.logFunc()
})
if tt.expected {
assert.Contains(t, output, "message")
} else {
assert.Empty(t, output)
}
})
}
}
func TestLogger_JSONOutput(t *testing.T) {
output := captureOutput(func() {
Init(&LogConfig{Level: "debug", Format: "json"})
Info("test message")
})
// Verify it's valid JSON
var data map[string]interface{}
err := json.Unmarshal([]byte(output), &data)
require.NoError(t, err)
// Check required fields
assert.Contains(t, data, "message")
assert.Contains(t, data, "level")
assert.Contains(t, data, "@timestamp")
}
func TestLogger_TextOutput(t *testing.T) {
output := captureOutput(func() {
Init(&LogConfig{Level: "info", Format: "text"})
Warn("test warning")
})
// Basic checks for text format
assert.Contains(t, output, "test warning")
assert.Contains(t, output, "level=warning")
}
func TestLogger_WithFields(t *testing.T) {
output := captureOutput(func() {
Init(&LogConfig{Level: "info", Format: "json"})
WithFields(Fields{
"key1": "value1",
"key2": 42,
}).Info("test fields")
})
var data map[string]interface{}
err := json.Unmarshal([]byte(output), &data)
require.NoError(t, err)
assert.Equal(t, "value1", data["key1"])
assert.Equal(t, float64(42), data["key2"])
}
func TestLogger_WithError(t *testing.T) {
err := io.EOF
output := captureOutput(func() {
Init(&LogConfig{Level: "error", Format: "json"})
WithError(err).Error("test error")
})
var data map[string]interface{}
jsonErr := json.Unmarshal([]byte(output), &data)
require.NoError(t, jsonErr)
assert.Contains(t, data["error"], "EOF")
}
func TestLogger_CallerInfo(t *testing.T) {
output := captureOutput(func() {
Init(&LogConfig{
Level: "info",
Format: "json",
EnableCaller: true,
})
Info("test caller")
})
var data map[string]interface{}
err := json.Unmarshal([]byte(output), &data)
require.NoError(t, err)
// Caller should be in the format "file:line"
caller, ok := data["caller"].(string)
require.True(t, ok)
assert.True(t, strings.Contains(caller, ".go:"), "caller should contain file and line number")
}
func TestLogger_Concurrent(t *testing.T) {
Init(&LogConfig{Level: "info", Format: "json"})
var wg sync.WaitGroup
count := 10
for i := 0; i < count; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
WithFields(Fields{"goroutine": n}).Info("concurrent log")
}(i)
}
// Just verify no panic occurs
wg.Wait()
}
func TestLogger_DefaultFields(t *testing.T) {
// Create a buffer to capture output
var buf bytes.Buffer
// Initialize logger with test configuration
Init(&LogConfig{
Level: "info",
Format: "json",
EnableCaller: true,
})
// Replace the output writer with our buffer
log.SetOutput(&buf)
// Log a test message
Info("test default fields")
// Parse the JSON output
var data map[string]interface{}
err := json.Unmarshal(buf.Bytes(), &data)
require.NoError(t, err, "Failed to unmarshal log output")
// Check that default fields are included
assert.Equal(t, "starter-kit", data["app_name"], "app_name should be set in default fields")
// The env field might be empty in test environment, which is fine
// as long as the field exists in the log entry
_, envExists := data["env"]
assert.True(t, envExists, "env field should exist in log entry")
}
func TestLogger_LevelChanges(t *testing.T) {
output := captureOutput(func() {
Init(&LogConfig{Level: "error", Format: "json"})
Debug("should not appear")
SetLevel("debug")
Debug("should appear")
})
// Split output into lines
lines := strings.Split(strings.TrimSpace(output), "\n")
// Should only have one log message (the second Debug)
assert.Len(t, lines, 1)
assert.Contains(t, lines[0], "should appear")
}

View File

@ -0,0 +1,142 @@
// Package lifecycle provides application lifecycle management using tomb
package lifecycle
import (
"context"
"os"
"os/signal"
"sync"
"syscall"
"time"
"go.uber.org/multierr"
"gopkg.in/tomb.v2"
"starter-kit/internal/helper/logger"
)
// Lifecycle manages the application lifecycle
type Lifecycle struct {
tomb tomb.Tomb
shutdownTimeout time.Duration
mu sync.Mutex
services []Service
}
// Service represents a service that can be started and stopped
type Service interface {
Name() string
Start() error
Shutdown(ctx context.Context) error
}
// New creates a new Lifecycle with the given shutdown timeout
func New(shutdownTimeout time.Duration) *Lifecycle {
return &Lifecycle{
shutdownTimeout: shutdownTimeout,
services: make([]Service, 0),
}
}
// Register adds a service to the lifecycle manager
func (l *Lifecycle) Register(service Service) {
l.mu.Lock()
defer l.mu.Unlock()
l.services = append(l.services, service)
}
// Start starts all registered services
func (l *Lifecycle) Start() error {
l.mu.Lock()
defer l.mu.Unlock()
// Start all services
for _, svc := range l.services {
service := svc // Create a new variable for the closure
l.tomb.Go(func() error {
logger.Info("Starting service: ", service.Name())
if err := service.Start(); err != nil {
logger.Errorf("Service %s failed to start: %v", service.Name(), err)
return err
}
logger.Info("Service started: ", service.Name())
return nil
})
}
return nil
}
// Wait blocks until all services have stopped
func (l *Lifecycle) Wait() error {
return l.tomb.Wait()
}
// Shutdown gracefully shuts down all services
func (l *Lifecycle) Shutdown() error {
l.mu.Lock()
defer l.mu.Unlock()
logger.Info("Initiating graceful shutdown...")
// Create a context with timeout
ctx, cancel := context.WithTimeout(context.Background(), l.shutdownTimeout*time.Second)
defer cancel()
// Notify all services to shut down
l.tomb.Kill(nil)
// Shutdown all services in reverse order
var errs error
for i := len(l.services) - 1; i >= 0; i-- {
svc := l.services[i]
logger.Infof("Shutting down service: %s", svc.Name())
if err := svc.Shutdown(ctx); err != nil {
logger.Errorf("Error shutting down %s: %v", svc.Name(), err)
errs = multierr.Append(errs, err)
}
}
// Wait for all services to stop or timeout
errChan := make(chan error, 1)
go func() {
errChan <- l.tomb.Wait()
}()
select {
case err := <-errChan:
if err != nil {
errs = multierr.Append(errs, err)
}
case <-ctx.Done():
errs = multierr.Append(errs, ctx.Err())
}
if errs != nil {
logger.Errorf("Shutdown completed with errors: %v", errs)
} else {
logger.Info("Shutdown completed successfully")
}
return errs
}
// ShutdownOnSignal shuts down the lifecycle when a signal is received
func (l *Lifecycle) ShutdownOnSignal(signals ...os.Signal) {
if len(signals) == 0 {
signals = []os.Signal{syscall.SIGINT, syscall.SIGTERM}
}
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, signals...)
l.tomb.Go(func() error {
select {
case sig := <-sigChan:
logger.Infof("Received signal: %v. Initiating shutdown...", sig)
return l.Shutdown()
case <-l.tomb.Dying():
return nil
}
})
}

View File

@ -0,0 +1,70 @@
package handler
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"starter-kit/internal/helper/config"
)
// HealthHandler xử lý các endpoint liên quan đến sức khỏe hệ thống
type HealthHandler struct {
config *config.Config
startTime time.Time
appVersion string
}
// NewHealthHandler tạo một handler mới cho health endpoints
func NewHealthHandler(cfg *config.Config) *HealthHandler {
return &HealthHandler{
config: cfg,
startTime: time.Now(),
appVersion: cfg.App.Version,
}
}
// HealthCheck trả về trạng thái sức khỏe của hệ thống
// @Summary Kiểm tra trạng thái của hệ thống
// @Description Trả về thông tin về trạng thái của ứng dụng và các thành phần
// @Tags Health
// @Produce json
// @Success 200 {object} map[string]interface{}
// @Router /health [get]
func (h *HealthHandler) HealthCheck(c *gin.Context) {
uptime := time.Since(h.startTime).String()
// Tạo response
resp := gin.H{
"status": "ok",
"app": gin.H{
"name": h.config.App.Name,
"version": h.appVersion,
"env": h.config.App.Environment,
},
"uptime": uptime,
// Trong môi trường thực tế, thêm kiểm tra trạng thái của database và các dịch vụ bên ngoài
"components": gin.H{
"database": "ok", // Giả định - trong thực tế sẽ kiểm tra kết nối
"cache": "ok", // Giả định
},
"timestamp": time.Now().Format(time.RFC3339),
}
c.JSON(http.StatusOK, resp)
}
// Ping endpoint đơn giản để kiểm tra server đang chạy
// @Summary Ping server
// @Description Endpoint đơn giản để kiểm tra server đang hoạt động
// @Tags Health
// @Produce json
// @Success 200 {object} map[string]string
// @Router /ping [get]
func (h *HealthHandler) Ping(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "ok",
"message": "pong",
"timestamp": time.Now().Format(time.RFC3339),
})
}

View File

@ -0,0 +1,35 @@
package middleware
import (
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"starter-kit/internal/helper/logger"
)
// Logger middleware for logging HTTP requests
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
// Generate request ID
requestID := uuid.New().String()
c.Set("RequestID", requestID)
// Start timer
start := time.Now()
// Process request
c.Next()
// Log request details
logger.WithFields(logger.Fields{
"request_id": requestID,
"method": c.Request.Method,
"path": c.Request.URL.Path,
"status": c.Writer.Status(),
"latency": time.Since(start).String(),
"client_ip": c.ClientIP(),
"user_agent": c.Request.UserAgent(),
}).Info("HTTP Request")
}
}

View File

@ -0,0 +1,49 @@
package http
import (
"github.com/gin-gonic/gin"
"starter-kit/internal/helper/config"
"starter-kit/internal/transport/http/handler"
"starter-kit/internal/transport/http/middleware"
)
// SetupRouter cấu hình router cho HTTP server
func SetupRouter(cfg *config.Config) *gin.Engine {
// Khởi tạo router với mode phù hợp với môi trường
if cfg.App.Environment == "production" {
gin.SetMode(gin.ReleaseMode)
}
router := gin.New()
// Logger middleware
router.Use(middleware.Logger())
// Recovery middleware
router.Use(gin.Recovery())
// CORS middleware nếu cần
// router.Use(middleware.CORS())
// Khởi tạo các handlers
healthHandler := handler.NewHealthHandler(cfg)
// Đăng ký các routes
// Health check routes
router.GET("/ping", healthHandler.Ping)
router.GET("/health", healthHandler.HealthCheck)
// API versioning - Cảnh báo: API routes hiện đang được comment out
// Khi cần sử dụng, bỏ comment đoạn code sau
/*
v1 := router.Group("/api/v1")
{
// Các API endpoints version 1
// v1.GET("/resources", resourceHandler.List)
// v1.POST("/resources", resourceHandler.Create)
}
*/
return router
}

View File

@ -0,0 +1,102 @@
package http
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"time"
"github.com/gin-gonic/gin"
"starter-kit/internal/helper/config"
"starter-kit/internal/helper/logger"
)
// ErrServerClosed is returned by the Server's Start method after a call to Shutdown
var ErrServerClosed = errors.New("http: Server closed")
// Server represents the HTTP server
type Server struct {
server *http.Server
config *config.Config
router *gin.Engine
listener net.Listener
serverErr chan error
}
// NewServer creates a new HTTP server with the given configuration
func NewServer(cfg *config.Config) *Server {
// Create a new Gin router
router := SetupRouter(cfg)
// Create the HTTP server
server := &http.Server{
Addr: fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port),
Handler: router,
ReadTimeout: time.Duration(cfg.Server.ReadTimeout) * time.Second,
WriteTimeout: time.Duration(cfg.Server.WriteTimeout) * time.Second,
}
return &Server{
server: server,
config: cfg,
router: router,
serverErr: make(chan error, 1),
}
}
// Start starts the HTTP server
func (s *Server) Start() error {
// Create a listener
listener, err := net.Listen("tcp", s.server.Addr)
if err != nil {
return fmt.Errorf("failed to create listener: %w", err)
}
s.listener = listener
// Log server start
logger.WithFields(logger.Fields{
"address": s.server.Addr,
}).Info("Starting HTTP server")
// Start the server in a goroutine
go func() {
s.serverErr <- s.server.Serve(s.listener)
close(s.serverErr)
}()
// Check if server started successfully
select {
case err := <-s.serverErr:
return fmt.Errorf("server failed to start: %w", err)
case <-time.After(100 * time.Millisecond):
logger.Info("HTTP server started successfully")
return nil
}
}
// Shutdown gracefully shuts down the server
func (s *Server) Shutdown(ctx context.Context) error {
logger.Info("Shutting down HTTP server...")
// Try to gracefully shutdown
err := s.server.Shutdown(ctx)
if err != nil {
logger.WithError(err).Error("Error during server shutdown")
return err
}
logger.Info("HTTP server stopped")
return nil
}
// GetRouter returns the underlying router
func (s *Server) GetRouter() *gin.Engine {
return s.router
}
// GetConfig returns the server configuration
func (s *Server) GetConfig() *config.Config {
return s.config
}

29
templates/.env.example Normal file
View File

@ -0,0 +1,29 @@
# App Configuration
APP_APP_NAME="ULFlow Starter Kit"
APP_APP_VERSION=0.1.0
APP_APP_ENVIRONMENT=development
APP_APP_TIMEZONE=Asia/Ho_Chi_Minh
# Logger Configuration
APP_LOGGER_LEVEL=info # debug, info, warn, error
# Server Configuration
APP_SERVER_HOST=0.0.0.0
APP_SERVER_PORT=8080
APP_SERVER_READ_TIMEOUT=15
APP_SERVER_WRITE_TIMEOUT=15
APP_SERVER_SHUTDOWN_TIMEOUT=30
APP_SERVER_ALLOW_ORIGINS=*
# Database Configuration
APP_DATABASE_DRIVER=postgres
APP_DATABASE_HOST=localhost
APP_DATABASE_PORT=5432
APP_DATABASE_USERNAME=postgres
APP_DATABASE_PASSWORD=postgres
APP_DATABASE_DATABASE=ulflow
APP_DATABASE_SSL_MODE=disable
APP_DATABASE_MAX_OPEN_CONNS=25
APP_DATABASE_MAX_IDLE_CONNS=5
APP_DATABASE_CONN_MAX_LIFETIME=300
APP_DATABASE_MIGRATION_PATH=migrations

View File

@ -0,0 +1,31 @@
app:
name: "ULFlow Starter Kit"
version: "0.1.0"
environment: "development"
timezone: "Asia/Ho_Chi_Minh"
logger:
level: "info" # debug, info, warn, error
server:
host: "0.0.0.0"
port: 8080
read_timeout: 15
write_timeout: 15
shutdown_timeout: 30
trusted_proxies: []
allow_origins:
- "*"
database:
driver: "postgres"
host: "localhost"
port: 5432
username: "postgres"
password: "postgres"
database: "ulflow"
ssl_mode: "disable"
max_open_conns: 25
max_idle_conns: 5
conn_max_lifetime: 300
migration_path: "migrations"

55
tomb.yaml Normal file
View File

@ -0,0 +1,55 @@
# Tomb Configuration for ULFlow Starter Kit
# This file configures how our application handles signals and graceful shutdown
# Signal configuration
signals:
# Signal to use for graceful shutdown
graceful: "SIGTERM"
# Signal to use for forceful shutdown
force: "SIGKILL"
# Timeout for graceful shutdown before force shutdown
timeout: "10s"
# Hooks to execute during shutdown process
hooks:
# Pre-shutdown hooks run before beginning shutdown
pre_shutdown:
- name: "logger"
command: "go"
args: ["run", "./scripts/shutdown_logger.go", "pre"]
timeout: "2s"
async: true
# Shutdown hooks run during shutdown
shutdown:
- name: "close-http-server"
timeout: "5s"
async: false
# Post-shutdown hooks run after shutdown completes
post_shutdown:
- name: "cleanup"
command: "go"
args: ["run", "./scripts/shutdown_logger.go", "post"]
timeout: "2s"
async: true
# Resources to manage during shutdown
resources:
- name: "http-server"
type: "http"
grace_period: "5s"
- name: "database-connections"
type: "sql"
grace_period: "3s"
- name: "background-workers"
type: "worker"
grace_period: "8s"
# Monitoring configuration
monitoring:
enabled: true
interval: "1s"
threshold: 5 # number of consecutive failures before alerting