MCPcopy Index your code
hub / github.com/acronis/go-appkit

github.com/acronis/go-appkit @v1.31.0

Chat with this repo
repository ↗ · DeepWiki ↗ · release v1.31.0 ↗ · + Follow
1,915 symbols 8,484 edges 201 files 981 documented · 51%
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

Common Go packages for writing applications, services, and tools

GoDoc Widget

The project includes the following packages:

  • config - loading configuration from environment variables, files, and io.Reader. YAML and JSON formats are supported out of the box.
  • grpcserver - gRPC server with configuration from YAML/JSON files, built-in interceptors, and observability features.
  • grpcserver/interceptor - collection of gRPC interceptors for logging, metrics collection, panic recovery, request-id tracing, rate and in-flight request limiting, etc.
  • grpcserver/interceptor/throttle - ready-to-use interceptors for gRPC throttling that can be flexibly configured via JSON or YAML.
  • httpclient - helpers and a set of http.RoundTripper implementations for simplifying typical HTTP client operations (e.g. retries, client-side throttling, setting any header for each request, etc.).
  • httpserver - configurable HTTP server (wrapper around http.Server) that includes graceful shutdown support, panic recovery, metrics collection, and logging.
  • httpserver/middleware - collection of middlewares for HTTP server (e.g. request logging, metrics collection, panic recovery, rate and in-flight request limiting, request-id tracing, etc.).
  • httpserver/middleware/throttle - ready-to-use middleware for server-side throttling that can be flexibly configured via JSON or YAML.
  • log - unified interface for structured logging with included configurable adapter for tiny, fast, and memory-efficient (zero-allocation) logf logger.
  • lrucache - in-memory LRU cache with collecting Prometheus metrics.
  • netutil - utilities for working with network.
  • profserver - profiling HTTP server (pprof).
  • restapi - set of simple functions for doing requests and sending responses in the REST API.
  • retry - helper functions for doing retryable operations.
  • service - ready-to-use primitives for creating services and managing their lifecycle.
  • testutil - helpers for writing tests.

Installation

go get -u github.com/acronis/go-appkit

Examples

Simple service that provides HTTP API

The following example demonstrates how to create a simple service with + Configuration loading from environment variables and a yaml file. + HTTP server with request logging, metrics collection, primitive tracing and versioned API endpoints. + Profiling server (pprof) for debugging purposes running on a separate port. + Graceful shutdown on SIGTERM and SIGINT signals.

package main

import (
    "fmt"
    golog "log"
    "net/http"

    "github.com/go-chi/chi/v5"

    "github.com/acronis/go-appkit/config"
    "github.com/acronis/go-appkit/httpserver"
    "github.com/acronis/go-appkit/httpserver/middleware"
    "github.com/acronis/go-appkit/log"
    "github.com/acronis/go-appkit/profserver"
    "github.com/acronis/go-appkit/restapi"
    "github.com/acronis/go-appkit/service"
)

func main() {
    if err := runApp(); err != nil {
        golog.Fatal(err)
    }
}

func runApp() error {
    cfg, err := loadConfigFromFile("config.yaml")
    if err != nil {
        return fmt.Errorf("load config: %w", err)
    }

    logger, closeFn := log.NewLogger(cfg.Log)
    defer closeFn()

    var serviceUnits []service.Unit

    // Create HTTP server that provides /healthz, /metrics, and /api/{service-name}/v{number}/* endpoints.
    httpServer, err := makeHTTPServer(cfg.Server, logger)
    if err != nil {
        return err
    }
    serviceUnits = append(serviceUnits, httpServer)

    if cfg.ProfServer.Enabled {
        // Create HTTP server for profiling (pprof is used under the hood).
        serviceUnits = append(serviceUnits, profserver.New(cfg.ProfServer, logger))
    }

    return service.New(logger, service.NewCompositeUnit(serviceUnits...)).Start()
}

