Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,36 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added
- **framework:** `AddUnaryInterceptor`, `AddStreamInterceptor`, and `AddHTTPMiddleware` on `App` — bundles can register gRPC interceptors and HTTP middleware during `Initialize`.
- **framework:** `WithLogging`, `WithObservability`, and `WithHealthRegistry` options to inject custom infrastructure managers, plus an `Observability()` accessor.
- **prometheus:** Automatic HTTP and gRPC request metrics — adding the bundle now records request metrics with no per-handler code.
- **jwt:** `StreamServerInterceptor` so streaming RPCs are authenticated (previously only unary RPCs were), and a `TrustedProxyHeader` config flag gating `X-Forwarded-Proto` trust.
- **httpclient:** Optional `AllowedHosts` allowlist (defense-in-depth SSRF guard) covering both requests and redirect targets.
- **health:** Background check runner honoring per-check `Interval`/`InitialDelay`; probe endpoints serve cached results so a slow dependency cannot stall Kubernetes probes.

### Changed (breaking)
- **API naming:** `Get`-prefixed getters renamed to drop the prefix — `Tracer`, `Meter`, `RegisteredChecks`, `CheckConfig`, `HealthySummary`, `MetricsHandler`, `SecureMetricsHandler`, `ConfigInfo`, `CircuitBreakerState`, `CircuitBreakerCounts`, and `jwt.ServiceID`/`jwt.ServiceName`. (`CredentialProvider.GetAPIKey`/`GetJWTToken` are unchanged — they are context-taking fetch operations, not accessors.)
- **health:** `HealthStatus` renamed to `Report`; constructors `NewHealthyStatus`/`NewUnhealthyStatus`/`NewUnknownStatus` renamed to `NewHealthyReport`/`NewUnhealthyReport`/`NewUnknownReport`.
- **errors package:** moved from `github.com/datariot/forge/errors` to `github.com/datariot/forge/forgeerrors` (package `forgeerrors`) so it no longer shadows the standard library `errors`.
- **postgresql:** switched the driver from `lib/pq` (maintenance mode) to `pgx/v5/stdlib`. Connection-string compatible; no config changes required.
- Health check HTTP responses no longer include raw per-check error strings (which could leak internal hostnames/ports); full detail remains in logs.
- gRPC reflection now requires an explicit `EnableReflection` flag instead of being on by default in development.

### Security
- Bumped `google.golang.org/grpc` (GO-2026-4762, authorization bypass) and `go.opentelemetry.io/otel/sdk` (GO-2026-4394, code execution); `govulncheck` reports no vulnerabilities affecting the code.
- **httpclient:** auth headers are stripped on cross-host redirects.
- Opt-in HTTP Basic Auth for the `/metrics` endpoint; pprof is now blocked outside development.

### Migration from 0.1.0

- Replace imports of `github.com/datariot/forge/errors` with `github.com/datariot/forge/forgeerrors` and update references from `errors.X` to `forgeerrors.X`.
- Drop the `Get` prefix from the renamed getters listed above.
- Replace `health.HealthStatus` with `health.Report` and the three `New*Status` constructors with `New*Report`.
- No action needed for the pgx driver swap unless you referenced the `"postgres"` driver name directly.

## [0.1.0] - 2026-02-25

### Added
Expand All @@ -28,4 +58,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Overall coverage raised from 29.6% to 70.0%
- Added unit tests across all packages: errors (100%), health (96.5%), prometheus (76.4%), httpclient (75.9%), config (73.8%), testutil (71.1%), framework (70.0%), configloader (66.7%), jwt (62.0%), redis (36.0%), postgresql (30.6%)

[Unreleased]: https://github.com/datariot/forge/compare/v0.1.0...HEAD
[0.1.0]: https://github.com/datariot/forge/releases/tag/v0.1.0
65 changes: 65 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Contributing to Forge

Thanks for your interest in improving Forge. This guide covers how to get set up, what's expected of a change, and how to get it merged.

## Prerequisites

- **Go 1.25+**
- **[Task](https://taskfile.dev)** (optional but recommended — `brew install go-task`)
- **Docker + Docker Compose** (only for integration tests)
- **[golangci-lint](https://golangci-lint.run) v2.12.x** — CI pins this version; match it locally to avoid surprises.

## Getting started

```bash
git clone https://github.com/datariot/forge
cd forge
task test # or: go test ./...
```

## Development workflow

```bash
task test # unit tests with the race detector
task test:coverage # coverage report (opens coverage.html)
task test:integration # integration tests — starts PostgreSQL + Redis via Docker, runs, tears down
task lint # gofmt check + go vet + golangci-lint
task fmt # format the tree
task build:all # build the framework and every example
task --list # all available tasks
```

The raw `go` commands (`go test ./...`, `go build ./...`, `go vet ./...`) work without Task if you prefer.

## What a change needs

Before opening a pull request, make sure:

1. **It builds and passes** — `go build ./...`, `go test ./... -race`, and every module under `examples/` builds. CI runs all three.
2. **It's formatted** — `gofmt -l .` reports nothing. CI fails on unformatted code.
3. **It lints clean** — `golangci-lint run` reports 0 issues.
4. **It's tested** — new behavior comes with tests. The project targets **70%+ coverage** overall (CI enforces the threshold); the health and framework core aim higher.
5. **Exported symbols are documented** — godoc comments on exported types, functions, and methods, starting with the symbol name. pkg.go.dev is the API reference, so this matters.

### Adding a bundle

Bundles live under `bundles/<name>/` and follow a consistent shape:

- A `Config` struct with a `Validate() error` method and a `DefaultConfig()` constructor.
- `NewBundle(config Config) *Bundle`.
- `Name() string`, `Initialize(app *framework.App) error`, and `Stop(ctx context.Context) error`.
- Health checks via the `HealthContributor` interface where the bundle manages a dependency.
- **Evaluate third-party dependencies carefully** — Forge is "batteries included" but deliberately lightweight. A new direct dependency should earn its place; prefer the standard library where it's close enough.

Model new bundles on `bundles/postgresql` or `bundles/redis`.

## Commit and PR conventions

- Use [Conventional Commits](https://www.conventionalcommits.org) (`feat:`, `fix:`, `docs:`, `refactor:`, `chore:`; use `!` for breaking changes, e.g. `refactor!:`).
- Keep pull requests focused; a reviewable diff beats a sweeping one.
- Note any breaking API change in the PR description and in [CHANGELOG.md](CHANGELOG.md) under "Unreleased".
- CI (unit tests with race detection, formatting, linting, example builds, coverage) must be green before merge.

## Reporting issues

Open a GitHub issue with the Forge version (or commit), Go version, a minimal reproduction if you can, and what you expected versus what happened.
194 changes: 87 additions & 107 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,50 @@
# Forge

A batteries-included Go framework for building production-ready microservices.
[![CI](https://github.com/datariot/forge/actions/workflows/test.yml/badge.svg)](https://github.com/datariot/forge/actions/workflows/test.yml)
[![Go Reference](https://pkg.go.dev/badge/github.com/datariot/forge.svg)](https://pkg.go.dev/github.com/datariot/forge)
[![Go Report Card](https://goreportcard.com/badge/github.com/datariot/forge)](https://goreportcard.com/report/github.com/datariot/forge)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
![Go 1.25+](https://img.shields.io/badge/Go-1.25%2B-00ADD8?logo=go)

## Overview
A batteries-included Go framework for building production-ready microservices — inspired by DropWizard, designed for Go's strengths.

Forge is inspired by DropWizard but designed specifically for Go's strengths - interfaces, goroutines, and clean composition. It provides opinionated defaults while maintaining flexibility through a pluggable architecture.
Forge wires up the boilerplate every service re-implements — lifecycle orchestration, health checks, structured logging, tracing, metrics, graceful shutdown — behind a small, composable API, so you write business logic instead of plumbing.

## Features
```go
app, err := framework.New(
framework.WithConfig(&cfg),
framework.WithComponent(myService),
framework.WithBundle(postgresql.NewBundle(dbConfig)),
)
if err != nil {
log.Fatal(err)
}
log.Fatal(app.Run(context.Background()))
```

## Why Forge

**Use Forge when** you're running more than one Go service and are tired of copy-pasting the same `main.go` — the health endpoints, the OTel setup, the signal handling, the "did I remember to drain connections on shutdown" checklist. Forge gives you opinionated, production-grade defaults for all of it and a bundle system to add PostgreSQL, Redis, JWT auth, a resilient HTTP client, and Prometheus metrics with one line each.

**What you get out of the box:**

- **Lifecycle orchestration** — ordered startup, reverse-order graceful shutdown, timeout handling, and startup/shutdown hooks.
- **Health checks** — Kubernetes-style `/health`, `/health/ready`, `/health/live` plus a gRPC health service, run on a background schedule and cached so a slow dependency can't stall your probes.
- **Observability** — OpenTelemetry tracing (W3C propagation, configurable sampling), structured JSON logging via zerolog, and automatic HTTP/gRPC request metrics when the Prometheus bundle is added.
- **Bundles** — PostgreSQL (pooled, via pgx), Redis (cache, pub/sub, distributed locks, rate limiting), JWT service auth (unary + stream interceptors), a circuit-breaking HTTP client, Prometheus, and multi-source config loading with hot reload.
- **Config with validation** — a common `BaseConfig` with environment-aware defaults; layer YAML files and environment overrides via the configloader bundle.

**Forge is probably not for you if** you want a full web framework with routing, middleware, and an ORM (reach for Echo/Gin/Fiber + your data layer), or if your service is a single small binary where the framework's structure is more than you need. Forge is opinionated about *operability*, not about how you write handlers.

**Status:** pre-1.0. The API is settling and may still change between minor versions; pin your dependency and read the [CHANGELOG](CHANGELOG.md) before upgrading.

## Install

```bash
go get github.com/datariot/forge
```

- **Clean Architecture**: Component-based design with clear separation of concerns
- **Observability Built-in**: OpenTelemetry tracing, structured logging, Prometheus metrics
- **Health Checks**: Comprehensive liveness and readiness checks
- **Graceful Lifecycle**: Sophisticated startup and shutdown orchestration
- **Configuration Management**: Validated config with YAML + env overrides via the configloader bundle
- **Database Integration**: PostgreSQL connection pooling with health checks
- **Redis Integration**: Caching, pub/sub, distributed locks, and rate limiting
- **Resilient HTTP Client**: Circuit breaker, retries, and backoff built in
- **Security First**: No hardcoded credentials, explicit validation requirements
Requires Go 1.25+.

## Quick Start

Expand All @@ -27,137 +55,89 @@ import (
"context"
"log"

"github.com/datariot/forge/bundles/postgresql"
"github.com/datariot/forge/config"
"github.com/datariot/forge/framework"
"github.com/datariot/forge/health"
)

type Greeter struct{}

func (Greeter) Start(ctx context.Context) error { log.Println("greeter up"); return nil }
func (Greeter) Stop(ctx context.Context) error { log.Println("greeter down"); return nil }

func (Greeter) HealthChecks() []health.Check {
return []health.Check{health.NewAlwaysHealthyCheck("greeter")}
}

func main() {
cfg := config.DefaultBaseConfig()
cfg.ServiceName = "my-service"

pgConfig := postgresql.DefaultConfig()
pgConfig.DatabaseURL = "postgres://user:pass@localhost:5432/mydb"
cfg.ServiceName = "greeter"

svc := Greeter{}
app, err := framework.New(
framework.WithConfig(&cfg),
framework.WithVersion("1.0.0"),
framework.WithComponent(NewMyComponent(&cfg)),
framework.WithBundle(postgresql.NewBundle(pgConfig)),
framework.WithComponent(svc),
framework.WithHealthContributor(svc),
)
if err != nil {
log.Fatal(err)
}

// Run blocks until SIGINT/SIGTERM, then shuts down gracefully.
if err := app.Run(context.Background()); err != nil {
log.Fatal(err)
}
}
```

## Installation
Run it, then hit the health endpoints on the HTTP server (`:8081` by default):

```bash
go get github.com/datariot/forge
curl localhost:8081/health # overall status
curl localhost:8081/health/ready # readiness probe
curl localhost:8081/health/live # liveness probe
```

## Development

### Prerequisites
The gRPC server (`:8080` by default) starts only when you register a gRPC service with `framework.WithGRPCRegistrar(...)`, so an HTTP-only service like the one above runs without it.

- Go 1.25+
- Docker & Docker Compose (for integration tests)
- [Task](https://taskfile.dev) (optional, for task runner)
See [`examples/`](examples/) for complete services using each bundle, and the [Getting Started guide](docs/getting-started.md) for a step-by-step walkthrough.

### Common Commands
## Bundles

```bash
# Run tests
task test
# OR
go test ./...
Add a bundle with `framework.WithBundle(...)`; it self-initializes during startup, contributes its own health checks, and cleans up on shutdown.

# Run tests with coverage
task test:coverage
| Bundle | What it provides |
|--------|------------------|
| `bundles/postgresql` | Pooled `database/sql` access via pgx, with a readiness check |
| `bundles/redis` | Cache, pub/sub, distributed locks, sliding-window rate limiting |
| `bundles/jwt` | Service-to-service auth with gRPC unary + stream interceptors and HTTP middleware |
| `bundles/httpclient` | HTTP client with circuit breaker, retries, backoff, and an optional host allowlist |
| `bundles/prometheus` | Metrics registry and **automatic** HTTP/gRPC request metrics |
| `bundles/configloader` | YAML + environment config loading with validation and hot reload |

# Run integration tests (requires Docker)
task test:integration
Full reference: [pkg.go.dev/github.com/datariot/forge](https://pkg.go.dev/github.com/datariot/forge).

# Build framework and examples
task build:all

# Format and lint code
task lint

# See all available tasks
task --list
```

### Testing
## Documentation

**Unit Tests** (no external dependencies):
```bash
task test
```
- [Getting Started](docs/getting-started.md) — build your first service step by step
- [Bundles Guide](docs/bundles.md) — configuring and using each integration
- [API Reference](https://pkg.go.dev/github.com/datariot/forge) — full godoc on pkg.go.dev
- [Examples](examples/) — runnable services
- [Contributing](CONTRIBUTING.md) — dev setup and PR workflow

**Integration Tests** (requires Docker):
```bash
task docker:up # Start PostgreSQL + Redis
task test:integration # Run integration tests
task docker:down # Stop services
```
## Development

**Coverage Report**:
```bash
task test:coverage # Generates coverage.html
task test # unit tests (race-enabled)
task test:coverage # coverage report
task lint # gofmt check + go vet + golangci-lint
task build:all # framework + examples
task --list # everything else
```

Current test coverage: **70.0%** (Target: 70%+)

See [TESTING.md](TESTING.md) for comprehensive testing strategy.

## Architecture

Forge follows Clean Architecture principles:

- **Framework**: Core application lifecycle and interfaces
- **Bundles**: Pre-built integrations (PostgreSQL, Redis, JWT, HTTP client, Prometheus, configloader)
- **Components**: Your business logic implementing framework interfaces
- **Config**: Common service configuration with validation

## Documentation

- [Getting Started](docs/getting-started.md)
- [API Reference](docs/api-reference.md)
- [Bundles Guide](docs/bundles.md)
- [Examples](examples/)
- [Development Guide](CLAUDE.md)

## CI/CD

GitHub Actions automatically runs on all PRs:
- Unit tests with race detection
- Code formatting checks
- Linting (go vet + golangci-lint)
- Build verification (framework + examples)
- Coverage reporting (70% threshold)

See [.github/workflows/test.yml](.github/workflows/test.yml) for details.
`go test ./...` and `go build ./...` work without Task if you prefer. Integration tests (PostgreSQL + Redis) run under `task test:integration` and require Docker. See [CONTRIBUTING.md](CONTRIBUTING.md).

## License

MIT License - see LICENSE file for details.

## Contributing

1. Fork the repository
2. Create a feature branch
3. Add tests for your changes
4. Ensure `task ci` passes
5. Submit a pull request

All contributions must:
- Include tests (maintain 70%+ coverage)
- Follow Go best practices
- Include documentation
- Pass CI/CD checks
MIT — see [LICENSE](LICENSE).
Loading
Loading