This is the abridged developer documentation for Platforma
# platforma
> Golang backend application framework
### Install package [Section titled “Install package”](#install-package) to use framework in your application ```bash go get -u github.com/platforma-dev/platforma@latest ``` ### Install command [Section titled “Install command”](#install-command) to use `platforma` CLI ```bash go install github.com/platforma-dev/platforma@latest ```
# CLI
## docs [Section titled “docs”](#docs) Serving this documentations ```bash platforma docs ``` Parameters: * `-p`, `--port` - set port on what documentation is served. Default is `4444` ## generate [Section titled “generate”](#generate) ### domain [Section titled “domain”](#domain) ```bash platforma generate domain ```
# Getting started
> Installation and first application
`platforma` is a framework for building backend applications with Golang. It’s has several packages like `httpserver` and `database` that provide solid foundation which you can build on top of. ## Installation [Section titled “Installation”](#installation) ```shell go get -u github.com/platforma-dev/platforma@latest ``` ## First application [Section titled “First application”](#first-application) main.go ```go package main import ( "context" "fmt" "time" "github.com/platforma-dev/platforma/application" "github.com/platforma-dev/platforma/log" ) type Clock struct{} func (r *Clock) Run(ctx context.Context) error { ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() for { select { case <-ticker.C: log.InfoContext(ctx, "tick") case <-ctx.Done(): log.InfoContext(ctx, "finished") return fmt.Errorf("context error: %w", ctx.Err()) } } } func main() { ctx := context.Background() app := application.New() app.RegisterService("clock", &Clock{}) if err := app.Run(ctx); err != nil { log.ErrorContext(ctx, "app finished with error", "error", err) } } ```
# application
The `application` package provides the central orchestrator for managing the lifecycle of your application, including startup tasks, services, databases, and health checks. Core Components: * `Application`: Central orchestrator that manages startup tasks, services, databases, and health checks * `Runner`: Interface that services and startup tasks must implement to be executed by the application * `RunnerFunc`: Function type that implements `Runner` for simple inline tasks * `StartupTaskConfig`: Configuration for startup tasks with name and abort-on-error behavior * `Domain`: Interface for self-contained modules that bundle repository and other components * `Healthchecker`: Interface for services that can report their health status * `HealthCheckHandler`: HTTP handler for exposing application health as JSON * `ApplicationHealth`: Tracks overall application health and individual service statuses * `ServiceHealth`: Health status for a single service including start time and errors [Full package docs at pkg.go.dev](https://pkg.go.dev/github.com/platforma-dev/platforma/application) ## Step-by-step guide [Section titled “Step-by-step guide”](#step-by-step-guide) 1. Create a new `Application` instance ```go app := application.New() ``` 2. Define a service that implements the `Runner` interface ```go type MyService struct{} func (s *MyService) Run(ctx context.Context) error { ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() for { select { case <-ticker.C: log.InfoContext(ctx, "service tick") case <-ctx.Done(): log.InfoContext(ctx, "service shutting down") return nil } } } ``` The `Run` method should block until the context is canceled or an error occurs. 3. Register startup tasks that run before services start ```go app.OnStartFunc(func(ctx context.Context) error { log.InfoContext(ctx, "initialization complete") return nil }, application.StartupTaskConfig{ Name: "init", AbortOnError: true, }) ``` Setting `AbortOnError` to `true` stops the application if this task fails. Use `OnStart` for `Runner` implementations or `OnStartFunc` for inline functions. 4. Register your service with the application ```go app.RegisterService("my-service", &MyService{}) ``` 5. Run the application ```go ctx := context.Background() if err := app.Run(ctx); err != nil { log.ErrorContext(ctx, "app finished with error", "error", err) } ``` The `Run` method parses CLI arguments from `os.Args`: ```bash # Run database migrations ./myapp migrate # Start the application (services and startup tasks) ./myapp run # Show usage ./myapp --help ``` 6. Run migrations first, then start your app ```bash ./myapp migrate ./myapp run ``` You will see output like this: ```plaintext time=2025-11-11T22:01:26.630+03:00 level=INFO msg="starting application" startupTasks=1 time=2025-11-11T22:01:26.630+03:00 level=INFO msg="running task" task=init index=0 time=2025-11-11T22:01:26.630+03:00 level=INFO msg="initialization complete" time=2025-11-11T22:01:26.631+03:00 level=INFO msg="starting service" serviceName=my-service time=2025-11-11T22:01:27.631+03:00 level=INFO msg="service tick" time=2025-11-11T22:01:28.631+03:00 level=INFO msg="service tick" ``` Press Ctrl+C to gracefully shutdown the application. ## CLI commands [Section titled “CLI commands”](#cli-commands) The `Run` method reads `os.Args` and executes the appropriate command: | Command | Description | | -------------- | ------------------------------------------------ | | `run` | Start the application (startup tasks + services) | | `migrate` | Run database migrations and exit | | `--help`, `-h` | Show usage information | If no command is provided, usage information is printed. ## Execution order [Section titled “Execution order”](#execution-order) When you run `./myapp run`, the following happens in order: 1. **Startup tasks** - Tasks run sequentially in registration order 2. **Services** - All services start concurrently in separate goroutines 3. **Wait** - Application waits for context cancellation (Ctrl+C) 4. **Shutdown** - Services receive context cancellation for graceful shutdown When you run `./myapp migrate`: 1. **Database migrations** - All registered databases run their migrations 2. **Exit** - Application exits after migrations complete ## Register methods [Section titled “Register methods”](#register-methods) ### RegisterService [Section titled “RegisterService”](#registerservice) Registers a named service that runs concurrently when the application starts. ```go app.RegisterService("api", httpServer) app.RegisterService("queue-processor", processor) ``` If the service implements `Healthchecker`, its health status is automatically tracked. ### RegisterDatabase [Section titled “RegisterDatabase”](#registerdatabase) Registers a database connection. Migrations are run when you execute the `migrate` command. ```go db, _ := database.New("postgres://user:pass@localhost:5432/mydb?sslmode=disable") app.RegisterDatabase("main", db) ``` ### RegisterRepository [Section titled “RegisterRepository”](#registerrepository) Registers a repository with a database. The repository must have a `Migrations()` method. ```go app.RegisterRepository("main", "users", userRepo) ``` ### RegisterDomain [Section titled “RegisterDomain”](#registerdomain) Registers a domain module. If a database name is provided, the domain’s repository is registered automatically. ```go app.RegisterDomain("auth", "main", authDomain) ``` ## Health checks [Section titled “Health checks”](#health-checks) Services implementing `Healthchecker` have their health tracked automatically: ```go type Healthchecker interface { Healthcheck(context.Context) any } ``` Expose health via HTTP using `HealthCheckHandler`: ```go api.Handle("/health", application.NewHealthCheckHandler(app)) ``` The response includes application start time and per-service status: ```json { "startedAt": "2025-01-01T12:00:00Z", "services": { "api": { "status": "STARTED", "startedAt": "2025-01-01T12:00:00Z" } } } ``` ## Error handling [Section titled “Error handling”](#error-handling) The application returns specific error types: * `ErrUnknownCommand` - Returned when an unknown CLI command is provided * `ErrStartupTaskFailed` - Returned when a startup task with `AbortOnError: true` fails * `ErrDatabaseMigrationFailed` - Returned when database migration fails (from `migrate` command) Both error types support unwrapping to get the underlying error: ```go if err := app.Run(ctx); err != nil { var migrationErr *application.ErrDatabaseMigrationFailed if errors.As(err, &migrationErr) { log.ErrorContext(ctx, "migration failed", "cause", migrationErr.Unwrap()) } } ``` ## Complete example [Section titled “Complete example”](#complete-example) application.go ```go package main import ( "context" "fmt" "time" "github.com/platforma-dev/platforma/application" "github.com/platforma-dev/platforma/log" ) type Clock struct{} func (r *Clock) Run(ctx context.Context) error { ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() for { select { case <-ticker.C: log.InfoContext(ctx, "tick") case <-ctx.Done(): log.InfoContext(ctx, "finished") return fmt.Errorf("context error: %w", ctx.Err()) } } } func main() { ctx := context.Background() app := application.New() app.RegisterService("clock", &Clock{}) if err := app.Run(ctx); err != nil { log.ErrorContext(ctx, "app finished with error", "error", err) } } ```
# auth
The `auth` package provides session-based authentication with username/password credentials, bundled as a self-contained domain. Core Components: * `Domain`: Bundles repository, service, HTTP handlers, and middleware together. Implements `application.Domain` interface for registration with an `Application`. * `Service`: Core authentication logic for user registration, login/logout, password changes, and user deletion. * `Repository`: PostgreSQL storage for users with automatic schema migrations. * `User`: User model with ID, username, hashed password, salt, timestamps, and status. * `AuthenticationMiddleware`: HTTP middleware that validates session cookies and injects the authenticated user into request context. * `UserCleanupJob`: Job struct for enqueueing post-deletion cleanup tasks. * `UserFromContext`: Helper function to retrieve the authenticated user from request context. [Full package docs at pkg.go.dev](https://pkg.go.dev/github.com/platforma-dev/platforma/auth) ## Step-by-step guide [Section titled “Step-by-step guide”](#step-by-step-guide) 1. Set up the database and session storage ```go db, err := database.New("postgres://user:pass@localhost:5432/mydb?sslmode=disable") if err != nil { log.ErrorContext(ctx, "failed to connect to database", "error", err) return } sessionDomain := session.New(db.Connection()) ``` The auth package requires a database connection and session storage. The `session` package provides a compatible session storage implementation. 2. Create the auth domain ```go authDomain := auth.New( db.Connection(), // database connection sessionDomain.Service, // session storage "session_id", // cookie name for sessions nil, // username validator (nil uses default) nil, // password validator (nil uses default) nil, // cleanup job enqueuer (optional) ) ``` Default validators require usernames to be 5-20 characters and passwords to be 8-100 characters. Pass custom validator functions to override. 3. Register domains with the application ```go app := application.New() app.RegisterDatabase("main", db) app.RegisterDomain("session", "main", sessionDomain) app.RegisterDomain("auth", "main", authDomain) ``` Registering domains with a database name automatically registers their repositories for migrations. 4. Create an HTTP server and mount the auth endpoints ```go api := httpserver.New("8080", 3*time.Second) api.HandleGroup("/auth", authDomain.HandleGroup) ``` This exposes the following endpoints: * `POST /auth/register` - Create a new user * `POST /auth/login` - Login and receive session cookie * `POST /auth/logout` - Clear session * `GET /auth/me` - Get current user info * `POST /auth/change-password` - Change password (requires auth) * `DELETE /auth/me` - Delete user account (requires auth) 5. Protect routes with authentication middleware ```go protectedGroup := httpserver.NewHandlerGroup() protectedGroup.Use(authDomain.Middleware) protectedGroup.HandleFunc("/profile", func(w http.ResponseWriter, r *http.Request) { user := auth.UserFromContext(r.Context()) if user != nil { w.Write([]byte("Hello, " + user.Username)) } }) api.HandleGroup("/api", protectedGroup) ``` The `AuthenticationMiddleware` returns 401 Unauthorized if no valid session is found. Use `auth.UserFromContext()` to access the authenticated user. 6. Register the server and run the application ```go app.RegisterService("api", api) if err := app.Run(ctx); err != nil { log.ErrorContext(ctx, "app finished with error", "error", err) } ``` Expected output: ```plaintext time=2025-01-01T12:00:00.000+00:00 level=INFO msg="running migrations" database=main time=2025-01-01T12:00:00.010+00:00 level=INFO msg="starting service" serviceName=api time=2025-01-01T12:00:00.010+00:00 level=INFO msg="starting http server" address=:8080 ``` ## Using with Application [Section titled “Using with Application”](#using-with-application) The `auth.Domain` implements the `application.Domain` interface, so it integrates seamlessly with an `Application`: ```go app := application.New() // Set up database db, _ := database.New("postgres://user:pass@localhost:5432/mydb?sslmode=disable") app.RegisterDatabase("main", db) // Set up session and auth domains sessionDomain := session.New(db.Connection()) app.RegisterDomain("session", "main", sessionDomain) authDomain := auth.New(db.Connection(), sessionDomain.Service, "session_id", nil, nil, nil) app.RegisterDomain("auth", "main", authDomain) // Set up HTTP server with auth endpoints api := httpserver.New("8080", 3*time.Second) api.HandleGroup("/auth", authDomain.HandleGroup) app.RegisterService("api", api) app.Run(ctx) ``` ## HTTP endpoints [Section titled “HTTP endpoints”](#http-endpoints) | Endpoint | Method | Auth Required | Description | | ------------------ | ------ | ------------- | ----------------------------------------------------------------------- | | `/register` | POST | No | Create new user with `{"login": "...", "password": "..."}` | | `/login` | POST | No | Login with `{"login": "...", "password": "..."}`, sets session cookie | | `/logout` | POST | No | Clears session cookie | | `/me` | GET | No | Returns `{"username": "..."}` if authenticated, 401 otherwise | | `/change-password` | POST | Yes | Change password with `{"currentPassword": "...", "newPassword": "..."}` | | `/me` | DELETE | Yes | Delete user account and all sessions | ## Custom validators [Section titled “Custom validators”](#custom-validators) Override the default username and password validation: ```go usernameValidator := func(username string) error { if len(username) < 3 { return errors.New("username must be at least 3 characters") } if !regexp.MustCompile(`^[a-zA-Z0-9_]+$`).MatchString(username) { return errors.New("username can only contain letters, numbers, and underscores") } return nil } passwordValidator := func(password string) error { if len(password) < 12 { return errors.New("password must be at least 12 characters") } return nil } authDomain := auth.New(db.Connection(), sessionDomain.Service, "session_id", usernameValidator, passwordValidator, nil) ``` ## User cleanup jobs [Section titled “User cleanup jobs”](#user-cleanup-jobs) When a user is deleted, you can enqueue cleanup jobs to handle related data. The `queue.Processor` implements the required interface directly: ```go // Create a processor for cleanup jobs cleanupHandler := queue.HandlerFunc[auth.UserCleanupJob](func(ctx context.Context, job auth.UserCleanupJob) { log.InfoContext(ctx, "cleaning up user data", "user_id", job.UserID, "deleted_at", job.DeletedAt) // Delete user's files, posts, comments, etc. }) cleanupQueue := queue.NewChanQueue[auth.UserCleanupJob](100, 5*time.Second) cleanupProcessor := queue.New(cleanupHandler, cleanupQueue, 2, 10*time.Second) // Pass processor directly to auth.New() authDomain := auth.New(db.Connection(), sessionDomain.Service, "session_id", nil, nil, cleanupProcessor) // Register processor as a service app.RegisterService("cleanup", cleanupProcessor) ``` The `UserCleanupJob` contains `UserID` and `DeletedAt` fields for processing cleanup tasks asynchronously. ## Error types [Section titled “Error types”](#error-types) The package exports these errors for handling specific cases: * `ErrUserNotFound` - User does not exist * `ErrWrongUserOrPassword` - Invalid credentials during login * `ErrInvalidUsername` / `ErrShortUsername` / `ErrLongUsername` - Username validation failed * `ErrInvalidPassword` / `ErrShortPassword` / `ErrLongPassword` - Password validation failed * `ErrCurrentPasswordIncorrect` - Current password wrong during password change ## Complete example [Section titled “Complete example”](#complete-example) auth.go ```go package main import ( "context" "net/http" "time" "github.com/platforma-dev/platforma/application" "github.com/platforma-dev/platforma/auth" "github.com/platforma-dev/platforma/database" "github.com/platforma-dev/platforma/httpserver" "github.com/platforma-dev/platforma/log" "github.com/platforma-dev/platforma/session" ) func main() { ctx := context.Background() db, err := database.New("postgres://user:pass@localhost:5432/mydb?sslmode=disable") if err != nil { log.ErrorContext(ctx, "failed to connect to database", "error", err) return } sessionDomain := session.New(db.Connection()) authDomain := auth.New(db.Connection(), sessionDomain.Service, "session_id", nil, nil, nil) app := application.New() app.RegisterDatabase("main", db) app.RegisterDomain("session", "main", sessionDomain) app.RegisterDomain("auth", "main", authDomain) api := httpserver.New("8080", 3*time.Second) api.Use(log.NewTraceIDMiddleware(nil, "")) api.Use(httpserver.NewRecoverMiddleware()) api.HandleGroup("/auth", authDomain.HandleGroup) protected := httpserver.NewHandlerGroup() protected.Use(authDomain.Middleware) protected.HandleFunc("/profile", func(w http.ResponseWriter, r *http.Request) { user := auth.UserFromContext(r.Context()) w.Write([]byte("Welcome, " + user.Username)) }) api.HandleGroup("/api", protected) api.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("pong")) }) app.RegisterService("api", api) if err := app.Run(ctx); err != nil { log.ErrorContext(ctx, "app finished with error", "error", err) } } ```
# database
The `database` package provides PostgreSQL connection management with built-in migration support using sqlx. Core Components: * `Database`: Database connection with migration orchestration capabilities. * `Migration`: Represents a single database migration with Up and Down SQL statements. * `New(connection string) (*Database, error)`: Creates a new PostgreSQL database connection. * `ParseMigrations(fsys fs.FS) ([]Migration, error)`: Parses SQL migration files from a filesystem. [Full package docs at pkg.go.dev](https://pkg.go.dev/github.com/platforma-dev/platforma/database) ## Step-by-step guide [Section titled “Step-by-step guide”](#step-by-step-guide) 1. Create a database connection ```go db, err := database.New("postgres://user:password@localhost:5432/mydb?sslmode=disable") if err != nil { log.ErrorContext(ctx, "failed to connect", "error", err) os.Exit(1) } ``` This establishes a PostgreSQL connection using the provided connection string. 2. Define your repository with migrations ```go type UserRepository struct { db *sqlx.DB } //go:embed *.sql var migrations embed.FS func (r *UserRepository) Migrations() fs.FS { return migrations } ``` The `Migrations()` method returns a filesystem containing SQL migration files. 3. Create SQL migration files ```sql -- 001_create_users_table.sql -- +migrate Up CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT UNIQUE NOT NULL ); -- +migrate Down DROP TABLE users; ``` Migration files use `-- +migrate Up` and `-- +migrate Down` markers to define schema changes. 4. Register your repository ```go userRepo := NewUserRepository(db.Connection()) db.RegisterRepository("users", userRepo) ``` Repositories that implement the `Migrations()` method returning `fs.FS` will automatically be registered for migration. 5. Run migrations ```go err = db.Migrate(ctx) if err != nil { log.ErrorContext(ctx, "failed to run migrations", "error", err) os.Exit(1) } ``` This applies all pending migrations and tracks them in the `platforma_migrations` table. 6. Use the database connection ```go var users []User err := db.Connection().SelectContext(ctx, &users, "SELECT id, name, email FROM users") ``` Access the underlying `sqlx.DB` connection through the `Connection()` method. ## Using with Application [Section titled “Using with Application”](#using-with-application) The `Database` type does not implement the `Runner` interface. Instead, it’s registered with the application using the dedicated database registration methods: ```go app := application.New() // Register database connection app.RegisterDatabase("main", db) // Register repositories with the database app.RegisterRepository("main", "users", userRepo) // Register services app.RegisterService("api", httpServer) // Run the application (starts services) app.Run(ctx) ``` Migrations are NOT run automatically when starting services. You must run migrations separately using the CLI: ```bash # Run migrations first myapp migrate # Then start services myapp run ``` If migrations fail, the application will exit with an error. ## Migration file format [Section titled “Migration file format”](#migration-file-format) Migration files use special markers to separate up and down SQL: ```sql -- +migrate Up CREATE TABLE users ( id SERIAL PRIMARY KEY, name TEXT NOT NULL ); -- +migrate Down DROP TABLE users; ``` | Marker | Required | Description | | ------------------ | -------- | ----------------------------------------- | | `-- +migrate Up` | Yes | Marks the start of the up migration SQL | | `-- +migrate Down` | No | Marks the start of the down migration SQL | The migration ID is derived from the filename without the `.sql` extension. For example, `001_create_users.sql` becomes ID `001_create_users`. ## Migration tracking [Section titled “Migration tracking”](#migration-tracking) Migrations are tracked in the `platforma_migrations` table with three columns: | Column | Description | | ------------ | ------------------------------------------ | | `repository` | Name used in `RegisterRepository` | | `id` | Migration ID derived from the SQL filename | | `timestamp` | When the migration was applied | If a migration fails, previously applied migrations in the same batch are reverted using their `Down` SQL. ## Complete example [Section titled “Complete example”](#complete-example) 001\_create\_users\_table.sql ```sql -- +migrate Up CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT UNIQUE NOT NULL ); -- +migrate Down DROP TABLE users; ``` main.go ```go package main import ( "context" "embed" "fmt" "io/fs" "os" "github.com/jmoiron/sqlx" "github.com/platforma-dev/platforma/database" "github.com/platforma-dev/platforma/log" ) // User represents a user in our system type User struct { ID int `db:"id"` Name string `db:"name"` Email string `db:"email"` } // UserRepository handles database operations for users type UserRepository struct { db *sqlx.DB } // NewUserRepository creates a new UserRepository with the given connection func NewUserRepository(db *sqlx.DB) *UserRepository { return &UserRepository{db: db} } //go:embed *.sql var migrations embed.FS func (r *UserRepository) Migrations() fs.FS { return migrations } // Create inserts a new user into the database func (r *UserRepository) Create(ctx context.Context, name, email string) (User, error) { var user User err := r.db.QueryRowxContext(ctx, "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id, name, email", name, email, ).StructScan(&user) return user, err } // GetAll retrieves all users from the database func (r *UserRepository) GetAll(ctx context.Context) ([]User, error) { var users []User err := r.db.SelectContext(ctx, &users, "SELECT id, name, email FROM users") return users, err } func main() { ctx := context.Background() // Get database connection string from environment variable // Example: "postgres://user:password@localhost:5432/mydb?sslmode=disable" connStr := os.Getenv("DATABASE_URL") if connStr == "" { log.ErrorContext(ctx, "DATABASE_URL environment variable is not set") os.Exit(1) } // Create new database connection db, err := database.New(connStr) if err != nil { log.ErrorContext(ctx, "failed to connect to database", "error", err) os.Exit(1) } log.InfoContext(ctx, "connected to database") // Create repository and register it with the database userRepo := NewUserRepository(db.Connection()) db.RegisterRepository("users", userRepo) // Run migrations err = db.Migrate(ctx) if err != nil { log.ErrorContext(ctx, "failed to run migrations", "error", err) os.Exit(1) } log.InfoContext(ctx, "migrations completed successfully") // Create a new user user, err := userRepo.Create(ctx, "John Doe", "john@example.com") if err != nil { log.ErrorContext(ctx, "failed to create user", "error", err) os.Exit(1) } log.InfoContext(ctx, "user created", "id", user.ID, "name", user.Name, "email", user.Email) // Get all users users, err := userRepo.GetAll(ctx) if err != nil { log.ErrorContext(ctx, "failed to get users", "error", err) os.Exit(1) } fmt.Printf("Found %d user(s):\n", len(users)) for _, u := range users { fmt.Printf(" - ID: %d, Name: %s, Email: %s\n", u.ID, u.Name, u.Email) } } ```
# httpserver
The `httpserver` package provides an HTTP server with middleware support, composable handler groups, and graceful shutdown. Core Components: * `HTTPServer`: HTTP server with middleware support and graceful shutdown. Implements `Runner` interface so it can be used as an `application` service. * `HandlerGroup`: Composable group of HTTP handlers that share common middlewares. Implements `http.Handler` for nesting. * `Middleware`: Interface for HTTP middleware with a `Wrap(http.Handler) http.Handler` method. * `MiddlewareFunc`: Function type that implements `Middleware` for inline middleware definitions. * `TraceIDMiddleware`: Adds a unique trace ID to request context and response headers. * `RecoverMiddleware`: Catches panics in handlers and returns HTTP 500 responses. * `FileServer`: Serves static files from an `fs.FS`. Implements `Runner` interface. [Full package docs at pkg.go.dev](https://pkg.go.dev/github.com/platforma-dev/platforma/httpserver) ## Step-by-step guide [Section titled “Step-by-step guide”](#step-by-step-guide) 1. Create a new HTTP server ```go server := httpserver.New("8080", 3*time.Second) ``` The first argument is the port, the second is the shutdown timeout for graceful shutdown. 2. Register handlers ```go server.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("pong")) }) server.Handle("/health", healthHandler) ``` Use `HandleFunc` for handler functions or `Handle` for `http.Handler` implementations. 3. Add middleware to the server ```go server.Use(httpserver.NewTraceIDMiddleware(nil, "")) server.Use(httpserver.NewRecoverMiddleware()) ``` Middlewares are applied in the order they are added. `TraceIDMiddleware` adds a trace ID to all requests, `RecoverMiddleware` catches panics. 4. Create a handler group for nested routes ```go apiGroup := httpserver.NewHandlerGroup() apiGroup.HandleFunc("/users", usersHandler) apiGroup.HandleFunc("/posts", postsHandler) ``` Handler groups let you organize routes with shared middleware. 5. Add middleware specific to the group ```go apiGroup.UseFunc(func(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.InfoContext(r.Context(), "API request", "path", r.URL.Path) h.ServeHTTP(w, r) }) }) ``` Use `UseFunc` to add inline middleware functions, or `Use` for `Middleware` implementations. 6. Mount the group on the server ```go server.HandleGroup("/api", apiGroup) ``` The group is now accessible at `/api/users` and `/api/posts`. The path prefix is automatically stripped. 7. Run the server ```go ctx := context.Background() if err := server.Run(ctx); err != nil { log.ErrorContext(ctx, "server error", "error", err) } ``` Expected output: ```plaintext time=2025-01-01T12:00:00.000+00:00 level=INFO msg="starting http server" address=:8080 ``` Press Ctrl+C to trigger graceful shutdown. The server waits for in-flight requests up to the shutdown timeout. ## Using with Application [Section titled “Using with Application”](#using-with-application) Since `HTTPServer` implements the `Runner` interface, it can be registered as a service in an `Application`: ```go app := application.New() server := httpserver.New("8080", 3*time.Second) server.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("pong")) }) app.RegisterService("api", server) app.Run(ctx) ``` The server also implements `Healthchecker`, so its health status is automatically tracked by the application. ## Built-in middlewares [Section titled “Built-in middlewares”](#built-in-middlewares) ### TraceIDMiddleware [Section titled “TraceIDMiddleware”](#traceidmiddleware) Adds a unique UUID trace ID to each request’s context and response headers. ```go // Uses defaults: log.TraceIDKey for context, "Platforma-Trace-Id" header server.Use(httpserver.NewTraceIDMiddleware(nil, "")) // Custom context key and header server.Use(httpserver.NewTraceIDMiddleware(myContextKey, "X-Request-ID")) ``` The trace ID is available in handlers via `r.Context().Value(log.TraceIDKey)`. ### RecoverMiddleware [Section titled “RecoverMiddleware”](#recovermiddleware) Catches panics in handlers, logs the error with request context, and returns HTTP 500. ```go server.Use(httpserver.NewRecoverMiddleware()) ``` ## FileServer [Section titled “FileServer”](#fileserver) Serves static files from an `fs.FS` implementation: ```go //go:embed static var staticFiles embed.FS fileServer := httpserver.NewFileServer(staticFiles, "/static", "8080") app.RegisterService("files", fileServer) ``` ## Complete example [Section titled “Complete example”](#complete-example) httpserver.go ```go package main import ( "context" "net/http" "time" "github.com/platforma-dev/platforma/application" "github.com/platforma-dev/platforma/httpserver" "github.com/platforma-dev/platforma/log" ) func main() { ctx := context.Background() // Initialize new application app := application.New() // Create HTTP server api := httpserver.New("8080", 3*time.Second) // Add /ping endpoint to `api` api.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("pong")) }) // Add /long endpoint to HTTP server to test graceful shutdown api.HandleFunc("/long", func(w http.ResponseWriter, r *http.Request) { time.Sleep(10 * time.Second) w.Write([]byte("pong")) }) // Add middleware to HTTP server. It will add trace ID to logs and responce headers api.Use(log.NewTraceIDMiddleware(nil, "")) // Create handler group subApiGroup := httpserver.NewHandlerGroup() // Add /clock endpoint to handler group subApiGroup.HandleFunc("/clock", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(time.Now().String())) }) // Add middleware to HTTP server. It will log all incoming requests to this handle group subApiGroup.UseFunc(func(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.InfoContext(r.Context(), "incoming request", "addr", r.RemoteAddr) h.ServeHTTP(w, r) }) }) // Add handle group to HTTP server with /subApi path api.HandleGroup("/subApi", subApiGroup) // Register HTTP server as application server app.RegisterService("api", api) // Run application if err := app.Run(ctx); err != nil { log.ErrorContext(ctx, "app finished with error", "error", err) } // Now you can access http://localhost:8080/ping, http://localhost:8080/long // and http://localhost:8080/subApi/clock URLs with GET method } ```
# log
The `log` package provides structured application logs, HTTP trace propagation, and request-wide event logging with tail sampling. Core Components: * `Logger`, `SetDefault`, `Debug`/`Info`/`Warn`/`Error`: Package-level logging API built on top of `slog`. * `New`: Builds a text or JSON logger that automatically extracts values like `traceId` and `serviceName` from `context.Context`. * `TraceIDMiddleware`: Adds a per-request trace ID to context and response headers. * `Event`: Mutable wide-event model with attrs, steps, errors, severity, and duration. * `WideEventLogger`: Writes finalized `Event` values through a `Sampler`. * `WideEventMiddleware`: Creates request-wide events, stores them in context, and emits them after handlers finish. * `Sampler`, `SamplerFunc`, `DefaultSampler`: Tail-sampling rules for keeping errors, slow requests, selected status codes, and random samples. * `EventFromContext`: Fetches the current request-wide event from context using `WideEventKey`. [Full package docs at pkg.go.dev](https://pkg.go.dev/github.com/platforma-dev/platforma/log) ## Step-by-step guide [Section titled “Step-by-step guide”](#step-by-step-guide) 1. Configure default structured logging ```go logger := log.New(os.Stdout, "json", slog.LevelInfo, nil) log.SetDefault(logger) ``` This switches package-level logging to JSON and keeps context extraction enabled. 2. Add trace IDs to each request ```go server := httpserver.New("8080", 3*time.Second) server.Use(log.NewTraceIDMiddleware(nil, "")) ``` With default arguments, the middleware stores IDs under `log.TraceIDKey` and writes `Platforma-Trace-Id` response headers. 3. Configure wide-event logging with sampling ```go wideLogger := log.NewWideEventLogger( os.Stdout, log.NewDefaultSampler(2*time.Second, 500, 0.05), "json", nil, ) server.Use(log.NewWideEventMiddleware(wideLogger, "", nil)) ``` This keeps all error events, slow requests (>=2s), `5xx` responses, and 5% of the remaining traffic. 4. Enrich events and logs inside handlers ```go server.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) { log.InfoContext(r.Context(), "users request started") if ev := log.EventFromContext(r.Context()); ev != nil { ev.AddStep(slog.LevelInfo, "query users table") ev.AddAttrs(map[string]any{"users.limit": 50}) } w.WriteHeader(http.StatusOK) }) ``` `InfoContext` includes request metadata (for example `traceId`), and `EventFromContext` lets handlers attach detailed wide-event data. 5. Verify runtime output ```json {"level":"INFO","msg":"users request started","traceId":"14b3..."} {"level":"INFO","name":"http.request","duration":"12ms","request.status":200} ``` The first line is an immediate log entry. The second line is the finalized request-wide event emitted after the response is completed. ## Using with Application [Section titled “Using with Application”](#using-with-application) Integrate logging by registering an HTTP service and attaching `log` middlewares before `app.Run`: ```go app := application.New() server := httpserver.New("8080", 3*time.Second) server.Use(log.NewTraceIDMiddleware(nil, "")) wideLogger := log.NewWideEventLogger( os.Stdout, log.NewDefaultSampler(2*time.Second, 500, 0.05), "json", nil, ) server.Use(log.NewWideEventMiddleware(wideLogger, "", nil)) server.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { log.InfoContext(r.Context(), "health check") w.WriteHeader(http.StatusOK) }) app.RegisterService("api", server) app.Run(ctx) ``` ## Complete example [Section titled “Complete example”](#complete-example) wide-events.go ```go package main import ( "context" "errors" "log/slog" "os" "time" "github.com/platforma-dev/platforma/log" ) func main() { logger := log.NewWideEventLogger( os.Stdout, log.NewDefaultSampler(3*time.Second, 200, 0.1), "json", nil, ) ev := log.NewEvent("test_event") ev.AddStep(slog.LevelInfo, "some step") ev.AddError(errors.New("some error")) ev.AddAttrs(map[string]any{ "attr1": 1, "attr2": true, }) logger.WriteEvent(context.Background(), ev) } ```
# queue
The `queue` package provides a generic, concurrent job processing system with worker pools and graceful shutdown. Core Components: * `Processor[T]`: Manages a pool of workers to process jobs from a queue. Implements `Runner` interface so it can be used as an `application` service. * `Handler[T]`: Interface for processing jobs with a `Handle(ctx context.Context, job T)` method. * `HandlerFunc[T]`: Function type that implements `Handler` for inline handler definitions. * `Provider[T]`: Interface for queue implementations, allowing custom backends. * `ChanQueue[T]`: Built-in thread-safe channel-based queue implementation. * `ErrTimeout`: Error returned when an enqueue operation times out. * `ErrClosedQueue`: Error returned when attempting to operate on a closed queue. [Full package docs at pkg.go.dev](https://pkg.go.dev/github.com/platforma-dev/platforma/queue) ## Step-by-step guide [Section titled “Step-by-step guide”](#step-by-step-guide) 1. Define your job type ```go type job struct { data int } ``` Jobs can be any type. Use a struct for complex payloads. 2. Create a handler function ```go func jobHandler(ctx context.Context, job job) { log.InfoContext(ctx, "job handled", "data", job.data) } ``` The handler receives the job and a context with worker ID for logging. 3. Create a queue ```go q := queue.NewChanQueue[job](10, 3*time.Second) ``` First argument is the buffer size, second is the enqueue timeout. When the buffer is full, enqueue blocks until timeout. 4. Create a processor ```go p := queue.New(queue.HandlerFunc[job](jobHandler), q, 2, time.Second) ``` Arguments: handler, queue provider, number of workers, shutdown timeout. The shutdown timeout allows workers to drain remaining jobs on shutdown. 5. Start the processor ```go go p.Run(ctx) ``` Run blocks until context is cancelled and all workers complete. Use a goroutine for non-blocking operation. Expected output: ```plaintext time=2025-01-01T12:00:00.000+00:00 level=INFO msg="worker started" workerID=abc-123 time=2025-01-01T12:00:00.000+00:00 level=INFO msg="worker started" workerID=def-456 ``` 6. Enqueue jobs ```go p.Enqueue(ctx, job{data: 1}) p.Enqueue(ctx, job{data: 2}) ``` Jobs are distributed to available workers. Enqueue returns an error if the queue is full (timeout) or closed. Expected output: ```plaintext time=2025-01-01T12:00:00.001+00:00 level=INFO msg="job handled" workerID=abc-123 data=1 time=2025-01-01T12:00:00.001+00:00 level=INFO msg="job handled" workerID=def-456 data=2 ``` ## Using with Application [Section titled “Using with Application”](#using-with-application) Since `Processor` implements the `Runner` interface, it can be registered as a service in an `Application`: ```go app := application.New() q := queue.NewChanQueue[job](100, 5*time.Second) p := queue.New(queue.HandlerFunc[job](jobHandler), q, 4, 10*time.Second) app.RegisterService("queue", p) app.Run(ctx) ``` The processor starts when the application runs and gracefully shuts down with it. ## Custom queue providers [Section titled “Custom queue providers”](#custom-queue-providers) Implement the `Provider` interface to use custom queue backends like Redis, RabbitMQ, or databases: ```go type Provider[T any] interface { Open(ctx context.Context) error Close(ctx context.Context) error EnqueueJob(ctx context.Context, job T) error GetJobChan(ctx context.Context) (chan T, error) } ``` The `Open` and `Close` methods handle connection lifecycle. `GetJobChan` returns the channel that workers read from. ## Error handling [Section titled “Error handling”](#error-handling) The package provides two error types for queue operations: | Error | Condition | | ---------------- | ----------------------------------------- | | `ErrTimeout` | Enqueue operation timed out (buffer full) | | `ErrClosedQueue` | Attempted operation on a closed queue | Workers recover from panics automatically and log the error without crashing the processor. ## Complete example [Section titled “Complete example”](#complete-example) queue.go ```go package main import ( "context" "time" "github.com/platforma-dev/platforma/log" "github.com/platforma-dev/platforma/queue" ) type job struct { data int } func jobHandler(ctx context.Context, job job) { log.InfoContext(ctx, "job handled", "data", job.data) } func main() { ctx := context.Background() q := queue.NewChanQueue[job](10, 3*time.Second) p := queue.New(queue.HandlerFunc[job](jobHandler), q, 2, time.Second) go p.Run(ctx) time.Sleep(time.Millisecond) p.Enqueue(ctx, job{data: 1}) p.Enqueue(ctx, job{data: 2}) p.Enqueue(ctx, job{data: 3}) time.Sleep(time.Millisecond) } ```
# scheduler
The `scheduler` package provides periodic task execution at fixed intervals. Core Components: * `Scheduler`: Executes a runner at configured intervals. Implements `Runner` interface so it can be used as an `application` service. * `New(period, runner)`: Creates a new scheduler with the specified interval and runner. [Full package docs at pkg.go.dev](https://pkg.go.dev/github.com/platforma-dev/platforma/scheduler) ## Step-by-step guide [Section titled “Step-by-step guide”](#step-by-step-guide) 1. Create a task function ```go func scheduledTask(ctx context.Context) error { log.InfoContext(ctx, "scheduled task executed") return nil } ``` The function receives a context with a unique trace ID for each execution. 2. Create a scheduler ```go s := scheduler.New(time.Second, application.RunnerFunc(scheduledTask)) ``` First argument is the interval between executions. Second is any `application.Runner` implementation. Use `application.RunnerFunc` to wrap a function. 3. Run the scheduler ```go err := s.Run(ctx) ``` The scheduler blocks and executes the task at each interval until the context is cancelled. Expected output: ```plaintext time=2025-01-01T12:00:01.000+00:00 level=INFO msg="scheduler task started" traceID=abc-123 time=2025-01-01T12:00:01.000+00:00 level=INFO msg="scheduled task executed" traceID=abc-123 time=2025-01-01T12:00:01.000+00:00 level=INFO msg="scheduler task finished" traceID=abc-123 time=2025-01-01T12:00:02.000+00:00 level=INFO msg="scheduler task started" traceID=def-456 time=2025-01-01T12:00:02.000+00:00 level=INFO msg="scheduled task executed" traceID=def-456 time=2025-01-01T12:00:02.000+00:00 level=INFO msg="scheduler task finished" traceID=def-456 ``` 4. Handle errors ```go func scheduledTask(ctx context.Context) error { if err := doWork(); err != nil { return err // Logged automatically, scheduler continues } return nil } ``` Errors returned from the task are logged but do not stop the scheduler. The next execution proceeds as normal. ## Using with Application [Section titled “Using with Application”](#using-with-application) Since `Scheduler` implements the `Runner` interface, it can be registered as a service in an `Application`: ```go app := application.New() s := scheduler.New(time.Minute, application.RunnerFunc(scheduledTask)) app.RegisterService("scheduler", s) app.Run(ctx) ``` The scheduler starts when the application runs and stops when the application shuts down. ## Complete example [Section titled “Complete example”](#complete-example) scheduler.go ```go package main import ( "context" "time" "github.com/platforma-dev/platforma/application" "github.com/platforma-dev/platforma/log" "github.com/platforma-dev/platforma/scheduler" ) func scheduledTask(ctx context.Context) error { log.InfoContext(ctx, "scheduled task executed") return nil } func main() { ctx, cancel := context.WithCancel(context.Background()) s := scheduler.New(time.Second, application.RunnerFunc(scheduledTask)) go func() { time.Sleep(3500 * time.Millisecond) cancel() }() s.Run(ctx) } ```