func makeHTTPServer(cfg *httpserver.Config, logger log.FieldLogger) (*httpserver.HTTPServer, error) {
    const errorDomain = "MyService" // Error domain is useful for distinguishing errors from different services (e.g. proxies).

    apiRoutes := map[httpserver.APIVersion]httpserver.APIRoute{
        1: func(router chi.Router) {
            router.Get("/hello", v1HelloHandler())
        },
        2: func(router chi.Router) {
            router.Get("/hi", v2HiHandler(errorDomain))
        },
    }

    opts := httpserver.Opts{
        ServiceNameInURL: "my-service",
        ErrorDomain:      errorDomain,
        APIRoutes:        apiRoutes,
        HealthCheck: func() (httpserver.HealthCheckResult, error) {
            // 503 status code will be returned if any of the components is unhealthy.
            return map[httpserver.HealthCheckComponentName]httpserver.HealthCheckStatus{
                "component-a": httpserver.HealthCheckStatusOK,
                "component-b": httpserver.HealthCheckStatusOK,
            }, nil
        },
    }

    httpServer, err := httpserver.New(cfg, logger, opts)
    if err != nil {
        return nil, err
    }

    // Custom routes can be added using chi.Router directly.
    httpServer.HTTPRouter.Get("/custom-route", customRouteHandler)

    return httpServer, nil
}

func loadConfigFromFile(filePath string) (*AppConfig, error) {
    cfg := NewAppConfig()
    err := config.NewDefaultLoader("my_service").LoadFromFile(filePath, config.DataTypeYAML, cfg)
    return cfg, err
}

func v1HelloHandler() func(rw http.ResponseWriter, r *http.Request) {
    return func(rw http.ResponseWriter, r *http.Request) {
        logger := middleware.GetLoggerFromContext(r.Context())
        restapi.RespondJSON(rw, map[string]string{"message": "Hello from v1"}, logger)
    }
}

func v2HiHandler(errorDomain string) func(rw http.ResponseWriter, r *http.Request) {
    return func(rw http.ResponseWriter, r *http.Request) {
        logger := middleware.GetLoggerFromContext(r.Context())
        name := r.URL.Query().Get("name")
        if len(name) < 3 {
            apiErr := restapi.NewError(errorDomain, "invalidName", "Name must be at least 3 characters long")
            restapi.RespondError(rw, http.StatusBadRequest, apiErr, middleware.GetLoggerFromContext(r.Context()))
            return
        }
        restapi.RespondJSON(rw, map[string]string{"message": fmt.Sprintf("Hi %s from v2", name)}, logger)
    }
}

func customRouteHandler(rw http.ResponseWriter, r *http.Request) {
    logger := middleware.GetLoggerFromContext(r.Context())
    if _, err := rw.Write([]byte("Content from the custom route")); err != nil {
        logger.Error("error while writing response body", log.Error(err))
    }
}

type AppConfig struct {
    Server     *httpserver.Config
    ProfServer *profserver.Config
    Log        *log.Config
}

func NewAppConfig() *AppConfig {
    return &AppConfig{
        Server:     httpserver.NewConfig(),
        ProfServer: profserver.NewConfig(),
        Log:        log.NewConfig(),
    }
}

func (c *AppConfig) SetProviderDefaults(dp config.DataProvider) {
    config.CallSetProviderDefaultsForFields(c, dp)
}

func (c *AppConfig) Set(dp config.DataProvider) error {
    return config.CallSetForFields(c, dp)
}

Configuration file config.yaml:

server:
  address: ":8888"
  timeouts:
    write: 1m
    read: 15s
    readHeader: 10s
    idle: 1m
    shutdown: 5s
  limits:
    maxBodySize: 1M
  log:
    requestStart: true
profServer:
  enabled: true
  address: ":8889"
log:
  level: info
  format: json
  output: stdout

Run the service:

$ go run main.go

Check the service API:

$ curl -w "\nHTTP code: %{http_code}\n" localhost:8888/api/my-service/v1/hello                                                                                                                                                               [130]
{"message":"Hello"}
HTTP code: 200

