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.
@bootstrap — synchronous 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
@worker — asynchronous 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:
- an explicit argument —
MetricsContext(service=...) - the request context —
set_service("checkout") WSHT_SERVICE_NAMEAWS_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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
da04460946656af59076eb0c18de7d0464f765942e793694df3356547318b109
|
|
| MD5 |
ba65de37e58cb465bcd1c58bd8a6e81f
|
|
| BLAKE2b-256 |
77cc43b27488021d47863f66685bc86e50cb5637109e1158843c8f9c8e400b05
|
Provenance
The following attestation bundles were made for wshtlib-0.4.0.tar.gz:
Publisher:
ci.yml on pjosols/wshtlib
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
wshtlib-0.4.0.tar.gz -
Subject digest:
da04460946656af59076eb0c18de7d0464f765942e793694df3356547318b109 - Sigstore transparency entry: 2382583747
- Sigstore integration time:
-
Permalink:
pjosols/wshtlib@4eda3ef21433ba6a7011f5cc762666d7a6b4604e -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/pjosols
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@4eda3ef21433ba6a7011f5cc762666d7a6b4604e -
Trigger Event:
push
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6c5913abfa336c06977bfb3f694732525343f215760ce2b85702a231d34c0984
|
|
| MD5 |
3130ad611c58feeb024aab6105b59ad5
|
|
| BLAKE2b-256 |
4b76c84e048e7d924a44f2d9ff83c60717e2269b3b6d25620dbe5589801ac1a3
|
Provenance
The following attestation bundles were made for wshtlib-0.4.0-py3-none-any.whl:
Publisher:
ci.yml on pjosols/wshtlib
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
wshtlib-0.4.0-py3-none-any.whl -
Subject digest:
6c5913abfa336c06977bfb3f694732525343f215760ce2b85702a231d34c0984 - Sigstore transparency entry: 2382583831
- Sigstore integration time:
-
Permalink:
pjosols/wshtlib@4eda3ef21433ba6a7011f5cc762666d7a6b4604e -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/pjosols
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@4eda3ef21433ba6a7011f5cc762666d7a6b4604e -
Trigger Event:
push
-
Statement type: