ambatukam

package module
v1.2.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 25, 2026 License: MIT Imports: 21 Imported by: 0

README

Ambatukam Go

Ambatukam Go

Your Go HTTP client just got superpowers.
One line of code. Zero dependencies. Production-grade resilience.

CI Go Reference Go Report Card Documentation License Stars

Stop writing retry loops. Stop handling timeouts manually.
Stop panicking when Stripe goes down at 3 AM.
Ambatukam Go handles all of it — automatically.


😤 The Problem

Every Go backend that calls external services ends up like this:

resp, err := http.Get("https://api.stripe.com/charges")
if err != nil {
    // retry? how many times? with what backoff?
    // what if it keeps failing? circuit break?
    // what about rate limits? timeouts?
    // TODO: fix this later (you never will)
}

You end up stitching together 3-5 libraries, writing glue code nobody owns, and debugging resilience bugs at 3 AM.

😎 The Solution

client := ambatukam.New(
    ambatukam.WithRetry(ambatukam.RetryConfig{MaxRetries: 3}),
    ambatukam.WithCircuitBreaker(ambatukam.CircuitConfig{FailureThreshold: 5}),
    ambatukam.WithTimeout(ambatukam.TimeoutConfig{Timeout: 2 * time.Second}),
)
resp, err := client.Get(ctx, "https://api.stripe.com/charges")

10 lines. Zero dependencies. Production-grade.


⚡ Why Ambatukam Go?

Feature Ambatukam Go Other Libraries
Retry with backoff + jitter ✅ Built-in Need cenkalti/backoff
Circuit breaker ✅ Built-in Need sony/gobreaker
Bulkhead (FIFO queue) ✅ Built-in DIY or slok/goresilience
Rate limiter (lock-free) ✅ Built-in Need golang.org/x/time/rate
Per-attempt timeout ✅ Built-in Manual
Per-URL timeout map ✅ Built-in DIY
Fallback strategy ✅ Built-in DIY
Singleflight (body-aware) ✅ Built-in Need golang.org/x/sync
Health check endpoint ✅ Built-in DIY
Prometheus metrics ✅ Built-in DIY
Custom logger (zerolog/zap) ✅ Built-in Manual
Body buffering for POST retry ✅ Automatic Often broken
Retry-After header ✅ Automatic Most skip
Generic JSON helpers ✅ Built-in DIY
Request ID propagation ✅ Built-in DIY
Hooks (auth, logging, metrics) ✅ 5 callbacks Varies
Composable policies Chain() Manual
Max body size limit WithMaxBodySize DIY
Response cache WithCache Need patrickmn/go-cache
Adaptive timeout WithAdaptiveTimeout DIY
Retry budget WithRetryBudget DIY
Interceptors WithInterceptor Manual
Priority bulkhead WithBulkhead(Priority:true) DIY
Request logging WithRequestLog Manual
Client stats client.Stats() DIY
Zero dependencies None 3-5 deps

🚀 Install

go get github.com/farhanturu/ambatukam-go

Requires Go 1.21+. Zero external dependencies.


🎯 Quick Start

package main

import (
    "context"
    "fmt"
    "log"
    "time"

    "github.com/farhanturu/ambatukam-go"
)

func main() {
    client := ambatukam.New(
        ambatukam.WithTimeout(ambatukam.TimeoutConfig{Timeout: 2 * time.Second}),
        ambatukam.WithRetry(ambatukam.RetryConfig{MaxRetries: 3}),
        ambatukam.WithCircuitBreaker(ambatukam.CircuitConfig{FailureThreshold: 5}),
        ambatukam.WithBulkhead(ambatukam.BulkheadConfig{MaxConcurrent: 10}),
        ambatukam.WithRateLimit(ambatukam.RateLimitConfig{Rate: 10, Burst: 5}),
    )
    defer client.Close()

    resp, err := client.Get(context.Background(), "https://api.example.com/users")
    if err != nil {
        log.Fatal(err)
    }
    defer resp.Body.Close()
    fmt.Println("status:", resp.StatusCode)
}

🏗️ Architecture

┌──────────────────────────────────────────────────────────┐
│                      Client.Do()                         │
│                                                          │
│   ┌────────┐  ┌──────────┐  ┌────────┐  ┌──────────┐   │
│   │ Retry  │→│ Circuit   │→│Bulkhead│→│Rate Limit│   │
│   │        │  │ Breaker   │  │(FIFO)  │  │(chan)    │   │
│   └────────┘  └──────────┘  └────────┘  └──────────┘   │
│        ↓           ↓             ↓            ↓         │
│   ┌────────┐  ┌──────────┐  ┌──────────┐               │
│   │Fallback│  │Singleflight│ │  Timeout │               │
│   │        │  │(body-aware)│ │          │               │
│   └────────┘  └──────────┘  └──────────┘               │
│        ↓           ↓             ↓                      │
│   ┌──────────────────────────────────────────────────┐  │
│   │  Request ID · HooksPolicy · Metrics              │  │
│   └──────────────────────────────────────────────────┘  │
│                         ↓                                │
│                   http.Client.Do()                       │
└──────────────────────────────────────────────────────────┘

Policies are composable middleware — outer-to-inner order: retry → circuit → bulkhead → rate limit → fallback → singleflight → timeout → hooks → HTTP.


🎨 Features

🔄 Retry with Backoff

ambatukam.WithRetry(ambatukam.RetryConfig{
    MaxRetries:     3,
    InitialBackoff: 100 * time.Millisecond,
    MaxBackoff:     5 * time.Second,
    Multiplier:     2.0,
    Jitter:         0.2,
})

Three strategies: ExponentialBackoff, ConstantBackoff, LinearBackoff.

Body buffering is automatic — POST bodies are read once and replayed on each retry. Only idempotent methods retry by default; opt in for POST with custom ShouldRetry.

⚡ Circuit Breaker

ambatukam.WithCircuitBreaker(ambatukam.CircuitConfig{
    FailureThreshold: 5,
    OpenDuration:     30 * time.Second,
    HalfOpenMaxReqs:  1,
})

Three-state machine: closed → open → half-open. Race-safe with sync.RWMutex and generation counter for stale probe protection.

🚧 Bulkhead (FIFO)