$ curl -w "\nHTTP code: %{http_code}\n" 'localhost:8888/api/my-service/v2/hi?name='
{"error":{"domain":"MyService","code":"invalidName","message":"Name must be at least 3 characters long"}}
HTTP code: 400

$ curl -w "\nHTTP code: %{http_code}\n" 'localhost:8888/api/my-service/v2/hi?name=Alice'
{"message":"Hi Alice"}
HTTP code: 200

Check the service health and metrics:


$ curl -w "\nHTTP code: %{http_code}\n" localhost:8888/healthz
{"components":{"component-a":true,"component-b":true}}
HTTP code: 200

$ curl localhost:8888/metrics
...
# HELP http_request_duration_seconds A histogram of the HTTP request durations.
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{method="GET",route_pattern="/api/my-service/v1/hello",status_code="200",user_agent_type="http-client",le="0.01"} 1
...
http_request_duration_seconds_bucket{method="GET",route_pattern="/api/my-service/v1/hello",status_code="200",user_agent_type="http-client",le="600"} 1
http_request_duration_seconds_bucket{method="GET",route_pattern="/api/my-service/v1/hello",status_code="200",user_agent_type="http-client",le="+Inf"} 1
http_request_duration_seconds_sum{method="GET",route_pattern="/api/my-service/v1/hello",status_code="200",user_agent_type="http-client"} 0.000184125
http_request_duration_seconds_count{method="GET",route_pattern="/api/my-service/v1/hello",status_code="200",user_agent_type="http-client"} 1
...
http_request_duration_seconds_bucket{method="GET",route_pattern="/api/my-service/v2/hi",status_code="400",user_agent_type="http-client",le="0.01"} 1
...
http_request_duration_seconds_bucket{method="GET",route_pattern="/api/my-service/v2/hi",status_code="400",user_agent_type="http-client",le="600"} 1
http_request_duration_seconds_bucket{method="GET",route_pattern="/api/my-service/v2/hi",status_code="400",user_agent_type="http-client",le="+Inf"} 1
http_request_duration_seconds_sum{method="GET",route_pattern="/api/my-service/v2/hi",status_code="400",user_agent_type="http-client"} 0.000326041
http_request_duration_seconds_count{method="GET",route_pattern="/api/my-service/v2/hi",status_code="400",user_agent_type="http-client"} 1
...
# HELP http_requests_in_flight Current number of HTTP requests being served.
# TYPE http_requests_in_flight gauge
http_requests_in_flight{method="GET",route_pattern="/api/my-service/v1/hello",user_agent_type="http-client"} 0
http_requests_in_flight{method="GET",route_pattern="/api/my-service/v2/hi",user_agent_type="http-client"} 0
...

Service logs:

