chore(init): Create Initial repo
Upload First repo
This commit is contained in:
commit
86cff0489e
58
.air.toml
Normal file
58
.air.toml
Normal 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
5
.feature-flags.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"enable_database": {
|
||||||
|
"enabled": false
|
||||||
|
}
|
||||||
|
}
|
||||||
14
.gitea/commit-template.txt
Normal file
14
.gitea/commit-template.txt
Normal 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
48
.gitea/hooks/pre-commit
Normal 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
|
||||||
34
.gitea/hooks/prepare-commit-msg
Normal file
34
.gitea/hooks/prepare-commit-msg
Normal 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
126
.gitea/workflows/ci.yml
Normal 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
135
.gitea/workflows/docker.yml
Normal 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
50
.gitignore
vendored
Normal 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
58
Dockerfile
Normal 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
32
Dockerfile.local
Normal 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
21
LICENSE
Normal 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
171
Makefile
Normal 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
192
Readme.md
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
# ULFlow Golang Starter Kit
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
[](https://go.dev/)
|
||||||
|
[](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
149
cmd/app/main.go
Normal 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
31
configs/config.yaml
Normal 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
83
docker-compose.prod.yml
Normal 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
85
docker-compose.yml
Normal 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
1
docs/.obsidian/app.json
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
1
docs/.obsidian/appearance.json
vendored
Normal file
1
docs/.obsidian/appearance.json
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
31
docs/.obsidian/core-plugins.json
vendored
Normal file
31
docs/.obsidian/core-plugins.json
vendored
Normal 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
184
docs/.obsidian/workspace.json
vendored
Normal 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
149
docs/adapter.md
Normal 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
234
docs/architecture.md
Normal 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
63
docs/changelog.md
Normal 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
79
docs/general.md
Normal 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
48
docs/roadmap.md
Normal 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
136
docs/secrets.md
Normal 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` và `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
130
docs/spec.md
Normal 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
119
docs/testing.md
Normal 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
57
docs/ux.md
Normal 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
84
docs/workflow.md
Normal 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
72
go.mod
Normal 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
594
go.sum
Normal 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=
|
||||||
117
internal/helper/config/load.go
Normal file
117
internal/helper/config/load.go
Normal 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)
|
||||||
|
}
|
||||||
48
internal/helper/config/types.go
Normal file
48
internal/helper/config/types.go
Normal 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"`
|
||||||
|
}
|
||||||
154
internal/helper/database/database.go
Normal file
154
internal/helper/database/database.go
Normal 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)
|
||||||
|
}
|
||||||
75
internal/helper/feature/feature.go
Normal file
75
internal/helper/feature/feature.go
Normal 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
|
||||||
|
}
|
||||||
118
internal/helper/logger/README.md
Normal file
118
internal/helper/logger/README.md
Normal 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)
|
||||||
291
internal/helper/logger/logger.go
Normal file
291
internal/helper/logger/logger.go
Normal 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
249
internal/helper/logger/logger_test.go
Normal file
249
internal/helper/logger/logger_test.go
Normal 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")
|
||||||
|
}
|
||||||
142
internal/pkg/lifecycle/lifecycle.go
Normal file
142
internal/pkg/lifecycle/lifecycle.go
Normal 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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
70
internal/transport/http/handler/health_handler.go
Normal file
70
internal/transport/http/handler/health_handler.go
Normal 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),
|
||||||
|
})
|
||||||
|
}
|
||||||
35
internal/transport/http/middleware/logger.go
Normal file
35
internal/transport/http/middleware/logger.go
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
49
internal/transport/http/router.go
Normal file
49
internal/transport/http/router.go
Normal 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
|
||||||
|
}
|
||||||
102
internal/transport/http/server.go
Normal file
102
internal/transport/http/server.go
Normal 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
29
templates/.env.example
Normal 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
|
||||||
31
templates/config.example.yaml
Normal file
31
templates/config.example.yaml
Normal 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
55
tomb.yaml
Normal 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
|
||||||
Loading…
x
Reference in New Issue
Block a user