🐛 fix: emit all config-loading logs in correct JSON format from the start (#16)
## Summary Closes #15 When `logging.json: true` (or `DLC_LOGGING_JSON=true`), the logger was unconditionally initialised to console/text format at the top of `LoadConfig()`, so early log lines — most visibly **"Config file loaded"** — were always written as human-readable text regardless of configuration. ## Root cause Classic chicken-and-egg: the format flag lives inside the config that is being loaded. The format-switch block only ran *after* `v.Unmarshal()`, too late for the config-file log. ## Changes ### `pkg/config/config.go` - Add `peekJSONLogging()`: resolves the JSON flag **before** any log is emitted by (1) checking `DLC_LOGGING_JSON` directly via `os.Getenv`, then (2) doing a minimal throwaway Viper pre-read of the config file for the `logging.json` key. This mirrors Viper's own priority order without parsing the full config twice. - Apply the resolved format immediately and emit **"Logging configured"** as the very first log line. - Remove the now-redundant format-switch block that ran after `Unmarshal()`. ### `scripts/start-server.sh`, `test-graceful-shutdown.sh`, `test-opentelemetry.sh` - Replace hardcoded `PROJECT_DIR` path with a dynamic `SCRIPTS_DIR=$(dirname $(realpath ${BASH_SOURCE[0]}))` derivation so scripts work from any worktree or clone location. ## Test plan - [x] `go test ./pkg/...` — all pass - [x] `scripts/test-graceful-shutdown.sh` — all JSON valid, all startup logs present - [x] Manual smoke test: first line is `{"level":"info",...,"message":"Logging configured"}`, every line is valid JSON Reviewed-on: #16 Co-authored-by: Gabriel Radureau <arcodange@gmail.com> Co-committed-by: Gabriel Radureau <arcodange@gmail.com>
This commit was merged in pull request #16.
This commit is contained in:
@@ -203,6 +203,31 @@ cmd_wait_job() {
|
||||
}
|
||||
|
||||
# Comment on PR
|
||||
# Create a pull request
|
||||
cmd_create_pr() {
|
||||
local owner="$1"
|
||||
local repo="$2"
|
||||
local title="$3"
|
||||
local body="$4"
|
||||
local head="$5"
|
||||
local base="${6:-main}"
|
||||
|
||||
if [[ -z "$owner" || -z "$repo" || -z "$title" || -z "$head" ]]; then
|
||||
echo "Usage: $0 create-pr <owner> <repo> <title> <body> <head_branch> [base_branch]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local endpoint="/repos/${owner}/${repo}/pulls"
|
||||
local data
|
||||
data=$(jq -n \
|
||||
--arg title "$title" \
|
||||
--arg body "$body" \
|
||||
--arg head "$head" \
|
||||
--arg base "$base" \
|
||||
'{title: $title, body: $body, head: $head, base: $base}')
|
||||
api_request "POST" "$endpoint" "$data"
|
||||
}
|
||||
|
||||
cmd_comment_pr() {
|
||||
local owner="$1"
|
||||
local repo="$2"
|
||||
@@ -215,7 +240,8 @@ cmd_comment_pr() {
|
||||
fi
|
||||
|
||||
local endpoint="/repos/${owner}/${repo}/issues/${pr_number}/comments"
|
||||
local data="{\"body\": \"${comment}\"}"
|
||||
local data
|
||||
data=$(jq -n --arg body "$comment" '{body: $body}')
|
||||
api_request "POST" "$endpoint" "$data"
|
||||
}
|
||||
|
||||
@@ -250,6 +276,7 @@ main() {
|
||||
monitor-workflow) cmd_monitor_workflow "$@" ;;
|
||||
diagnose-job) cmd_diagnose_job "$@" ;;
|
||||
recent-workflows) cmd_recent_workflows "$@" ;;
|
||||
create-pr) cmd_create_pr "$@" ;;
|
||||
comment-pr) cmd_comment_pr "$@" ;;
|
||||
pr-status) cmd_pr_status "$@" ;;
|
||||
list-issues) cmd_list_issues "$@" ;;
|
||||
@@ -274,6 +301,7 @@ main() {
|
||||
echo " monitor-workflow <owner> <repo> <workflow_run_id> [interval_seconds]" >&2
|
||||
echo " diagnose-job <owner> <repo> <job_id>" >&2
|
||||
echo " recent-workflows <owner> <repo> [limit] [status_filter]" >&2
|
||||
echo " create-pr <owner> <repo> <title> <body> <head_branch> [base_branch]" >&2
|
||||
echo " comment-pr <owner> <repo> <pr_number> <comment>" >&2
|
||||
echo " pr-status <owner> <repo> <pr_number>" >&2
|
||||
echo " list-issues <owner> <repo> [state]" >&2
|
||||
|
||||
429
README.md
429
README.md
@@ -1,421 +1,98 @@
|
||||
# dance-lessons-coach
|
||||
|
||||
[](https://gitea.arcodange.fr/arcodange/dance-lessons-coach/actions/workflows/ci-cd.yaml/badge.svg)
|
||||
[](https://goreportcard.com/report/github.com/arcodange/dance-lessons-coach)
|
||||
[](https://gitea.arcodange.fr/arcodange/dance-lessons-coach/actions/workflows/ci-cd.yaml)
|
||||
[](https://gitea.arcodange.fr/arcodange/dance-lessons-coach/releases)
|
||||
[](LICENSE)
|
||||
[](https://gitea.arcodange.lab/arcodange/dance-lessons-coach)
|
||||
[](https://gitea.arcodange.lab/arcodange/dance-lessons-coach)
|
||||
|
||||
A Go project demonstrating idiomatic package structure, CLI implementation, and JSON API with Chi router.
|
||||
=======
|
||||
Go web service demonstrating idiomatic package structure, versioned JSON API, and production-ready features.
|
||||
|
||||
## Features
|
||||
|
||||
- Greet function with default behavior
|
||||
- Command-line interface
|
||||
- JSON API with versioned endpoints
|
||||
- Chi router integration
|
||||
- Zerolog for high-performance logging
|
||||
- Viper for configuration management
|
||||
- Graceful shutdown with context
|
||||
- Readiness endpoint for Kubernetes/service mesh integration
|
||||
- OpenTelemetry integration with Jaeger support
|
||||
- OpenAPI/Swagger documentation
|
||||
- Unit tests
|
||||
- Go 1.26.1 compatible
|
||||
- Versioned JSON API (`/api/v1`, `/api/v2`)
|
||||
- Chi router with graceful shutdown
|
||||
- Zerolog structured logging (console and JSON modes)
|
||||
- Viper configuration (file + env vars)
|
||||
- Readiness endpoint for Kubernetes / service mesh
|
||||
- OpenTelemetry / Jaeger distributed tracing
|
||||
- OpenAPI / Swagger UI (embedded in binary)
|
||||
- PostgreSQL user service with JWT auth
|
||||
- BDD + unit tests
|
||||
|
||||
## Installation
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://gitea.arcodange.lab/arcodange/dance-lessons-coach.git
|
||||
cd dance-lessons-coach
|
||||
|
||||
# Build all binaries
|
||||
./scripts/build.sh
|
||||
|
||||
# Use the new Cobra CLI
|
||||
./bin/dance-lessons-coach --help
|
||||
|
||||
# Or use the legacy greet CLI
|
||||
go run ./cmd/greet
|
||||
./scripts/build.sh # produces ./bin/server and ./bin/greet
|
||||
./scripts/start-server.sh start
|
||||
```
|
||||
|
||||
## CI/CD Pipeline
|
||||
|
||||
dance-lessons-coach features an optimized CI/CD pipeline using GitHub Actions with container/services architecture:
|
||||
|
||||
### Key Features
|
||||
- ✅ **Container-based execution**: All steps run in pre-built Docker cache images
|
||||
- ✅ **Service-based PostgreSQL**: Automatic database service provisioning
|
||||
- ✅ **Smart caching**: Dependency-aware cache invalidation
|
||||
- ✅ **Multi-platform**: Compatible with Gitea, GitHub, and GitLab
|
||||
- ✅ **Fast execution**: No Docker Compose overhead
|
||||
- ✅ **Reliable testing**: Full database connectivity with proper environment setup
|
||||
|
||||
### Architecture
|
||||
|
||||
The pipeline uses GitHub Actions' native `container` and `services` directives instead of Docker Compose:
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
ci-pipeline:
|
||||
container:
|
||||
image: gitea.arcodange.lab/arcodange/dance-lessons-coach-build-cache:${{ needs.build-cache.outputs.deps_hash }}
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: dance_lessons_coach_bdd_test
|
||||
```
|
||||
|
||||
### Benefits
|
||||
|
||||
1. **Performance**: Direct container execution without compose overhead
|
||||
2. **Reliability**: Service containers managed by GitHub Actions
|
||||
3. **Simplicity**: Cleaner workflow definition
|
||||
4. **Portability**: Works across CI platforms
|
||||
5. **Caching**: Intelligent dependency-based cache rebuilding
|
||||
|
||||
### Workflow Steps
|
||||
|
||||
1. **Build Cache**: Creates Docker image with Go tools and dependencies
|
||||
2. **CI Pipeline**: Runs tests, builds binaries, and generates documentation
|
||||
3. **Database Tests**: Connects to PostgreSQL service container
|
||||
4. **Coverage Reporting**: Updates coverage badges automatically
|
||||
5. **Artifact Publishing**: Builds and pushes Docker images (main branch only)
|
||||
|
||||
### Environment Configuration
|
||||
|
||||
The pipeline automatically sets up database environment variables:
|
||||
|
||||
```bash
|
||||
echo "DLC_DATABASE_HOST=postgres" >> $GITHUB_ENV
|
||||
echo "DLC_DATABASE_PORT=5432" >> $GITHUB_ENV
|
||||
echo "DLC_DATABASE_USER=postgres" >> $GITHUB_ENV
|
||||
echo "DLC_DATABASE_PASSWORD=postgres" >> $GITHUB_ENV
|
||||
echo "DLC_DATABASE_NAME=dance_lessons_coach_bdd_test" >> $GITHUB_ENV
|
||||
echo "DLC_DATABASE_SSL_MODE=disable" >> $GITHUB_ENV
|
||||
curl http://localhost:8080/api/health
|
||||
curl http://localhost:8080/api/v1/greet/Alice
|
||||
```
|
||||
|
||||
### Status
|
||||
Stop: `./scripts/start-server.sh stop`
|
||||
|
||||
[](https://gitea.arcodange.fr/arcodange/dance-lessons-coach)
|
||||
## Greet CLI
|
||||
|
||||
=======
|
||||
- ✅ **Linting**: Code quality checks with `go fmt` and `go vet`
|
||||
- ✅ **Version Management**: Automatic version detection
|
||||
- ✅ **Portable**: Uses standard GitHub Actions workflow format
|
||||
|
||||
### Workflow File
|
||||
```yaml
|
||||
# .github/workflows/main.yml
|
||||
jobs:
|
||||
build-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.26.1'
|
||||
- run: go build ./...
|
||||
- run: go test ./... -cover
|
||||
|
||||
lint-format:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: go fmt ./...
|
||||
- run: go vet ./...
|
||||
```bash
|
||||
go run ./cmd/greet # Hello world!
|
||||
go run ./cmd/greet Alice # Hello Alice!
|
||||
```
|
||||
|
||||
### Setup Instructions
|
||||
1. **Gitea**: Enable GitHub Actions compatibility in repo settings
|
||||
2. **GitHub**: Push to mirror repository (workflow runs automatically)
|
||||
3. **GitLab**: Convert workflow to `.gitlab-ci.yml` or use compatibility mode
|
||||
|
||||
**See [ADR 0016](adr/0016-ci-cd-pipeline-design.md) for complete CI/CD design and [STATUS_BADGES.md](STATUS_BADGES.md) for badge setup.**
|
||||
|
||||
## Configuration
|
||||
|
||||
Basic configuration options:
|
||||
All options are available via `config.yaml` or `DLC_*` environment variables.
|
||||
|
||||
```bash
|
||||
# Start with default configuration
|
||||
./scripts/start-server.sh start
|
||||
| Env var | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
| `DLC_SERVER_PORT` | `8080` | Listening port |
|
||||
| `DLC_SERVER_HOST` | `0.0.0.0` | Bind address |
|
||||
| `DLC_LOGGING_JSON` | `false` | JSON log format |
|
||||
| `DLC_LOGGING_OUTPUT` | stderr | Log file path |
|
||||
| `DLC_SHUTDOWN_TIMEOUT` | `30s` | Graceful shutdown window |
|
||||
| `DLC_API_V2_ENABLED` | `false` | Enable `/api/v2` routes |
|
||||
| `DLC_CONFIG_FILE` | `./config.yaml` | Override config path |
|
||||
|
||||
# Custom port
|
||||
export DLC_SERVER_PORT=9090
|
||||
./scripts/start-server.sh start
|
||||
See `config.example.yaml` for a full template.
|
||||
|
||||
# JSON logging
|
||||
export DLC_LOGGING_JSON=true
|
||||
./scripts/start-server.sh start
|
||||
```
|
||||
## API
|
||||
|
||||
**See [AGENTS.md](AGENTS.md#configuration-management) for comprehensive configuration guide including:**
|
||||
- File-based configuration
|
||||
- Environment variables
|
||||
- Configuration priority rules
|
||||
- OpenTelemetry setup
|
||||
- Advanced scenarios
|
||||
|
||||
## Usage
|
||||
|
||||
### New Cobra CLI (Recommended)
|
||||
|
||||
```bash
|
||||
# Show help
|
||||
./bin/dance-lessons-coach --help
|
||||
|
||||
# Show version
|
||||
./bin/dance-lessons-coach version
|
||||
|
||||
# Greet someone
|
||||
./bin/dance-lessons-coach greet John
|
||||
|
||||
# Start server
|
||||
./bin/dance-lessons-coach server
|
||||
```
|
||||
|
||||
### Legacy CLI (Deprecated)
|
||||
|
||||
```bash
|
||||
# Default greeting
|
||||
go run ./cmd/greet
|
||||
# Output: Hello world!
|
||||
|
||||
# Custom greeting
|
||||
go run ./cmd/greet John
|
||||
# Output: Hello John!
|
||||
```
|
||||
|
||||
### Web Server
|
||||
|
||||
**Using the server control script (recommended):**
|
||||
|
||||
```bash
|
||||
# Start the server
|
||||
./scripts/start-server.sh start
|
||||
|
||||
# Test API endpoints
|
||||
./scripts/start-server.sh test
|
||||
|
||||
# Access OpenAPI documentation
|
||||
# Swagger UI: http://localhost:8080/swagger/
|
||||
# OpenAPI spec: http://localhost:8080/swagger/doc.json
|
||||
|
||||
# Stop the server
|
||||
./scripts/start-server.sh stop
|
||||
```
|
||||
|
||||
**Manual server management:**
|
||||
|
||||
```bash
|
||||
# Start the server
|
||||
go run ./cmd/server
|
||||
|
||||
# Test API endpoints
|
||||
curl http://localhost:8080/api/health
|
||||
# Output: {"status":"healthy"}
|
||||
|
||||
curl http://localhost:8080/api/ready
|
||||
# Output: {"ready":true}
|
||||
|
||||
curl http://localhost:8080/api/v1/greet
|
||||
# Output: {"message":"Hello world!"}
|
||||
|
||||
curl http://localhost:8080/api/v1/greet/John
|
||||
# Output: {"message":"Hello John!"}
|
||||
```
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/health` | Liveness check |
|
||||
| GET | `/api/ready` | Readiness check (503 during shutdown) |
|
||||
| GET | `/api/version` | Version info (`?format=plain\|full\|json`) |
|
||||
| GET | `/api/v1/greet/` | Default greeting |
|
||||
| GET | `/api/v1/greet/{name}` | Named greeting |
|
||||
| POST | `/api/v2/greet` | V2 greeting with validation |
|
||||
| GET | `/swagger/` | Swagger UI |
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
go test ./...
|
||||
|
||||
# Run specific package tests
|
||||
go test ./pkg/greet/
|
||||
go test ./... # unit + integration tests
|
||||
./scripts/test-graceful-shutdown.sh # lifecycle + JSON logging validation
|
||||
./scripts/test-opentelemetry.sh # tracing end-to-end
|
||||
```
|
||||
|
||||
## CI/CD
|
||||
## Gitea Client
|
||||
|
||||
dance-lessons-coach includes a comprehensive CI/CD pipeline with multiple testing options:
|
||||
AI agent helper script at `.vibe/skills/gitea-client/scripts/gitea-client.sh`.
|
||||
|
||||
### Local Testing (No Gitea Required)
|
||||
Auth setup:
|
||||
```bash
|
||||
# Validate workflow structure
|
||||
./scripts/cicd.sh validate
|
||||
|
||||
# Test workflow steps locally
|
||||
./scripts/cicd.sh test-simple
|
||||
echo "your_token" > ~/.gitea_token
|
||||
chmod 600 ~/.gitea_token
|
||||
export GITEA_API_TOKEN_FILE="$HOME/.gitea_token"
|
||||
```
|
||||
|
||||
### Gitea Integration
|
||||
```bash
|
||||
# Test local setup with Gitea configuration
|
||||
./scripts/cicd.sh test-local
|
||||
|
||||
# Check pipeline status on Gitea
|
||||
./scripts/cicd.sh check-status
|
||||
```
|
||||
|
||||
### Full CI/CD Testing
|
||||
```bash
|
||||
# Test with docker compose (requires Gitea runner)
|
||||
./scripts/cicd.sh test-docker
|
||||
```
|
||||
|
||||
**See [adr/0016-ci-cd-pipeline-design.md](adr/0016-ci-cd-pipeline-design.md) for complete CI/CD architecture.**
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
dance-lessons-coach/
|
||||
├── adr/ # Architecture Decision Records
|
||||
├── cmd/ # Entry points (greet CLI, server)
|
||||
├── pkg/ # Core packages (config, greet, server, telemetry)
|
||||
│ └── server/docs/ # Generated OpenAPI documentation (gitignored)
|
||||
├── config.yaml # Configuration file
|
||||
├── scripts/ # Management scripts
|
||||
└── go.mod # Go module definition
|
||||
```
|
||||
|
||||
**See [AGENTS.md](AGENTS.md#project-structure) for detailed structure and component explanations.**
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Generate OpenAPI Documentation
|
||||
|
||||
The project uses [swaggo/swag](https://github.com/swaggo/swag) to generate OpenAPI/Swagger documentation from code annotations:
|
||||
|
||||
```bash
|
||||
# Generate documentation
|
||||
go generate ./pkg/server/
|
||||
|
||||
# This creates:
|
||||
# - pkg/server/docs/docs.go (swagger template)
|
||||
# - pkg/server/docs/swagger.json (OpenAPI spec)
|
||||
# - pkg/server/docs/swagger.yaml (YAML version)
|
||||
```
|
||||
|
||||
**Note:** `pkg/server/docs/` is gitignored. Documentation is embedded in the binary at build time.
|
||||
|
||||
### Documentation Annotations
|
||||
|
||||
Add swagger annotations to handlers and models:
|
||||
|
||||
```go
|
||||
// @Summary Get personalized greeting
|
||||
// @Description Returns a greeting with the specified name
|
||||
// @Tags greet
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param name path string true "Name to greet"
|
||||
// @Success 200 {object} GreetResponse "Successful response"
|
||||
// @Failure 400 {object} ErrorResponse "Invalid name parameter"
|
||||
// @Router /v1/greet/{name} [get]
|
||||
func (h *apiV1GreetHandler) handleGreetPath(w http.ResponseWriter, r *http.Request) {
|
||||
// handler implementation
|
||||
}
|
||||
```
|
||||
Get a token at https://gitea.arcodange.lab → Profile → Settings → Applications.
|
||||
|
||||
## Architecture
|
||||
|
||||
This project uses Architecture Decision Records (ADRs) to document key technical choices. See [adr/](adr/) for complete documentation including decisions on Go 1.26.1, Chi router, Zerolog, OpenTelemetry, interface-based design, graceful shutdown, configuration management, testing strategies, and OpenAPI documentation.
|
||||
|
||||
**Adding new decisions?** See [adr/README.md](adr/README.md) for guidelines.
|
||||
|
||||
## Gitea Integration
|
||||
|
||||
dance-lessons-coach includes AI agent skills for Gitea integration to monitor CI/CD jobs and interact with pull requests.
|
||||
|
||||
### Gitea Client Skill Setup
|
||||
|
||||
The Gitea client skill enables AI agents to:
|
||||
- Monitor CI/CD job status
|
||||
- Fetch job logs for debugging
|
||||
- Comment on pull requests
|
||||
- Track PR status
|
||||
|
||||
**Setup Instructions:**
|
||||
|
||||
1. **Create a Personal Access Token:**
|
||||
- Log in to https://gitea.arcodange.lab
|
||||
- Go to Profile → Settings → Applications
|
||||
- Generate token with `read:repository`, `write:repository`, and `read:user` scopes
|
||||
|
||||
2. **Configure Authentication:**
|
||||
```bash
|
||||
# Option 1: Environment variable
|
||||
export GITEA_API_TOKEN="your_token"
|
||||
|
||||
# Option 2: Token file (recommended)
|
||||
echo "your_token" > ~/.gitea_token
|
||||
chmod 600 ~/.gitea_token
|
||||
export GITEA_API_TOKEN_FILE="$HOME/.gitea_token"
|
||||
```
|
||||
|
||||
3. **Add to shell configuration:**
|
||||
```bash
|
||||
echo 'export GITEA_API_TOKEN_FILE="$HOME/.gitea_token"' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
```
|
||||
|
||||
**Usage Examples:**
|
||||
```bash
|
||||
# List recent jobs
|
||||
.vibe/skills/gitea-client/scripts/gitea-client.sh list-jobs owner repo workflow_id 5
|
||||
|
||||
# Wait for job completion
|
||||
.vibe/skills/gitea-client/scripts/gitea-client.sh wait-job owner repo job_id 300
|
||||
|
||||
# Comment on PR
|
||||
.vibe/skills/gitea-client/scripts/gitea-client.sh comment-pr owner repo 42 "Build completed!"
|
||||
```
|
||||
|
||||
**Documentation:** See [.vibe/skills/gitea-client/README.md](.vibe/skills/gitea-client/README.md) for complete setup and usage guide.
|
||||
|
||||
## 🤖 AI Agent Usage
|
||||
|
||||
### Quick Launch Commands
|
||||
|
||||
**Programmer Agent** (for code implementation, testing, CI/CD):
|
||||
```bash
|
||||
vibe start --agent dancelessonscoachprogrammer
|
||||
```
|
||||
|
||||
**Product Owner Agent** (for requirements, interviews, documentation):
|
||||
```bash
|
||||
vibe start --agent dancelessonscoach-product-owner
|
||||
```
|
||||
|
||||
### Full Documentation
|
||||
|
||||
For complete agent usage guide including:
|
||||
- Agent selection guidance
|
||||
- Common workflow examples
|
||||
- Configuration reference
|
||||
- Best practices
|
||||
- Troubleshooting tips
|
||||
|
||||
See: [AGENT_USAGE_GUIDE.md](documentation/AGENT_USAGE_GUIDE.md)
|
||||
|
||||
### Gitmoji Cheatsheet
|
||||
|
||||
Quick reference for commit messages:
|
||||
- **📝 `:memo:` docs** - Documentation
|
||||
- **✨ `:sparkles:` feat** - New feature
|
||||
- **🐛 `:bug:` fix** - Bug fix
|
||||
- **♻️ `:recycle:` refactor** - Code refactoring
|
||||
- **🔧 `:wrench:` chore** - Build/config changes
|
||||
|
||||
Full cheatsheet: [GITMOJI_CHEATSHEET.md](documentation/GITMOJI_CHEATSHEET.md)
|
||||
Key decisions are documented in [adr/](adr/). See [AGENTS.md](AGENTS.md) for the full development reference (commands, config, ADR index, commit conventions).
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -48,8 +48,10 @@ func main() {
|
||||
log.Fatal().Err(err).Msg("Failed to load configuration")
|
||||
}
|
||||
|
||||
// Create readiness context to control readiness state
|
||||
readyCtx, readyCancel := context.WithCancel(context.Background())
|
||||
// Create readiness context to control readiness state.
|
||||
// CancelableContext exposes Cancel() so that Server.Run() can cancel
|
||||
// readiness at the start of graceful shutdown (before the propagation sleep).
|
||||
readyCtx, readyCancel := server.NewCancelableContext(context.Background())
|
||||
defer readyCancel()
|
||||
|
||||
// Create and run server
|
||||
@@ -57,4 +59,5 @@ func main() {
|
||||
if err := server.Run(); err != nil {
|
||||
log.Fatal().Err(err).Msg("Server failed")
|
||||
}
|
||||
log.Trace().Msg("Server exited")
|
||||
}
|
||||
|
||||
@@ -118,6 +118,34 @@ type SamplerConfig struct {
|
||||
Ratio float64 `mapstructure:"ratio"`
|
||||
}
|
||||
|
||||
// peekJSONLogging determines whether JSON logging should be used before the full
|
||||
// config is loaded, solving the chicken-and-egg problem where the logger format
|
||||
// must be known before any log is emitted, yet the format is stored in the config.
|
||||
//
|
||||
// Resolution order (mirrors Viper's own priority):
|
||||
// 1. DLC_LOGGING_JSON env var — checked directly via os.Getenv (zero overhead)
|
||||
// 2. logging.json key in the config file — read with a minimal throwaway Viper
|
||||
// instance so we don't parse the whole config twice unnecessarily
|
||||
func peekJSONLogging() bool {
|
||||
// 1. Env var takes highest priority — check it first
|
||||
if env := os.Getenv("DLC_LOGGING_JSON"); env != "" {
|
||||
return strings.EqualFold(env, "true") || env == "1"
|
||||
}
|
||||
|
||||
// 2. Try to read logging.json from the config file
|
||||
preV := viper.New()
|
||||
preV.SetDefault("logging.json", false)
|
||||
if configFile := os.Getenv("DLC_CONFIG_FILE"); configFile != "" {
|
||||
preV.SetConfigFile(configFile)
|
||||
} else {
|
||||
preV.SetConfigName("config")
|
||||
preV.SetConfigType("yaml")
|
||||
preV.AddConfigPath(".")
|
||||
}
|
||||
_ = preV.ReadInConfig() // ignore errors — defaults apply on failure
|
||||
return preV.GetBool("logging.json")
|
||||
}
|
||||
|
||||
// LoadConfig loads configuration from file, environment variables, and defaults
|
||||
// Configuration priority: file > environment variables > defaults
|
||||
// To specify a custom config file path, set DLC_CONFIG_FILE environment variable
|
||||
@@ -129,9 +157,17 @@ func LoadConfig() (*Config, error) {
|
||||
|
||||
v := viper.New()
|
||||
|
||||
// Set up initial console logging for config loading messages
|
||||
consoleWriter := zerolog.ConsoleWriter{Out: os.Stderr}
|
||||
log.Logger = log.Output(consoleWriter)
|
||||
// Configure the logger format before emitting any log output.
|
||||
// peekJSONLogging reads the JSON setting early (env var + config file pre-read)
|
||||
// so that every log line — including those produced during config loading — is
|
||||
// already in the correct format.
|
||||
jsonLogging := peekJSONLogging()
|
||||
if jsonLogging {
|
||||
log.Logger = log.Output(os.Stderr)
|
||||
} else {
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
|
||||
}
|
||||
log.Info().Bool("json", jsonLogging).Msg("Logging configured")
|
||||
|
||||
// Set default values
|
||||
v.SetDefault("server.host", "0.0.0.0")
|
||||
@@ -227,15 +263,9 @@ func LoadConfig() (*Config, error) {
|
||||
return nil, fmt.Errorf("config unmarshal error: %w", err)
|
||||
}
|
||||
|
||||
// Configure log output format (JSON or console) first
|
||||
if config.Logging.JSON {
|
||||
log.Logger = log.Output(os.Stderr)
|
||||
} else {
|
||||
consoleWriter := zerolog.ConsoleWriter{Out: os.Stderr}
|
||||
log.Logger = log.Output(consoleWriter)
|
||||
}
|
||||
|
||||
// Setup logging based on configuration
|
||||
// Setup logging based on configuration (level, output file, time format).
|
||||
// The JSON/console format was already applied at the top of LoadConfig via
|
||||
// peekJSONLogging, so SetupLogging only needs to handle the remaining knobs.
|
||||
config.SetupLogging()
|
||||
|
||||
log.Info().
|
||||
|
||||
@@ -33,6 +33,28 @@ import (
|
||||
//go:embed docs/swagger.json
|
||||
var swaggerJSON embed.FS
|
||||
|
||||
// CancelableContext wraps a context.Context and exposes a Cancel() method so
|
||||
// that Server.Run() can cancel readiness during graceful shutdown via the type
|
||||
// assertion it already performs. Callers that don't need controlled cancellation
|
||||
// (tests, CLI) can pass a plain context.Background() — the assertion silently
|
||||
// fails and readiness is never explicitly cancelled, which is harmless.
|
||||
type CancelableContext struct {
|
||||
context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
// NewCancelableContext creates a CancelableContext whose Cancel() method will
|
||||
// be invoked by Server.Run() at the start of graceful shutdown, before the
|
||||
// 1-second readiness propagation window. The returned CancelFunc is a no-op
|
||||
// after Cancel() has been called, so it is safe to defer in main.
|
||||
func NewCancelableContext(parent context.Context) (*CancelableContext, context.CancelFunc) {
|
||||
ctx, cancel := context.WithCancel(parent)
|
||||
return &CancelableContext{Context: ctx, cancel: cancel}, cancel
|
||||
}
|
||||
|
||||
// Cancel satisfies the interface checked in Run() and cancels the context.
|
||||
func (c *CancelableContext) Cancel() { c.cancel() }
|
||||
|
||||
type Server struct {
|
||||
router *chi.Mux
|
||||
readyCtx context.Context
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
# This script starts the server in the background and provides control functions
|
||||
|
||||
# Configuration
|
||||
PROJECT_DIR="/Users/gabrielradureau/Work/Vibe/dance-lessons-coach"
|
||||
SCRIPTS_DIR=$(dirname "$(realpath "${BASH_SOURCE[0]}")")
|
||||
PROJECT_DIR=$(dirname "$SCRIPTS_DIR")
|
||||
SERVER_CMD="go run ./cmd/server"
|
||||
LOG_FILE="server.log"
|
||||
PID_FILE="server.pid"
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
PROJECT_DIR="/Users/gabrielradureau/Work/Vibe/dance-lessons-coach"
|
||||
SCRIPTS_DIR=$(dirname "$(realpath "${BASH_SOURCE[0]}")")
|
||||
PROJECT_DIR=$(dirname "$SCRIPTS_DIR")
|
||||
SERVER_CMD="./scripts/start-server.sh"
|
||||
LOG_FILE="server.log"
|
||||
PID_FILE="server.pid"
|
||||
@@ -59,11 +60,40 @@ echo "Response: $GREET_NAME_RESPONSE"
|
||||
echo ""
|
||||
echo "Stopping server gracefully..."
|
||||
|
||||
# Test readiness during shutdown (in background)
|
||||
(curl -s http://localhost:8080/api/ready > /dev/null 2>&1 &)
|
||||
# Send SIGTERM once and probe /api/ready during the 1-second propagation window
|
||||
# the server holds open (pkg/server/server.go: time.Sleep(1s) after readiness
|
||||
# cancel). Previously the curl fired *before* the signal — it always saw "ready".
|
||||
# We also avoid calling "$SERVER_CMD stop" afterwards because that would send a
|
||||
# second SIGTERM: after signal.NotifyContext is done, the default handler kicks in
|
||||
# and the process terminates with a non-JSON "signal: terminated" on stderr.
|
||||
SERVER_PID=$(cat "$PID_FILE" 2>/dev/null || echo "")
|
||||
if [[ -z "$SERVER_PID" ]]; then
|
||||
echo -e "\033[0;31m❌ FAIL: PID file not found\033[0m"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
$SERVER_CMD stop
|
||||
sleep 3
|
||||
kill -TERM "$SERVER_PID"
|
||||
# Brief yield so the signal handler runs and CancelableContext.Cancel() fires
|
||||
sleep 0.2
|
||||
READY_DURING_SHUTDOWN=$(curl -s -w "\n[HTTP %{http_code}]" http://localhost:8080/api/ready 2>&1 || echo "[connection refused]")
|
||||
echo "Readiness during shutdown: $READY_DURING_SHUTDOWN"
|
||||
|
||||
# Wait for the process to exit cleanly (up to 30s) without sending another signal
|
||||
echo "Waiting for server to exit..."
|
||||
for i in {1..30}; do
|
||||
if ! ps -p "$SERVER_PID" > /dev/null 2>&1; then
|
||||
echo "Server stopped successfully"
|
||||
rm -f "$PID_FILE"
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
if ps -p "$SERVER_PID" > /dev/null 2>&1; then
|
||||
echo -e "\033[0;31m❌ FAIL: Server did not stop within 30s\033[0m"
|
||||
kill -9 "$SERVER_PID" 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
sleep 0.5
|
||||
|
||||
echo ""
|
||||
echo "Analyzing server logs..."
|
||||
@@ -201,6 +231,12 @@ fi
|
||||
echo ""
|
||||
echo -e "\033[0;32m🎉 GRACEFUL SHUTDOWN TEST PASSED!\033[0m"
|
||||
echo "All required logs are present and in correct order."
|
||||
|
||||
echo ""
|
||||
echo "📋 Full server log:"
|
||||
echo "==============================="
|
||||
cat "$LOG_FILE" | jq -r '"[\(.level | ascii_upcase)] \(.time | tostring) — \(.message)"'
|
||||
echo "==============================="
|
||||
echo ""
|
||||
|
||||
# Clean up
|
||||
|
||||
@@ -9,7 +9,8 @@ echo -e "\033[1;34m=== dance-lessons-coach OpenTelemetry Test ===\033[0m"
|
||||
echo ""
|
||||
|
||||
# Configuration
|
||||
PROJECT_DIR="/Users/gabrielradureau/Work/Vibe/dance-lessons-coach"
|
||||
SCRIPTS_DIR=$(dirname "$(realpath "${BASH_SOURCE[0]}")")
|
||||
PROJECT_DIR=$(dirname "$SCRIPTS_DIR")
|
||||
SERVER_CMD="./scripts/start-server.sh"
|
||||
LOG_FILE="server.log"
|
||||
PID_FILE="server.pid"
|
||||
|
||||
Reference in New Issue
Block a user