{"level":"info","time":"2024-06-04T20:22:01.862351+03:00","msg":"starting application HTTP server...","pid":8455,"address":":8888","write_timeout":"1m0s","read_timeout":"15s","read_header_timeout":"10s","idle_timeout":"1m0s","shutdown_timeout":"
5s"}
{"level":"info","time":"2024-06-04T20:22:01.862516+03:00","msg":"starting profiling HTTP server...","pid":8455,"address":":8889"}
...
{"level":"info","time":"2024-06-04T20:22:08.075376+03:00","msg":"request started","pid":8455,"request_id":"cpfkqg3juspi21pmber0","int_request_id":"cpfkqg3juspi21pmberg","trace_id":"","method":"GET","uri":"/api/my-service/v1/hello","remote_addr":"[::1]:59994","content_length":0,"user_agent":"curl/8.6.0","remote_addr_ip":"::1","remote_addr_port":59994}
{"level":"info","time":"2024-06-04T20:22:08.075518+03:00","msg":"response completed in 0.000s","pid":8455,"request_id":"cpfkqg3juspi21pmber0","int_request_id":"cpfkqg3juspi21pmberg","trace_id":"","method":"GET","uri":"/api/my-service/v1/hello","remote_addr":"[::1]:59994","content_length":0,"user_agent":"curl/8.6.0","remote_addr_ip":"::1","remote_addr_port":59994,"duration_ms":0,"duration":184,"status":200,"bytes_sent":19}
...
{"level":"info","time":"2024-06-04T20:31:14.002993+03:00","msg":"service got signal","pid":8455,"signal":"interrupt"}
{"level":"info","time":"2024-06-04T20:31:14.003051+03:00","msg":"closing profiling HTTP server...","pid":8455}
{"level":"info","time":"2024-06-04T20:31:14.00321+03:00","msg":"profiling HTTP served closed","pid":8455,"address":":8889"}
{"level":"info","time":"2024-06-04T20:31:14.003254+03:00","msg":"shutting down application HTTP server...","pid":8455,"timeout":"5s"}
{"level":"info","time":"2024-06-04T20:31:14.003365+03:00","msg":"application HTTP server closed","pid":8455,"address":":8888","write_timeout":"1m0s","read_timeout":"15s","read_header_timeout":"10s","idle_timeout":"1m0s","shutdown_timeout":"5s"}
{"level":"info","time":"2024-06-04T20:31:14.003412+03:00","msg":"application HTTP server shut down","pid":8455}

License

Copyright © 2024 Acronis International GmbH.

Licensed under MIT License.

Extension points exported contracts — how you extend this code

Unit (Interface)
Unit represents a service unit that can be started and stopped. Each Unit is a distinct component within a service, with [7 …
service/unit.go
Config (Interface)
Config is a common interface for configuration objects that may be used by Loader. [19 implementers]
config/config.go
RequestHandler (Interface)
RequestHandler abstracts the common operations for both HTTP and gRPC requests. [4 implementers]
internal/ratelimit/request_processor.go
RequestHandler (Interface)
RequestHandler abstracts the common operations for both HTTP and gRPC requests. [4 implementers]
internal/inflightlimit/request_processor.go
FieldLogger (Interface)
FieldLogger is an interface for loggers which writes logs in structured format. [3 implementers]
log/logger.go
Policy (Interface)
Policy defines backoff strategy. [2 implementers]
retry/retry.go
DataProvider (Interface)
DataProvider is an interface for providing configuration data from different sources (files, reader, environment variabl [2 …
config/data_provider.go
AuthProvider (Interface)
AuthProvider provides auth information used for bearer authorization. [2 implementers]
httpclient/auth_bearer_round_tripper.go

Core symbols most depended-on inside this repo

Error
called by 310
log/logger.go
Run
called by 260
service/worker.go
Errorf
called by 204
log/logger.go
Get
called by 168
config/data_provider.go
NewRecorder
called by 126
log/logtest/recorder.go
Context
called by 118
grpcserver/interceptor/context.go
UnaryCall
called by 117
grpcserver/interceptor/throttle/interceptor_test.go
StreamingOutputCall
called by 113
grpcserver/interceptor/throttle/interceptor_test.go

Shape

Method 794
Function 716
Struct 290
FuncType 62
Interface 29
TypeAlias 24

Languages

Go100%

Modules by API surface

grpcserver/interceptor/rate_limit_interceptor.go57 symbols
grpcserver/interceptor/in_flight_limit_interceptor.go54 symbols
grpcserver/interceptor/throttle/interceptor_test.go45 symbols
internal/throttleconfig/config.go35 symbols
grpcserver/interceptor/metrics_interceptor.go35 symbols
log/logger.go33 symbols
httpserver/middleware/wrap_writer.go32 symbols
grpcserver/examples/echo-service/pb/echo_service_grpc.pb.go32 symbols
config/data_provider.go30 symbols
restapi/routing.go29 symbols
grpcserver/interceptor/rate_limit_interceptor_test.go28 symbols
grpcserver/interceptor/metrics_interceptor_test.go28 symbols

For agents

$ claude mcp add go-appkit \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact