maping

package module
v0.11.0 Latest Latest
Warning

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

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

README

mAPI-ng Client

github.com/arhuman/maping/client — hosted, zero-config API observability for Go services.

The client collects per-endpoint request metrics (rate, latency, error rate, bandwidth) using client-side DDSketch aggregation, and ships compact Summaries to the mAPI-ng collector over gRPC. License: MIT (see LICENSE).


60-second quickstart

1. Install

go get github.com/arhuman/maping/client

The Gin adapter lives in its own module (github.com/arhuman/maping/client/gin) so the core client stays free of web-framework dependencies. If you use Gin, install it as well:

go get github.com/arhuman/maping/client/gin

2. Wire the middleware

Register the mAPI-ng middleware above gin.Recovery() so panics are recorded as 5xx before Recovery converts them, without altering host behavior.

import (
    maping    "github.com/arhuman/maping/client"
    mapinggin "github.com/arhuman/maping/client/gin"
)

func main() {
    // One recorder for the process lifetime.
    rec := maping.NewRecorder(maping.WithService("my-api"))

    r := gin.New()
    r.Use(mapinggin.MiddlewareWithRecorder(rec)) // above Recovery
    r.Use(gin.Recovery())

    // ... register routes ...

    srv := &http.Server{Addr: ":9090", Handler: r}
    // start srv in a goroutine ...

    // On shutdown: stop HTTP first, then flush the recorder.
    srv.Shutdown(ctx)
    rec.Shutdown(ctx)
}

See example/main.go for the complete runnable version including signal handling and correct shutdown ordering.

3. Activate

export MAPING_KEY=your-ingest-key

That is the only required configuration. Everything else is inferred.


No-op / zero-config guarantee

When MAPING_KEY is absent (or the resolved key is empty), NewRecorder returns a no-op recorder. Every method is safe and does nothing; no goroutine starts. The middleware is always safe to add to a codebase — activation is decoupled from the code change.

The client also fails open: transport errors, bad config values, and internal panics are logged once at Warn via the host's logger and never block or crash the host process.


Core API

// NewRecorder resolves config and returns a Recorder. Returns a no-op recorder
// when no ingest key is resolved.
func NewRecorder(opts ...Option) *Recorder

// Observe records one completed request. Safe on a no-op recorder.
// Safe for concurrent use.
func (*Recorder) Observe(rec Record)

// Shutdown flushes the last window and drains pending uploads.
// Call AFTER http.Server.Shutdown so no request is still writing a Record.
// Idempotent. Bounded by ctx.
func (*Recorder) Shutdown(ctx context.Context) error

SdkVersion = "0.1.0".


Gin adapter

Module: github.com/arhuman/maping/client/gin

// Middleware creates its own Recorder from opts. Use when you do not need
// to call Shutdown (fire-and-forget).
func Middleware(opts ...maping.Option) gin.HandlerFunc

// MiddlewareWithRecorder binds to a caller-owned Recorder so you control
// its lifecycle and can call rec.Shutdown on process exit.
func MiddlewareWithRecorder(rec *maping.Recorder) gin.HandlerFunc

Unmatched routes (where c.FullPath() returns "") are silently skipped to avoid emitting raw paths and exploding series cardinality.

Registration order is required: the mAPI-ng middleware must be registered before gin.Recovery(). See the quickstart above.


TLS and endpoint

The default endpoint is https://ingest.mapi-ng.dev (the hosted collector). An http:// endpoint is permitted for local or dev use and switches the transport to H2C (cleartext HTTP/2), which allows gRPC without TLS. Any other scheme is invalid config (falls back to no-op recorder with a one-time Warn log).


Configuration

Precedence for every setting: code option > env var > default.

Setting Code option Env var Default
Ingest key WithKey MAPING_KEY (none — absent means no-op)
Endpoint WithEndpoint MAPING_ENDPOINT https://ingest.mapi-ng.dev
Service name WithService MAPING_SERVICE binary name (via OTEL_SERVICE_NAME or os.Args[0])
Instance id WithInstance MAPING_INSTANCE $HOSTNAME env, else OS hostname (os.Hostname())
Flush window WithFlushWindow MAPING_FLUSH_SECONDS 10s

For full validation rules, precedence details, and invalid-config behavior see CONFIG.md.

Documentation

Overview

Package maping is the framework-agnostic Core of the mAPI-ng client: hosted, zero-config API observability for Go services (see docs/context.md). It owns config/env parsing, the in-process Summary aggregation (counters + a latency DDSketch per series), and the background Connect uploader.

The guiding contract is zero-config (CONFIG.md): the only required input is the ingest key. With no key resolved, NewRecorder returns a no-op recorder so adding mAPI-ng to a codebase is always safe — activation is a matter of flipping an env var, decoupled from the code change. The client fails open: setup and upload problems are logged (rate-limited) and surfaced in the dashboard, but never panic or block the host.

A framework adapter (e.g. client/gin) extracts the route template and final status after each request and calls Observe with a neutral Record.

Index

Constants

View Source
const SdkVersion = "0.1.0"

SdkVersion is the client SDK version reported in every Envelope/Handshake for server-side compatibility handling.

Variables

This section is empty.

Functions

func DownstreamElapsed added in v0.2.0

func DownstreamElapsed(ctx context.Context) time.Duration

DownstreamElapsed returns the total downstream time accumulated on ctx since WithDownstreamTracking installed the counter, or 0 when none was installed or no downstream call was recorded.

func WithDownstreamTracking added in v0.2.0

func WithDownstreamTracking(ctx context.Context) context.Context

WithDownstreamTracking installs a fresh downstream-time accumulator on ctx and returns the derived context. An adapter calls it once at request start; every outbound request whose context descends from this one then folds its round-trip time into the same counter (see NewRoundTripper), which DownstreamElapsed reads back at request end. Without this call the RoundTripper is a no-op, so wiring is opt-in and costs nothing when unused.

Types

type Config

type Config struct {
	Key         string
	Endpoint    string
	Service     string
	Instance    string
	FlushWindow time.Duration

	// Deploy identity. These are stored, low-cardinality dimensions stamped onto
	// every Envelope (not the client-side series key), so the dashboard can ask
	// "did release X regress this endpoint?".
	DeployVersion string
	DeployID      string
	Environment   string
	Region        string
}

Config is the resolved recorder configuration.

type NoStatusReason added in v0.2.0

type NoStatusReason uint32

NoStatusReason explains why a request finished without an HTTP status written (see classify → NO_STATUS). An adapter sets it on the Record; the Core records it only for NO_STATUS requests. The values mirror the proto NoStatusReason enum so the flush maps them straight through.

const (
	NoStatusUnspecified     NoStatusReason = 0
	NoStatusContextCanceled NoStatusReason = 1
	NoStatusContextDeadline NoStatusReason = 2
	NoStatusWriteError      NoStatusReason = 3
	NoStatusPanic           NoStatusReason = 4
	NoStatusOther           NoStatusReason = 5
)

The NoStatus* values enumerate why a request finished without an HTTP status. They mirror the proto NoStatusReason enum so the flush maps them straight through; NoStatusUnspecified is the zero value.

type Option

type Option func(*Config)

Option configures a Recorder. Options follow the functional-options pattern; a code option always beats the matching env var, which beats the default (CONFIG.md precedence).

func WithDeployID added in v0.2.0

func WithDeployID(id string) Option

WithDeployID overrides the build identity (beats MAPING_DEPLOY_ID). When left unset it defaults to the VCS revision from the build info, if available.

func WithDeployVersion added in v0.2.0

func WithDeployVersion(v string) Option

WithDeployVersion overrides the release version (beats MAPING_DEPLOY_VERSION).

func WithEndpoint

func WithEndpoint(endpoint string) Option

WithEndpoint overrides the collector URL (beats MAPING_ENDPOINT).

func WithEnvironment added in v0.2.0

func WithEnvironment(env string) Option

WithEnvironment overrides the deployment environment (beats MAPING_ENVIRONMENT).

func WithFlushWindow

func WithFlushWindow(d time.Duration) Option

WithFlushWindow overrides the flush window (beats MAPING_FLUSH_SECONDS).

func WithInstance

func WithInstance(instance string) Option

WithInstance overrides the instance id (beats MAPING_INSTANCE).

func WithKey

func WithKey(key string) Option

WithKey sets the ingest key (beats MAPING_KEY). The key encodes the tenant.

func WithRegion added in v0.2.0

func WithRegion(region string) Option

WithRegion overrides the deployment region (beats MAPING_REGION).

func WithService

func WithService(service string) Option

WithService overrides the logical service name (beats MAPING_SERVICE).

type Record

type Record struct {
	Method        string
	RouteTemplate string
	Status        int
	Duration      time.Duration
	ReqBytes      int64
	RespBytes     int64
	TraceID       string
	SpanID        string
	RequestID     string
	// ErrorClass is an optional app/framework-supplied error label. The Core
	// normalizes it to uppercase [A-Z0-9_] and bounds the distinct set per series;
	// empty means the request carried no error label.
	ErrorClass string
	// NoStatusReason explains a NO_STATUS request (Status <= 0). It is ignored for
	// requests that did write a status.
	NoStatusReason NoStatusReason
	// DownstreamDuration is the time this request spent waiting on downstream calls
	// (outbound HTTP), captured by the maping RoundTripper. Zero when the host does
	// not wire it or the request made no downstream calls.
	DownstreamDuration time.Duration
}

Record is the neutral, framework-agnostic input to Observe. An adapter builds one from a completed request. TraceID/SpanID/RequestID are best-effort exemplar breadcrumbs: empty when the adapter cannot extract them.

type Recorder

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

Recorder aggregates request Records in process and ships Summaries on a flush cycle. A recorder with no resolved key is a no-op: every method is safe and does nothing, and no goroutine runs.

func NewRecorder

func NewRecorder(opts ...Option) *Recorder

NewRecorder resolves config and returns a Recorder. If the resolved key is empty (zero-config safety), it returns a no-op recorder with no goroutine. On a transport construction failure it logs once at Warn and also returns the no-op recorder — the host is never affected (CONFIG.md fail-open).

func NewRecorderForTest

func NewRecorderForTest(tx Uploader) *Recorder

NewRecorderForTest returns a running Recorder wired to an injected Uploader, with a minimal config. It is the seam adapter tests use to substitute a fake transport and assert what was observed, without a live collector.

func (*Recorder) BeginRequest added in v0.5.0

func (r *Recorder) BeginRequest() func()

BeginRequest marks a request as in flight and returns a function that marks it done; adapters call `defer rec.BeginRequest()()` at handler entry. It tracks the window peak concurrency for the InstanceWindow in_flight gauge. Safe on a no-op recorder and for concurrent use.

func (*Recorder) Observe

func (r *Recorder) Observe(rec Record)

Observe records one completed request. It is safe on a no-op recorder and safe for concurrent use.

The whole body is wrapped in a panic recovery: a bug in aggregation must be invisible to the host request (the core "mAPI-ng failing is invisible to the host" guarantee, docs/context.md). A recovered panic is logged once at Warn (rate-limited) and the observation is dropped.

Steady state (the series already exists) is allocation-free: shard select + lock + in-place counter increments + sketch.Add (itself alloc-free). Only the first sighting of a new series allocates (map insert + sketch.New).

func (*Recorder) Shutdown

func (r *Recorder) Shutdown(ctx context.Context) error

Shutdown stops the uploader goroutine, does a final flush (pushing the last window onto the ring), then synchronously drains the ring — attempting to send every pending request, bounded by ctx. A graceful shutdown therefore does not lose buffered summaries: it best-effort ships them before returning (this is what the E2E test relies on). Once ctx expires it stops retrying and returns; any still-pending requests are abandoned. It is synchronous and idempotent. Hosts must call it AFTER their http.Server.Shutdown so no request is still writing a Record.

type RoundTripper added in v0.2.0

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

RoundTripper wraps an http.RoundTripper and folds each outbound request's round-trip time into the downstream accumulator carried by the request context (installed by WithDownstreamTracking). A host sets it as the Transport of the http.Client it uses for downstream calls, and propagates the inbound request context to those calls, so mAPI-ng can split an endpoint's own time from time spent blocked on a dependency. When the context carries no accumulator it adds nothing and simply delegates, so it is safe to use everywhere.

func NewRoundTripper added in v0.2.0

func NewRoundTripper(base http.RoundTripper) *RoundTripper

NewRoundTripper wraps base so outbound round-trip time is attributed to the caller's request. A nil base falls back to http.DefaultTransport, matching the convention of the standard library's own transports.

func (*RoundTripper) RoundTrip added in v0.2.0

func (rt *RoundTripper) RoundTrip(req *http.Request) (*http.Response, error)

RoundTrip measures the wrapped round trip and adds its duration to the request context's downstream accumulator. The duration is recorded even on error, since a slow failing dependency is exactly the downstream time worth surfacing.

type Uploader

type Uploader interface {
	Upload(ctx context.Context, req *mapingv1.UploadRequest) error
	Register(ctx context.Context, hs *mapingv1.Handshake) error
}

Uploader is the transport dependency the Recorder needs. The concrete implementation lives in internal/transport; adapters and tests may inject a fake to observe uploads without a live collector (lang-go DI).

Directories

Path Synopsis
Package adapterutil holds the framework-agnostic helpers shared by every mAPI-ng web adapter (gin today; echo/chi/beego/net-http next).
Package adapterutil holds the framework-agnostic helpers shared by every mAPI-ng web adapter (gin today; echo/chi/beego/net-http next).
gin module
internal
buffer
Package buffer implements a bounded, drop-oldest ring of pending UploadRequests for the mAPI-ng client's fail-open uploader (docs/context.md).
Package buffer implements a bounded, drop-oldest ring of pending UploadRequests for the mAPI-ng client's fail-open uploader (docs/context.md).
transport
Package transport wraps the generated Connect IngestService client with the wire policy mAPI-ng requires: the gRPC protocol (ADR-0002) over a dedicated HTTP client with an explicit timeout, zstd send-compression, and cleartext HTTP/2 (H2C) for local/dev http:// endpoints.
Package transport wraps the generated Connect IngestService client with the wire policy mAPI-ng requires: the gRPC protocol (ADR-0002) over a dedicated HTTP client with an explicit timeout, zstd send-compression, and cleartext HTTP/2 (H2C) for local/dev http:// endpoints.
Package sketch implements a hand-rolled DDSketch specialised for request latency aggregation, as decided in ADR-0001.
Package sketch implements a hand-rolled DDSketch specialised for request latency aggregation, as decided in ADR-0001.

Jump to

Keyboard shortcuts

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