ambatukam.WithBulkhead(ambatukam.BulkheadConfig{
    MaxConcurrent: 10,
    MaxQueue:      100,
    QueueTimeout:  50 * time.Millisecond,
})

Worker pool with proper FIFO ordering. MaxQueue=0 for fail-fast mode. Graceful shutdown via client.Close().

🚦 Rate Limiter (Lock-Free)

ambatukam.WithRateLimit(ambatukam.RateLimitConfig{
    Rate:        10,
    Burst:       5,
    WaitTimeout: 100 * time.Millisecond,
})

Channel-based token bucket — no mutex contention under high concurrency. Rate == 0 disables the limiter (pass-through), Rate < 0 denies all requests.

⏱️ Timeout

ambatukam.WithTimeout(ambatukam.TimeoutConfig{Timeout: 2 * time.Second})

Per-attempt deadline. Parent ctx cancellation takes precedence.

⏱️ Timeout Map (Per-URL)

ambatukam.WithTimeoutMap(map[string]time.Duration{
    "/api/payments/*":  10 * time.Second,
    "/api/users/*/profile": 5 * time.Second,
    "/api/**":          2 * time.Second,
})

Different timeouts for different URL patterns. Supports * (single segment) and ** (multi segment) wildcards.

🛟 Fallback

ambatukam.WithFallback(ambatukam.FallbackConfig{
    Handler: func(req *http.Request, err error) (*http.Response, error) {
        return cachedResponse, nil
    },
})

Return custom response when everything fails. Propagates attempt count from upstream retry errors.

🔗 Singleflight (Body-Aware)

ambatukam.WithSingleflight()

Deduplicate identical concurrent requests. 10 goroutines requesting the same data = 1 HTTP call.

The dedup key is method + URL for idempotent methods (GET, HEAD, OPTIONS, DELETE), and method + URL + sha256(body) for methods with a payload (POST, PUT, PATCH). Requests with different bodies are never merged.

📏 Max Body Size

ambatukam.WithMaxBodySize(10 << 20) // 10MB

Limits request body buffering. Bodies exceeding the limit skip singleflight dedup and return an error in retry. Prevents OOM from large request bodies.

💾 Response Cache

ambatukam.WithCache(ambatukam.CacheConfig{
    TTL:        5 * time.Minute,
    MaxEntries: 1000,
    Methods:    []string{"GET", "HEAD"},
})

Built-in HTTP response cache with TTL and LRU eviction. Cache hits skip the entire middleware chain.

⏱️ Adaptive Timeout

ambatukam.WithAdaptiveTimeout(ambatukam.AdaptiveTimeoutConfig{
    Initial:    5 * time.Second,
    Percentile: 99,
    Window:     5 * time.Minute,
})

Timeout adjusts automatically based on p99 latency history. No manual tuning needed.

💰 Retry Budget

ambatukam.WithRetryBudget(0.1, 10*time.Second) // max 10% retries

Limits total retry ratio within a time window. Prevents cascade failures during outages.

🔌 Interceptor

ambatukam.WithInterceptor(func(req *http.Request, next ambatukam.PolicyFunc) (*http.Response, error) {
    req.Header.Set("X-Custom", "value")
    return next(req.Context(), req)
})

Flexible request/response middleware. Can transform, log, auth, or short-circuit requests.

🚦 Priority Bulkhead

ambatukam.WithBulkhead(ambatukam.BulkheadConfig{
    MaxConcurrent: 10,
    MaxQueue:      100,
    Priority:      true,
})
// Mark request as high-priority:
ctx := ambatukam.WithPriority(context.Background())
resp, err := client.Get(ctx, url)

High-priority requests jump the queue. Use ambatukam.WithPriority(ctx) to mark requests.

📝 Request Logging

ambatukam.WithRequestLog(ambatukam.RequestLogConfig{
    LogHeaders: []string{"Authorization", "Content-Type"},
})

Structured request/response logging with configurable header capture.

📈 Client Stats

stats := client.Stats()
fmt.Printf("Requests: %d, Failed: %d\n", stats.RequestsTotal, stats.RequestsFailed)
fmt.Printf("Cache Hits: %d, Misses: %d\n", stats.CacheHits, stats.CacheMisses)

Real-time metrics snapshot including circuit state, bulkhead in-flight, cache stats, and uptime.

🏷️ Request ID

ambatukam.WithRequestID("X-Request-ID")

Auto-generates 12-byte hex ID per request, or propagates existing one.

🪝 Hooks

ambatukam.WithHooks(ambatukam.Hooks{
    BeforeRequest: func(req *http.Request) error {
        req.Header.Set("Authorization", "Bearer "+token)
        return nil
    },
    OnRetry: func(req *http.Request, attempt int, nextDelay time.Duration) {
        log.Printf("retrying %s (attempt %d)", req.URL, attempt)
    },
    OnStateChange: func(name string, from, to ambatukam.State) {
        metrics.Gauge("circuit_state").Set(string(to))
    },
    OnFallback: func(req *http.Request, err error) {
        log.Printf("fallback triggered: %v", err)
    },
})

Five callbacks: BeforeRequest, AfterResponse, OnRetry, OnStateChange, OnFallback.

BeforeRequest and AfterResponse fire on every request attempt, regardless of whether WithRetry is configured. They are implemented as an innermost HooksPolicy that wraps the HTTP call. OnRetry only fires when retry is active. OnStateChange only fires with WithCircuitBreaker. OnFallback only fires with WithFallback.

📊 Metrics

ambatukam.WithMetrics(myPrometheusRecorder)

Implement MetricsRecorder interface for Prometheus, Datadog, or any metrics system.

📊 Prometheus Metrics

recorder := ambatukam.NewPrometheusRecorder(ambatukam.PrometheusConfig{
    RequestsTotal:      prometheusRequestsTotal,
    RetriesTotal:       prometheusRetriesTotal,
    CircuitState:       prometheusCircuitState,
    RequestDuration:    prometheusRequestDuration,
    BulkheadDenied:     prometheusBulkheadDenied,
    RateLimitDenied:    prometheusRateLimitDenied,
    FallbacksTotal:     prometheusFallbacksTotal,
    TimeoutsTotal:      prometheusTimeoutsTotal,
    CircuitTransitions: prometheusCircuitTransitions,
})
client := ambatukam.New(ambatukam.WithMetrics(recorder))

