Skip to main content
Yanked

This release has been yanked by its maintainers, and will be ignored by installers, except when explicitly specified.
Consider using release 0.5.0 instead.

wshtlib

Lightweight observability library for AWS Lambda and FastAPI. Zero external dependencies.

A focused alternative to aws-powertools — covers structured logging, CloudWatch metrics (EMF), request context propagation, and Lambda handler boilerplate. Nothing more.

Install

pip install wshtlib

FastAPI/Starlette middleware is optional:

pip install wshtlib[fastapi]

Usage

Lambda handler

Two decorators, one per invocation mode.

@bootstrapsynchronous invocations (API Gateway), where the return value is the response:

from wshtlib import bootstrap, get_logger

logger = get_logger("my-service")

@bootstrap
def handler(event, context):
    logger.info("invoked", path=event.get("path"))
    return {"statusCode": 200}

It handles:

  • Warming events ("source": "lambda-warming") — returns 200 early
  • Context init and structured log enrichment
  • Unhandled exceptions — logs error, returns 500

@workerasynchronous invocations (S3, EventBridge, SQS), where the return value is discarded:

from wshtlib import worker, get_logger

logger = get_logger("my-worker")

@worker
def handler(event, context):
    logger.info("processing", records=len(event["Records"]))

Same context init and structured error logging, but the exception is re-raised rather than swallowed — retries, on_failure destinations, the DLQ, and the Errors metric all depend on Lambda seeing the invocation fail. No warming-event handling.

Structured logging

from wshtlib import get_logger

logger = get_logger("my-service")
logger.info("user signed in", user_id="u_123", plan="pro")

Output is JSON to stdout, enriched with level, timestamp, service, logger, location, runtime fields, and Lambda context on invocation. location names the calling function and line. logger is the name passed to get_logger; service names the deployment unit and is resolved the same way metrics resolve it — see Service name.

Keyword arguments are the preferred spelling, but stdlib's extra={...} works too and lands in the same JSON entry; kwargs win if both supply the same key. Fields are also set as attributes on the LogRecord, so custom filters and %(field)s formatters can read them.

Field names are unrestricted — including msg, args, and level. Only exc_info, extra, stack_info, and stacklevel keep their stdlib meanings and cannot be used as fields. A field whose name collides with one the formatter owns (level, message, timestamp, service, logger, location, trace_id, exception, and the runtime/Lambda fields) is emitted with an extra_ prefix rather than replacing it:

logger.info("subscription renewed", level="premium")
# {"level": "INFO", ..., "message": "subscription renewed", "extra_level": "premium"}

This keeps an enrichment field from falsifying the record it was meant to enrich.

CloudWatch metrics (EMF)

from wshtlib.metrics import metrics

metrics.count("OrderPlaced")
metrics.put("Duration", 142.5, unit="Milliseconds")
metrics.flush()

metrics is a module-level MetricsContext instance, and @bootstrap/@worker flush it for you when the handler returns. For isolated contexts (e.g. per-request), instantiate MetricsContext() directly — one you create yourself is one you flush yourself.

A namespace is required. Pass MetricsContext(namespace=...) or set WSHT_METRICS_NAMESPACE; flush raises RuntimeError if neither does. It is resolved per flush, so setting the variable after import works.

Recording the same name more than once keeps every value rather than replacing it:

metrics.count("OrderPlaced")
metrics.count("OrderPlaced")
metrics.put("Duration", 50.0, unit="Milliseconds")
metrics.put("Duration", 60.0, unit="Milliseconds")
# {"OrderPlaced": [1.0, 1.0], "Duration": [50.0, 60.0], ...}

CloudWatch derives Sum, Average, Minimum, Maximum and SampleCount from those arrays, so a counter's total is its Sum. A name recorded once serialises as a bare number. Recording one name under two different units raises ValueError — a single metric definition carries a single unit, and picking one silently would mislabel real measurements.

EMF caps a document at 100 metric definitions and 100 values per metric, and CloudWatch rejects an over-limit document whole — losing every metric in it, not just the one that overflowed. Crossing either limit therefore flushes the accumulated metrics and starts a new document, so put may write before you call flush.

Service name

Logging and metrics resolve the service through resolve_service, taking the first that supplies a value:

  1. an explicit argument — MetricsContext(service=...)
  2. the request context — set_service("checkout")
  3. WSHT_SERVICE_NAME
  4. AWS_LAMBDA_FUNCTION_NAME, which Lambda always sets

A log line and a metric emitted from the same context therefore report the same service. If nothing supplies a value, logs fall back to the logger's own name and metrics omit the dimension rather than invent one.

Keep dimensions low-cardinality: every unique combination becomes its own CloudWatch metric and bills accordingly.

Request context

from wshtlib import get_context, set_user_id

set_user_id(claims["sub"])
ctx = get_context()  # {"trace_id": ..., "correlation_id": ..., "user_id": ...}

Context is stored in a ContextVar — safe for concurrent async handlers.

FastAPI middleware

from fastapi import FastAPI
from wshtlib.middleware import WshtlibMiddleware

app = FastAPI()
app.add_middleware(WshtlibMiddleware)

Initialises request context, logs method, path, status, duration_ms per request, and injects X-Trace-Id into the response.

Utilities

from wshtlib import require_env, require_https_url, require_secret

db_url = require_env("DATABASE_URL")          # raises RuntimeError if missing/empty
endpoint = require_https_url(require_env("API_URL"))  # raises ValueError if not https
api_key = require_secret("api/key")           # raises RuntimeError if missing/empty, cached

Environment variables

Every variable wshtlib reads is prefixed, so nothing else in the environment can steer it by accident.

Variable Default Description
WSHT_LOG_LEVEL INFO Logger level
WSHT_METRICS_NAMESPACE CloudWatch namespace. Required unless passed to MetricsContext
WSHT_SERVICE_NAME service dimension and log field, unless set explicitly
WSHT_ENVIRONMENT Added as a metrics dimension if set

Development

uv sync --group dev
uv run pytest
uv run mypy wshtlib
uv run ruff check wshtlib

License

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

wshtlib-0.4.0.tar.gz (68.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

wshtlib-0.4.0-py3-none-any.whl (15.9 kB view details)

Uploaded Python 3

File details

Details for the file wshtlib-0.4.0.tar.gz.

File metadata

  • Download URL: wshtlib-0.4.0.tar.gz
  • Upload date:
  • Size: 68.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for wshtlib-0.4.0.tar.gz
Algorithm Hash digest
SHA256 da04460946656af59076eb0c18de7d0464f765942e793694df3356547318b109
MD5 ba65de37e58cb465bcd1c58bd8a6e81f
BLAKE2b-256 77cc43b27488021d47863f66685bc86e50cb5637109e1158843c8f9c8e400b05

See more details on using hashes here.

Provenance

The following attestation bundles were made for wshtlib-0.4.0.tar.gz:

Publisher: ci.yml on pjosols/wshtlib

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file wshtlib-0.4.0-py3-none-any.whl.

File metadata

  • Download URL: wshtlib-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 15.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for wshtlib-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6c5913abfa336c06977bfb3f694732525343f215760ce2b85702a231d34c0984
MD5 3130ad611c58feeb024aab6105b59ad5
BLAKE2b-256 4b76c84e048e7d924a44f2d9ff83c60717e2269b3b6d25620dbe5589801ac1a3

See more details on using hashes here.

Provenance

The following attestation bundles were made for wshtlib-0.4.0-py3-none-any.whl:

Publisher: ci.yml on pjosols/wshtlib

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.5.0

2 files

This release

0.4.0 This release

2 files

0.3.0

2 files

0.2.0

2 files

0.1.2

2 files

0.1.1

2 files

0.1.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page