Full Prometheus integration with Counter, Gauge, Histogram vectors.

📝 Custom Logger

client := ambatukam.New(
    ambatukam.WithCustomLogger(myZerologAdapter),
)

Implement Logger interface (Debug, Info, Warn, Error) for zerolog, zap, or any logging library.

🏥 Health Check

hc := client.HealthChecker()
http.Handle("/health", hc.Handler())

Returns JSON with policy status, memory stats, and uptime. Background memory refresh every 10s. Goroutine cleanup via client.Close().

📦 Generic JSON Helpers

u, err := ambatukam.GetJSON[User](client, ctx, "https://api.example.com/users/1")
created, err := ambatukam.PostJSON[User](client, ctx, "https://api.example.com/users", user)

Auto-handles JSON encode/decode, content-type, and 4xx/5xx errors.


🎯 Preset Configs

Don't want to tune? Use presets:

client := ambatukam.New(ambatukam.ProductionConfig()...)
client := ambatukam.New(ambatukam.AggressiveConfig()...)
client := ambatukam.New(ambatukam.ConservativeConfig()...)
Preset Retries Timeout Circuit Threshold Bulkhead Rate Limit Singleflight
Production 3 30s 5 failures NumCPU×4
Aggressive 1 5s 3 failures NumCPU×2 50 rps, burst 20
Conservative 5 60s 20 failures NumCPU×8 200 rps, burst 100

💡 Real-World Patterns

Stripe / Payment Gateway

client := ambatukam.New(
    ambatukam.WithTimeout(ambatukam.TimeoutConfig{Timeout: 10 * time.Second}),
    ambatukam.WithRetry(ambatukam.RetryConfig{
        MaxRetries: 3,
        Backoff:    ambatukam.ConstantBackoff(500 * time.Millisecond),
    }),
    ambatukam.WithCircuitBreaker(ambatukam.CircuitConfig{FailureThreshold: 5}),
    ambatukam.WithFallback(ambatukam.FallbackConfig{
        Handler: func(req *http.Request, err error) (*http.Response, error) {
            return nil, errors.New("payment service unavailable, please retry later")
        },
    }),
)

Microservice with Auth + Tracing

client := ambatukam.New(
    ambatukam.WithRequestID("X-Request-ID"),
    ambatukam.WithHooks(ambatukam.Hooks{
        BeforeRequest: func(req *http.Request) error {
            req.Header.Set("Authorization", "Bearer "+getToken())
            return nil
        },
    }),
    ambatukam.WithSingleflight(),
    ambatukam.WithRetry(ambatukam.DefaultRetryConfig()),
)

Third-Party API with Rate Limit

client := ambatukam.New(
    ambatukam.WithRateLimit(ambatukam.RateLimitConfig{
        Rate:        5,
        Burst:       10,
        WaitTimeout: 2 * time.Second,
    }),
    ambatukam.WithTimeout(ambatukam.TimeoutConfig{Timeout: 30 * time.Second}),
    ambatukam.WithFallback(ambatukam.FallbackConfig{
        Handler: func(req *http.Request, err error) (*http.Response, error) {
            return getCachedData(req.URL.String())
        },
    }),
)

🚨 Error Handling

resp, err := client.Get(ctx, url)
switch {
case errors.Is(err, ambatukam.ErrCircuitOpen):
case errors.Is(err, ambatukam.ErrMaxRetries):
case errors.Is(err, ambatukam.ErrTimeout):
case errors.Is(err, ambatukam.ErrBulkheadFull):
case errors.Is(err, ambatukam.ErrRateLimited):
case errors.Is(err, ambatukam.ErrFallback):
case errors.Is(err, ambatukam.ErrNilRequest):
}

For full context:

var reqErr *ambatukam.RequestError
if errors.As(err, &reqErr) {
    log.Printf("[%s] %s %s returned %d after %d attempts",
        reqErr.Policy, reqErr.Method, reqErr.URL, reqErr.Status, reqErr.Attempts)
}

📈 Benchmarks

go test -bench=. -benchmem -benchtime=2s ./...
Setup ns/op B/op allocs/op
http.Client (raw stdlib) 98,785 5,106 63
Ambatukam Go (no policies) 98,325 4,466 57
Ambatukam Go (retry=3) 96,879 4,505 58
Ambatukam Go (full stack) 235,341 16,757 120
Ambatukam Go (parallel) 26,064 8,214 77

Zero overhead when no policies enabled. Full stack costs ~2.4x vs raw stdlib.


🧪 Testing

The test suite includes 20+ stress tests with DDoS-level concurrency (500-1000 goroutines), chaos servers, circuit breaker state transitions, and full-stack integration tests.

go test -race ./...                    # all tests with race detector
go test -run TestStress -v             # stress tests only
go test -bench=. -benchmem             # benchmarks

🔄 Migration

From cenkalti/backoff + sony/gobreaker
// Before: two libraries, manual wiring
import (
    "github.com/cenkalti/backoff/v4"
    "github.com/sony/gobreaker"
)

// After: one library, one config
import "github.com/farhanturu/ambatukam-go"

client := ambatukam.New(
    ambatukam.WithRetry(ambatukam.RetryConfig{MaxRetries: 3}),
    ambatukam.WithCircuitBreaker(ambatukam.CircuitConfig{FailureThreshold: 5}),
)
From hashicorp/go-retryablehttp

Ambatukam Go's *Client is a drop-in *http.Client. Wrap your existing transport via WithHTTPClient, or use Do/Get/Post directly. Adds circuit breaker, bulkhead, rate limit, fallback, singleflight, hooks, and request ID.


📚 Documentation

Document Description
🌐 Website Interactive documentation website
README.md You are here
COOKBOOK.md Recipes for common patterns
FAQ.md Frequently asked questions
MIGRATION.md Migrating from other libraries
CONTRIBUTING.md How to contribute
SECURITY.md Security policy

🐛 Troubleshooting

Problem Solution
POST isn't being retried Only idempotent methods retry by default. Use custom ShouldRetry.
Circuit opens too often Lower FailureThreshold or increase OpenDuration.
Bulkhead denies immediately Increase MaxQueue or MaxConcurrent.
Rate limit denies unexpectedly Rate == 0 = disabled, Rate < 0 = deny all.
Singleflight merging wrong requests Only affects POST/PUT/PATCH with identical bodies. GET is always safe.
Hooks not firing BeforeRequest/AfterResponse always fire. OnRetry needs WithRetry.
Need debug logging Use ambatukam.WithDebug().

🗺️ Roadmap

v1.2.5 (current)

Major feature release: response cache with TTL/LRU, adaptive timeout based on p99 latency, retry budget for cascade failure prevention, request/response interceptors, priority bulkhead queue, structured request logging, and real-time client stats.

v2.0 (next)

OpenTelemetry tracing, distributed (Redis-backed) circuit breaker, gRPC support.


🤝 Contributing

PRs welcome. Run go test -race ./... and go vet ./... before submitting; add a test for any new behavior. See CONTRIBUTING.md.


📄 License

MIT — see LICENSE.


Ambatukam Go
Stop writing resilience code. Start shipping features.

Built with ❤️ by farhanturu

Documentation

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	ErrCircuitOpen  = errors.New("ambatukam: circuit breaker is open")
	ErrMaxRetries   = errors.New("ambatukam: max retries exceeded")
	ErrNilRequest   = errors.New("ambatukam: nil request")
	ErrTimeout      = errors.New("ambatukam: per-attempt timeout exceeded")
	ErrBulkheadFull = errors.New("ambatukam: bulkhead full")
	ErrRateLimited  = errors.New("ambatukam: rate limited")
	ErrFallback     = errors.New("ambatukam: fallback failed")
)

Functions

func GetJSON

func GetJSON[T any](c *Client, ctx context.Context, url string) (T, error)
Example

ExampleGetJSON demonstrates the typed JSON helper.

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	fmt.Fprint(w, `{"name":"alice","age":30}`)
}))
defer srv.Close()

client := ambatukam.New()
defer client.Close()

u, err := ambatukam.GetJSON[testUser](client, context.Background(), srv.URL)
if err != nil {
	fmt.Println("error:", err)
	return
}
fmt.Println(u.Name, u.Age)
Output:
alice 30

func Permanent

func Permanent(err error) error
Example

ExamplePermanent demonstrates marking an error as non-retryable.

package main

import (
	"errors"
	"fmt"

	"github.com/farhanturu/ambatukam-go"
)

func main() {
	err := ambatukam.Permanent(errors.New("do not retry me"))
	fmt.Println(err)
}
Output:
permanent: do not retry me

func PostJSON

func PostJSON[T any](c *Client, ctx context.Context, url string, body any) (T, error)
Example

ExamplePostJSON demonstrates the typed JSON POST helper.

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(201)
	fmt.Fprint(w, `{"name":"bob","age":25}`)
}))
defer srv.Close()

client := ambatukam.New()
defer client.Close()

out, err := ambatukam.PostJSON[testUser](client, context.Background(), srv.URL, testUser{Name: "alice", Age: 30})
if err != nil {
	fmt.Println("error:", err)
	return
}
fmt.Println(out.Name, out.Age)
Output:
bob 25

func WithPriority added in v1.2.5

func WithPriority(ctx context.Context) context.Context

Types

type AdaptiveTimeoutConfig added in v1.2.5

type AdaptiveTimeoutConfig struct {
	Initial    time.Duration
	Percentile int
	Window     time.Duration
	MinSamples int
}

type AdaptiveTimeoutPolicy added in v1.2.5

type AdaptiveTimeoutPolicy struct {
	// contains filtered or unexported fields
}

func NewAdaptiveTimeout added in v1.2.5

func NewAdaptiveTimeout(cfg AdaptiveTimeoutConfig) *AdaptiveTimeoutPolicy

func (*AdaptiveTimeoutPolicy) CurrentTimeout added in v1.2.5

func (at *AdaptiveTimeoutPolicy) CurrentTimeout() time.Duration

func (*AdaptiveTimeoutPolicy) Execute added in v1.2.5

func (at *AdaptiveTimeoutPolicy) Execute(ctx context.Context, req *http.Request, next PolicyFunc) (*http.Response, error)

func (*AdaptiveTimeoutPolicy) WithMetrics added in v1.2.5

type Backoff

type Backoff interface {
	NextDelay(attempt int) time.Duration
}

func ConstantBackoff

func ConstantBackoff(d time.Duration) Backoff

func ExponentialBackoff

func ExponentialBackoff(initial, max time.Duration, multiplier, jitter float64) Backoff

func LinearBackoff

func LinearBackoff(initial, max, step time.Duration) Backoff

type BulkheadConfig

type BulkheadConfig struct {
	MaxConcurrent uint32
	MaxQueue      uint32
	QueueTimeout  time.Duration
	Priority      bool
}

type BulkheadPolicy

type BulkheadPolicy struct {
	// contains filtered or unexported fields
}

func NewBulkhead

func NewBulkhead(cfg BulkheadConfig) *BulkheadPolicy
Example

ExampleNewBulkhead demonstrates configuring a bulkhead.

package main

import (
	"fmt"
	"time"

	"github.com/farhanturu/ambatukam-go"
)

func main() {
	cfg := ambatukam.BulkheadConfig{
		MaxConcurrent: 5,
		MaxQueue:      10,
		QueueTimeout:  100 * time.Millisecond,
	}

	client := ambatukam.New(ambatukam.WithBulkhead(cfg))
	defer client.Close()

	fmt.Println("max concurrent:", cfg.MaxConcurrent)
}
Output:
max concurrent: 5

func (*BulkheadPolicy) Close added in v1.2.2

func (b *BulkheadPolicy) Close()

func (*BulkheadPolicy) Denied

func (b *BulkheadPolicy) Denied() uint64

func (*BulkheadPolicy) Execute

func (b *BulkheadPolicy) Execute(ctx context.Context, req *http.Request, next PolicyFunc) (*http.Response, error)

func (*BulkheadPolicy) InFlight

func (b *BulkheadPolicy) InFlight() uint32

func (*BulkheadPolicy) WithLogger

func (b *BulkheadPolicy) WithLogger(l *slog.Logger) *BulkheadPolicy

func (*BulkheadPolicy) WithMetrics added in v1.2.1

func (b *BulkheadPolicy) WithMetrics(m MetricsRecorder) *BulkheadPolicy

type CacheConfig added in v1.2.5

type CacheConfig struct {
	TTL        time.Duration
	MaxEntries int
	Methods    []string
}

type CachePolicy added in v1.2.5

type CachePolicy struct {
	// contains filtered or unexported fields
}

func NewCache added in v1.2.5

func NewCache(cfg CacheConfig) *CachePolicy

func (*CachePolicy) Execute added in v1.2.5

func (c *CachePolicy) Execute(ctx context.Context, req *http.Request, next PolicyFunc) (*http.Response, error)

func (*CachePolicy) SetStats added in v1.2.5

func (c *CachePolicy) SetStats(s *statsRecorder)

type CircuitBreakerPolicy

type CircuitBreakerPolicy struct {
	// contains filtered or unexported fields
}

func NewCircuitBreaker

func NewCircuitBreaker(cfg CircuitConfig) *CircuitBreakerPolicy
Example

ExampleNewCircuitBreaker demonstrates configuring the circuit breaker.

package main

import (
	"fmt"
	"time"

	"github.com/farhanturu/ambatukam-go"
)

func main() {
	cfg := ambatukam.DefaultCircuitConfig()
	cfg.FailureThreshold = 3
	cfg.OpenDuration = 10 * time.Second

	client := ambatukam.New(ambatukam.WithCircuitBreaker(cfg))
	defer client.Close()

	fmt.Println("failure threshold:", cfg.FailureThreshold)
}
Output:
failure threshold: 3

func (*CircuitBreakerPolicy) Execute

func (cb *CircuitBreakerPolicy) Execute(ctx context.Context, req *http.Request, next PolicyFunc) (*http.Response, error)

func (*CircuitBreakerPolicy) State

func (cb *CircuitBreakerPolicy) State() State

func (*CircuitBreakerPolicy) WithHooks

func (*CircuitBreakerPolicy) WithLogger

func (*CircuitBreakerPolicy) WithMetrics added in v1.2.1

func (*CircuitBreakerPolicy) WithName

func (cb *CircuitBreakerPolicy) WithName(name string) *CircuitBreakerPolicy

type CircuitConfig

type CircuitConfig struct {
	ShouldTrip       func(resp *http.Response, err error) bool
	OpenDuration     time.Duration
	FailureThreshold uint32
	HalfOpenMaxReqs  uint32
}

func DefaultCircuitConfig

func DefaultCircuitConfig() CircuitConfig

type Client

type Client struct {
	// contains filtered or unexported fields
}

func New

func New(opts ...Option) *Client

func NewDefaultClient

func NewDefaultClient() *Client
Example

ExampleNewDefaultClient demonstrates the one-call production-default client.

package main

import (
	"fmt"

	"github.com/farhanturu/ambatukam-go"
)

func main() {
	client := ambatukam.NewDefaultClient()
	defer client.Close()
	fmt.Println("ok")
}
Output:
ok

func (*Client) Close

func (c *Client) Close() error

func (*Client) Delete added in v1.2.4

func (c *Client) Delete(ctx context.Context, url string) (*http.Response, error)

func (*Client) Do

func (c *Client) Do(req *http.Request) (*http.Response, error)

func (*Client) DoWithContext

func (c *Client) DoWithContext(ctx context.Context, req *http.Request) (*http.Response, error)

func (*Client) Get

func (c *Client) Get(ctx context.Context, url string) (*http.Response, error)
Example

ExampleClient_Get demonstrates a basic GET request with all three policies.

package main

import (
	"context"
	"fmt"
	"net/http"
	"net/http/httptest"
	"time"

	"github.com/farhanturu/ambatukam-go"
)

func main() {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		fmt.Fprint(w, `{"hello":"world"}`)
	}))
	defer srv.Close()

	client := ambatukam.New(
		ambatukam.WithTimeout(ambatukam.TimeoutConfig{Timeout: 2 * time.Second}),
		ambatukam.WithRetry(ambatukam.DefaultRetryConfig()),
		ambatukam.WithCircuitBreaker(ambatukam.DefaultCircuitConfig()),
	)
	defer client.Close()

	resp, err := client.Get(context.Background(), srv.URL)
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	defer resp.Body.Close()
	fmt.Println("status:", resp.StatusCode)
}
Output:
status: 200

func (*Client) Head added in v1.2.4

func (c *Client) Head(ctx context.Context, url string) (*http.Response, error)

func (*Client) HealthChecker added in v1.1.0

func (c *Client) HealthChecker() *HealthChecker

func (*Client) Options added in v1.2.4

func (c *Client) Options(ctx context.Context, url string) (*http.Response, error)

func (*Client) Patch added in v1.2.4

func (c *Client) Patch(ctx context.Context, url, contentType string, body io.Reader) (*http.Response, error)

func (*Client) Post

func (c *Client) Post(ctx context.Context, url, contentType string, body io.Reader) (*http.Response, error)
Example

ExampleClient_Post demonstrates a POST with a JSON body.

package main

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"

	"github.com/farhanturu/ambatukam-go"
)

func main() {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		body, _ := io.ReadAll(r.Body)
		fmt.Println("server received:", string(body))
		w.WriteHeader(http.StatusCreated)
	}))
	defer srv.Close()

	client := ambatukam.New()
	defer client.Close()

	resp, err := client.Post(
		context.Background(),
		srv.URL+"/users",
		"application/json",
		io.NopCloser(strings.NewReader(`{"name":"alice"}`)),
	)
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	defer resp.Body.Close()
	fmt.Println("status:", resp.StatusCode)
}
Output:
server received: {"name":"alice"}
status: 201

func (*Client) Put added in v1.2.4

func (c *Client) Put(ctx context.Context, url, contentType string, body io.Reader) (*http.Response, error)

func (*Client) RoundTrip

func (c *Client) RoundTrip(req *http.Request) (*http.Response, error)
Example

ExampleClient_RoundTrip shows how to use an amba *Client as the Transport of a standard *http.Client.

package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"github.com/farhanturu/ambatukam-go"
)

func main() {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(200)
	}))
	defer srv.Close()

	amba := ambatukam.New(ambatukam.WithRetry(ambatukam.RetryConfig{MaxRetries: 2}))
	defer amba.Close()

	hc := &http.Client{Transport: amba.Transport()}
	resp, err := hc.Get(srv.URL)
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	defer resp.Body.Close()
	fmt.Println("status:", resp.StatusCode)
}
Output:
status: 200

func (*Client) Stats added in v1.2.5

func (c *Client) Stats() ClientStats

func (*Client) Transport

func (c *Client) Transport() http.RoundTripper

type ClientStats added in v1.2.5

type ClientStats struct {
	CircuitState      State
	BulkheadInFlight  uint32
	BulkheadDenied    uint64
	RateLimitTokens   int
	RequestsTotal     uint64
	RequestsFailed    uint64
	RetriesTotal      uint64
	FallbacksTotal    uint64
	TimeoutsTotal     uint64
	CacheHits         uint64
	CacheMisses       uint64
	AvgResponseTimeNs int64
	Uptime            time.Duration
}

type Counter added in v1.2.0

type Counter interface {
	Inc()
	Add(float64)
}

type CounterVec added in v1.2.0

type CounterVec interface {
	WithLabelValues(lvs ...string) Counter
}

type FallbackConfig added in v1.1.0

type FallbackConfig struct {
	Handler func(req *http.Request, err error) (*http.Response, error)
}

type FallbackPolicy added in v1.1.0

type FallbackPolicy struct {
	// contains filtered or unexported fields
}

func NewFallback added in v1.1.0

func NewFallback(cfg FallbackConfig) *FallbackPolicy

func (*FallbackPolicy) Execute added in v1.1.0

func (f *FallbackPolicy) Execute(ctx context.Context, req *http.Request, next PolicyFunc) (*http.Response, error)

func (*FallbackPolicy) WithHooks added in v1.1.0

func (f *FallbackPolicy) WithHooks(h Hooks) *FallbackPolicy

func (*FallbackPolicy) WithMetrics added in v1.2.1

func (f *FallbackPolicy) WithMetrics(m MetricsRecorder) *FallbackPolicy

type Gauge added in v1.2.0

type Gauge interface {
	Set(float64)
	Inc()
	Dec()
}

type GaugeVec added in v1.2.0

type GaugeVec interface {
	WithLabelValues(lvs ...string) Gauge
}

type HealthChecker added in v1.1.0

type HealthChecker struct {
	// contains filtered or unexported fields
}

func NewHealthChecker added in v1.1.0

func NewHealthChecker(c *Client) *HealthChecker

func (*HealthChecker) Close added in v1.2.1

func (h *HealthChecker) Close()

func (*HealthChecker) Handler added in v1.1.0

func (h *HealthChecker) Handler() http.HandlerFunc

type HealthStatus added in v1.1.0

type HealthStatus struct {
	Timestamp time.Time         `json:"timestamp"`
	Policies  map[string]string `json:"policies"`
	Status    string            `json:"status"`
	Memory    MemoryStats       `json:"memory"`
	Uptime    time.Duration     `json:"uptime"`
}

type Histogram added in v1.2.0

type Histogram interface {
	Observe(float64)
}

type HistogramVec added in v1.2.0

type HistogramVec interface {
	WithLabelValues(lvs ...string) Histogram
}

type Hooks

type Hooks struct {
	BeforeRequest func(req *http.Request) error
	AfterResponse func(req *http.Request, resp *http.Response, err error)
	OnRetry       func(req *http.Request, attempt int, nextDelay time.Duration)
	OnStateChange func(name string, from, to State)
	OnFallback    func(req *http.Request, err error)
}

type Interceptor added in v1.2.5

type Interceptor func(req *http.Request, next PolicyFunc) (*http.Response, error)

type Logger added in v1.2.0

type Logger interface {
	Debug(msg string, args ...any)
	Info(msg string, args ...any)
	Warn(msg string, args ...any)
	Error(msg string, args ...any)
}

type MemoryStats added in v1.1.0

type MemoryStats struct {
	Alloc      uint64 `json:"alloc_bytes"`
	TotalAlloc uint64 `json:"total_alloc_bytes"`
	Sys        uint64 `json:"sys_bytes"`
	NumGC      uint32 `json:"num_gc"`
}

type MetricsRecorder added in v1.1.0

type MetricsRecorder interface {
	RecordRequest(method, url string, status int, duration time.Duration)
	RecordRetry(method, url string, attempt int)
	RecordCircuitStateChange(name string, from, to State)
	RecordBulkheadDenied(method, url string)
	RecordRateLimitDenied(method, url string)
	RecordFallback(method, url string)
	RecordTimeout(method, url string)
}

func NewNoopMetricsRecorder added in v1.1.0

func NewNoopMetricsRecorder() MetricsRecorder

type Option

type Option func(*Client)

func AggressiveConfig

func AggressiveConfig() []Option

func ConservativeConfig

func ConservativeConfig() []Option

func DefaultConfig

func DefaultConfig() []Option

func ProductionConfig

func ProductionConfig() []Option

func WithAdaptiveTimeout added in v1.2.5

func WithAdaptiveTimeout(cfg AdaptiveTimeoutConfig) Option

func WithBulkhead

func WithBulkhead(cfg BulkheadConfig) Option

func WithCache added in v1.2.5

func WithCache(cfg CacheConfig) Option

func WithCircuitBreaker

func WithCircuitBreaker(cfg CircuitConfig) Option

func WithCustomLogger added in v1.2.0

func WithCustomLogger(l Logger) Option

func WithDebug

func WithDebug() Option

func WithFallback added in v1.1.0

func WithFallback(cfg FallbackConfig) Option

func WithHTTPClient

func WithHTTPClient(hc *http.Client) Option
Example

ExampleWithHTTPClient demonstrates swapping in a custom *http.Client.

package main

import (
	"fmt"
	"net/http"
	"time"

	"github.com/farhanturu/ambatukam-go"
)

func main() {
	client := ambatukam.New(
		ambatukam.WithHTTPClient(&http.Client{Timeout: 5 * time.Second}),
	)
	defer client.Close()
	fmt.Println("custom http.Client")
}
Output:
custom http.Client

func WithHooks

func WithHooks(h Hooks) Option

func WithInterceptor added in v1.2.5

func WithInterceptor(i Interceptor) Option

func WithLogger

func WithLogger(l *slog.Logger) Option

func WithMaxBodySize added in v1.2.4

func WithMaxBodySize(n int64) Option

func WithMetrics added in v1.1.0

func WithMetrics(r MetricsRecorder) Option

func WithPolicy

func WithPolicy(p Policy) Option

func WithRateLimit

func WithRateLimit(cfg RateLimitConfig) Option

func WithRequestID

func WithRequestID(header string) Option

func WithRequestIDPolicy

func WithRequestIDPolicy(p *RequestIDPolicy) Option

func WithRequestLog added in v1.2.5

func WithRequestLog(cfg RequestLogConfig) Option

func WithRetry

func WithRetry(cfg RetryConfig) Option

func WithRetryBudget added in v1.2.5

func WithRetryBudget(budget float64, window time.Duration) Option

func WithSingleflight added in v1.1.0

func WithSingleflight() Option

func WithTimeout

func WithTimeout(cfg TimeoutConfig) Option

func WithTimeoutMap added in v1.2.0

func WithTimeoutMap(rules map[string]time.Duration) Option

type PermanentError

type PermanentError struct{ Err error }

func (*PermanentError) Error

func (e *PermanentError) Error() string

func (*PermanentError) Unwrap

func (e *PermanentError) Unwrap() error

type Policy

type Policy interface {
	Execute(ctx context.Context, req *http.Request, next PolicyFunc) (*http.Response, error)
}

func Chain

func Chain(policies ...Policy) Policy
Example

ExampleChain demonstrates manual composition of policies.

package main

import (
	"fmt"
	"time"

	"github.com/farhanturu/ambatukam-go"
)

func main() {
	client := ambatukam.New(ambatukam.WithPolicy(ambatukam.Chain(
		ambatukam.NewTimeout(ambatukam.TimeoutConfig{Timeout: 2 * time.Second}),
		ambatukam.NewRetry(ambatukam.DefaultRetryConfig()),
		ambatukam.NewCircuitBreaker(ambatukam.DefaultCircuitConfig()),
	)))
	defer client.Close()

	fmt.Println("chained: ok")
}
Output:
chained: ok

type PolicyFunc

type PolicyFunc func(ctx context.Context, req *http.Request) (*http.Response, error)

func (PolicyFunc) Execute

func (f PolicyFunc) Execute(ctx context.Context, req *http.Request, next PolicyFunc) (*http.Response, error)

type PrometheusConfig added in v1.2.0

type PrometheusConfig struct {
	RequestsTotal      CounterVec
	RetriesTotal       CounterVec
	CircuitState       GaugeVec
	RequestDuration    HistogramVec
	BulkheadDenied     CounterVec
	RateLimitDenied    CounterVec
	FallbacksTotal     CounterVec
	TimeoutsTotal      CounterVec
	CircuitTransitions CounterVec
}

type PrometheusRecorder added in v1.2.0

type PrometheusRecorder struct {
	// contains filtered or unexported fields
}

func NewPrometheusRecorder added in v1.2.0

func NewPrometheusRecorder(cfg PrometheusConfig) *PrometheusRecorder

func (*PrometheusRecorder) RecordBulkheadDenied added in v1.2.0

func (r *PrometheusRecorder) RecordBulkheadDenied(method, url string)

func (*PrometheusRecorder) RecordCircuitStateChange added in v1.2.0

func (r *PrometheusRecorder) RecordCircuitStateChange(name string, from, to State)

func (*PrometheusRecorder) RecordFallback added in v1.2.0

func (r *PrometheusRecorder) RecordFallback(method, url string)

func (*PrometheusRecorder) RecordRateLimitDenied added in v1.2.0

func (r *PrometheusRecorder) RecordRateLimitDenied(method, url string)

func (*PrometheusRecorder) RecordRequest added in v1.2.0

func (r *PrometheusRecorder) RecordRequest(method, url string, status int, duration time.Duration)

func (*PrometheusRecorder) RecordRetry added in v1.2.0

func (r *PrometheusRecorder) RecordRetry(method, url string, attempt int)

func (*PrometheusRecorder) RecordTimeout added in v1.2.0

func (r *PrometheusRecorder) RecordTimeout(method, url string)

type RateLimitConfig

type RateLimitConfig struct {
	Rate        float64
	WaitTimeout time.Duration
	Burst       uint32
}

type RateLimitPolicy

type RateLimitPolicy struct {
	// contains filtered or unexported fields
}

func NewRateLimit

func NewRateLimit(cfg RateLimitConfig) *RateLimitPolicy
Example

ExampleNewRateLimit demonstrates building a rate-limit policy directly.

package main

import (
	"fmt"

	"github.com/farhanturu/ambatukam-go"
)

func main() {
	client := ambatukam.New(ambatukam.WithRateLimit(ambatukam.RateLimitConfig{
		Rate:  10,
		Burst: 5,
	}))
	defer client.Close()
	fmt.Println("rate limited client ready")
}
Output:
rate limited client ready

func (*RateLimitPolicy) AvailableTokens added in v1.2.5

func (r *RateLimitPolicy) AvailableTokens() int

func (*RateLimitPolicy) Close added in v1.2.2

func (r *RateLimitPolicy) Close()

func (*RateLimitPolicy) Execute

func (r *RateLimitPolicy) Execute(ctx context.Context, req *http.Request, next PolicyFunc) (*http.Response, error)

func (*RateLimitPolicy) WithLogger

func (r *RateLimitPolicy) WithLogger(l *slog.Logger) *RateLimitPolicy

func (*RateLimitPolicy) WithMetrics added in v1.2.1

func (r *RateLimitPolicy) WithMetrics(m MetricsRecorder) *RateLimitPolicy

type RequestError

type RequestError struct {
	Method   string
	URL      string
	Err      error
	Policy   string
	Status   int
	Attempts int
}

func (*RequestError) Error

func (e *RequestError) Error() string

func (*RequestError) Unwrap

func (e *RequestError) Unwrap() error

type RequestIDPolicy

type RequestIDPolicy struct {
	// contains filtered or unexported fields
}

func NewRequestIDPolicy

func NewRequestIDPolicy() *RequestIDPolicy
Example

ExampleNewRequestIDPolicy demonstrates registering a request-ID policy.

package main

import (
	"fmt"

	"github.com/farhanturu/ambatukam-go"
)

func main() {
	client := ambatukam.New(ambatukam.WithRequestIDPolicy(ambatukam.NewRequestIDPolicy()))
	defer client.Close()
	fmt.Println("request ID policy enabled")
}
Output:
request ID policy enabled

func (*RequestIDPolicy) Execute

func (r *RequestIDPolicy) Execute(ctx context.Context, req *http.Request, next PolicyFunc) (*http.Response, error)

func (*RequestIDPolicy) WithGenerator

func (r *RequestIDPolicy) WithGenerator(gen func() string) *RequestIDPolicy

func (*RequestIDPolicy) WithHeader

func (r *RequestIDPolicy) WithHeader(name string) *RequestIDPolicy

type RequestLogConfig added in v1.2.5

type RequestLogConfig struct {
	Logger     *slog.Logger
	LogHeaders []string
	LogBody    bool
}

type RequestLogger added in v1.2.5

type RequestLogger struct {
	// contains filtered or unexported fields
}

func NewRequestLogger added in v1.2.5

func NewRequestLogger(cfg RequestLogConfig) *RequestLogger

func (*RequestLogger) Wrap added in v1.2.5

func (rl *RequestLogger) Wrap(next PolicyFunc) PolicyFunc

type RetryBudget added in v1.2.5

type RetryBudget struct {
	// contains filtered or unexported fields
}

func NewRetryBudget added in v1.2.5

func NewRetryBudget(budget float64, window time.Duration) *RetryBudget

func (*RetryBudget) Allow added in v1.2.5

func (rb *RetryBudget) Allow() bool

func (*RetryBudget) Stats added in v1.2.5

func (rb *RetryBudget) Stats() (total, retries int64)

type RetryConfig

type RetryConfig struct {
	Backoff        Backoff
	ShouldRetry    func(resp *http.Response, err error) bool
	InitialBackoff time.Duration
	MaxBackoff     time.Duration
	Multiplier     float64
	Jitter         float64
	MaxRetries     int
	MaxBodySize    int64
}

func DefaultRetryConfig

func DefaultRetryConfig() RetryConfig

type RetryPolicy

type RetryPolicy struct {
	// contains filtered or unexported fields
}

func NewRetry

func NewRetry(cfg RetryConfig) *RetryPolicy
Example

ExampleNewRetry demonstrates configuring the retry policy.

package main

import (
	"fmt"
	"time"

	"github.com/farhanturu/ambatukam-go"
)

func main() {
	cfg := ambatukam.DefaultRetryConfig()
	cfg.MaxRetries = 5
	cfg.Backoff = ambatukam.ExponentialBackoff(50*time.Millisecond, time.Second, 2.0, 0.2)

	client := ambatukam.New(ambatukam.WithRetry(cfg))
	defer client.Close()

	fmt.Println("max retries:", cfg.MaxRetries)
}
Output:
max retries: 5

func (*RetryPolicy) Execute

func (r *RetryPolicy) Execute(ctx context.Context, req *http.Request, next PolicyFunc) (*http.Response, error)

func (*RetryPolicy) WithBudget added in v1.2.5

func (r *RetryPolicy) WithBudget(b *RetryBudget) *RetryPolicy

func (*RetryPolicy) WithHooks

func (r *RetryPolicy) WithHooks(h Hooks) *RetryPolicy

func (*RetryPolicy) WithLogger

func (r *RetryPolicy) WithLogger(l *slog.Logger) *RetryPolicy

func (*RetryPolicy) WithMetrics added in v1.2.1

func (r *RetryPolicy) WithMetrics(m MetricsRecorder) *RetryPolicy

type SingleflightConfig added in v1.1.0

type SingleflightConfig struct {
	Enabled bool
}

type SingleflightPolicy added in v1.1.0

type SingleflightPolicy struct {
	// contains filtered or unexported fields
}

func NewSingleflight added in v1.1.0

func NewSingleflight() *SingleflightPolicy

func (*SingleflightPolicy) Execute added in v1.1.0

func (sf *SingleflightPolicy) Execute(ctx context.Context, req *http.Request, next PolicyFunc) (*http.Response, error)

func (*SingleflightPolicy) SetMaxBodySize added in v1.2.4

func (sf *SingleflightPolicy) SetMaxBodySize(n int64)

type State

type State string
const (
	StateClosed   State = "closed"
	StateOpen     State = "open"
	StateHalfOpen State = "half-open"
)

type TimeoutConfig

type TimeoutConfig struct {
	Timeout time.Duration
}

type TimeoutMapPolicy added in v1.2.0

type TimeoutMapPolicy struct {
	// contains filtered or unexported fields
}

func NewTimeoutMap added in v1.2.0

func NewTimeoutMap(rules map[string]time.Duration) *TimeoutMapPolicy

func (*TimeoutMapPolicy) Execute added in v1.2.0

func (t *TimeoutMapPolicy) Execute(ctx context.Context, req *http.Request, next PolicyFunc) (*http.Response, error)

func (*TimeoutMapPolicy) WithMetrics added in v1.2.1

type TimeoutPolicy

type TimeoutPolicy struct {
	// contains filtered or unexported fields
}

func NewTimeout

func NewTimeout(cfg TimeoutConfig) *TimeoutPolicy
Example

ExampleNewTimeout demonstrates configuring the per-attempt timeout.

package main

import (
	"fmt"
	"time"

	"github.com/farhanturu/ambatukam-go"
)

func main() {
	client := ambatukam.New(
		ambatukam.WithTimeout(ambatukam.TimeoutConfig{Timeout: 500 * time.Millisecond}),
	)
	defer client.Close()

	fmt.Println("timeout: ok")
}
Output:
timeout: ok

func (*TimeoutPolicy) Execute

func (t *TimeoutPolicy) Execute(ctx context.Context, req *http.Request, next PolicyFunc) (*http.Response, error)

func (*TimeoutPolicy) WithMetrics added in v1.2.1

func (t *TimeoutPolicy) WithMetrics(m MetricsRecorder) *TimeoutPolicy

Directories

Path Synopsis
examples
basic command
Package main is a runnable example for ambatukam.
Package main is a runnable example for ambatukam.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL