Skip to main content

Local development gateway for AWS SAM Lambda functions with FastAPI + Swagger UI

Project description

lambda-local

Local development gateway for AWS SAM Lambda functions.
Reads your template.yml, discovers every HTTP route automatically, starts a FastAPI server with Swagger UI, assumes IAM roles, and hot-reloads on every file change — all from a single CLI command.

lambda-local start --all
╭──────────────────────────────────────────────────────────────────────────────╮
│                          Registered Routes                                   │
├────────────┬────────┬───────────────────────────────┬────────────────────────┤
│ Group      │ Method │ Path                          │ IAM Role               │
├────────────┼────────┼───────────────────────────────┼────────────────────────┤
│ buyer-api  │ POST   │ /api/v1/auth/signup           │ ambient                │
│ buyer-api  │ POST   │ /api/v1/auth/login            │ ambient                │
│ buyer-api  │ POST   │ /api/v1/auth/refresh          │ ambient                │
│ buyer-api  │ POST   │ /api/v1/otp/send              │ ambient                │
│ seller-api │ POST   │ /api/v1/seller/register       │ agarmart-seller-role   │
│ admin-api  │ POST   │ /api/v1/admin/auth/login      │ agarmart-admin-role    │
╰────────────┴────────┴───────────────────────────────┴────────────────────────╯

Swagger UI:   http://localhost:8000/docs
ReDoc:        http://localhost:8000/redoc

Table of Contents

  1. What Is lambda-local?
  2. Why Use It?
  3. How It Compares to sam local start-api
  4. Requirements
  5. Installation
  6. Quick Start
  7. CLI Commands
  8. Filtering & Targeting Functions
  9. IAM Role Support
  10. Auth Mock
  11. Environment Variables & Configuration
  12. Hot Reload
  13. Swagger UI & API Documentation
  14. Dependency Isolation
  15. How It Works Internally
  16. Project Structure
  17. Testing
  18. Troubleshooting

What Is lambda-local?

lambda-local is a developer tool that makes it fast and painless to run AWS SAM Lambda functions on your local machine — without Docker, without SAM CLI, and without slow cold starts.

It reads your template.yml, discovers all AWS::Serverless::Function resources that have API Gateway events, and dynamically registers them as routes in a live FastAPI web server. Every route is immediately browsable in Swagger UI, hot-reloads when you change any source file, and runs with the exact same IAM permissions your function would have in production.


Why Use It?

The problem with sam local start-api

  • Slow — spins up a Docker container for every request (3–10 seconds cold start)
  • Opaque — no Swagger UI, no structured request/response logging
  • Brittle — Docker dependency, port conflicts, environment variable setup is tedious
  • No IAM — cannot assume real IAM roles locally

What lambda-local gives you

Benefit Detail
Instant startup Python import, no Docker — functions start in milliseconds
📖 Auto Swagger UI Every route documented at /docs the moment the server starts
🔄 Hot reload Edit any handler .py or template.yml — server reloads automatically
🔑 Real IAM roles Calls STS AssumeRole using the Role: ARN from your template
🧪 Auth mock Bypass JWT auth in local testing with --mock-user-id
🌿 Smart filtering Run only buyer-api, or only Buyer* functions, or everything except Admin*
📦 Zero extra config Points at your existing template.yml — nothing else needed
🗂 Grouped Swagger Routes are tagged by folder group (buyer-api, seller-api, admin-api)
🔒 Credential safety IAM credentials injected per-invocation and cleaned up immediately after

How It Compares to sam local start-api

Capability lambda-local sam local start-api
Startup speed < 1 second 5–30 seconds (Docker pull + container)
Requires Docker No Yes
Swagger UI Yes — auto-generated No
Route grouping in docs Yes — by folder No
Hot reload Yes — uvicorn + watchdog No (manual restart)
IAM role assumption Yes — STS AssumeRole No
Auth mock Yes — --mock-user-id No
Per-function env vars Yes — env.json per logical ID Yes — env.json
CF intrinsic handling Graceful (strips/resolves) Full CloudFormation engine
Parity with Lambda runtime High (same Python interpreter) Exact (Docker image match)
Offline usage Yes Yes (with container cached)

Rule of thumb: Use lambda-local during active development for fast feedback loops.
Use sam local for final integration testing before deployment.


Requirements

  • Python 3.10+
  • AWS credentials configured (only needed for IAM role assumption — optional)

Installation

From PyPI

pip install lambda-local

From source (development mode)

cd lambda-local/
pip install -e ".[dev]"

Verify

lambda-local --help

Quick Start

# 1. Navigate to your SAM project
cd AgarMart-serverless-api/

# 2. Create a local env config (copy from example)
cp .env.example .env

# Fill in local values:
# MONGODB_URI=mongodb://localhost:27017
# REDIS_URL=redis://localhost:6379
# JWT_SECRET=my-dev-secret

# 3. Start ALL Lambda functions
lambda-local start --all

# 4. Open Swagger UI
open http://localhost:8000/docs

That's it. Every route from your template.yml is live and documented.


CLI Commands

start — Launch the gateway

lambda-local start [OPTIONS]
Option Short Default Description
--all -a false Run ALL functions (ignores other filters)
--function NAME -f Include only this function (repeatable)
--group NAME -g Include functions in this folder group (repeatable)
--match PATTERN -m Glob pattern for logical IDs e.g. Buyer* (repeatable)
--exclude PATTERN -e Glob pattern to exclude e.g. Admin* (repeatable)
--template PATH -t template.yml Path to SAM template
--host HOST 127.0.0.1 Bind address
--port PORT 8000 Bind port
--reload / --no-reload --reload Enable / disable hot reload
--env-file PATH auto Path to .env file
--env-json PATH auto Path to env.json
--mock-user-id ID Inject a fake user into requestContext.authorizer
--mock-role ROLE buyer Role for the mock user
--mock-email EMAIL dev@localhost Email for the mock user
--role-arn ARN Assume this IAM role for ALL functions
--no-role false Skip role assumption; use ambient AWS credentials

list — Print all function IDs

lambda-local list [--template PATH]

Lists every AWS::Serverless::Function logical ID in the template (alphabetically sorted).

AdminAuthFunction
AdminCategoriesFunction
BuyerAuthFunction
BuyerOtpFunction
SellerAuthFunction
SellerProductsFunction
...

routes — Print all HTTP routes

lambda-local routes [--template PATH] [--group NAME]
 Method  Path                              Group       Handler
 POST    /api/v1/auth/signup               buyer-api   auth/handler.lambda_handler
 POST    /api/v1/auth/login                buyer-api   auth/handler.lambda_handler
 GET     /api/v1/buyer/profile             buyer-api   profile/handler.lambda_handler
 POST    /api/v1/seller/register           seller-api  auth/handler.lambda_handler
 GET     /api/v1/seller/products           seller-api  products/handler.lambda_handler
 POST    /api/v1/admin/auth/login          admin-api   auth/handler.lambda_handler

Filter by group:

lambda-local routes --group seller-api

invoke — Call a function directly

lambda-local invoke FUNCTION_ID [OPTIONS]
Option Description
--event PATH JSON file with the full Lambda event payload
--template PATH Path to SAM template
--env-json PATH Path to env.json
--role-arn ARN Assume this IAM role before invoking
--no-role Skip role assumption

Basic invoke (event auto-built from route metadata):

lambda-local invoke BuyerAuthFunction

Invoke with a custom event file:

lambda-local invoke BuyerAuthFunction --event events/login.json

Example events/login.json:

{
  "httpMethod": "POST",
  "path": "/api/v1/auth/login",
  "headers": { "Content-Type": "application/json" },
  "body": "{\"email\": \"user@example.com\", \"password\": \"secret123\"}",
  "queryStringParameters": null,
  "pathParameters": null,
  "requestContext": {
    "stage": "local",
    "identity": { "sourceIp": "127.0.0.1" },
    "authorizer": {}
  }
}

Output:

{
  "statusCode": 200,
  "headers": { "Content-Type": "application/json" },
  "body": {
    "success": true,
    "data": {
      "access_token": "eyJ...",
      "refresh_token": "eyJ...",
      "token_type": "Bearer"
    }
  }
}

install — Set up per-function venvs

lambda-local install [OPTIONS]
Option Description
--group NAME Install only this group (repeatable)
--force Recreate venvs from scratch
--template PATH Path to SAM template

Creates an isolated virtual environment for each function group:

.lambda-local/
├── buyer-api/    ← buyer-api/requirements.txt installed here
├── seller-api/
└── admin-api/
lambda-local install           # all groups
lambda-local install -g buyer-api
lambda-local install --force   # rebuild from scratch

Add .lambda-local/ to your .gitignore.


Filtering & Targeting Functions

Run everything

lambda-local start --all

Single function

lambda-local start -f BuyerAuthFunction

Multiple specific functions

lambda-local start -f BuyerAuthFunction -f BuyerOtpFunction

By folder group

The group comes from the first segment of CodeUri:

BuyerAuthFunction:
  Properties:
    CodeUri: buyer-api/auth/   # group = "buyer-api"
lambda-local start --group buyer-api
lambda-local start --group buyer-api --group seller-api

Wildcard pattern

lambda-local start --match "Buyer*"      # everything starting with Buyer
lambda-local start --match "*Auth*"      # everything containing Auth
lambda-local start --match "Admin*"      # all admin functions

Exclude

lambda-local start --all --exclude "Admin*"
lambda-local start --all --exclude "*Job*"

Combine

# buyer-api but not the social auth endpoint
lambda-local start --group buyer-api --exclude "BuyerSocialAuthFunction"

IAM Role Support

In production, each Lambda function runs under an IAM execution role:

BuyerAuthFunction:
  Type: AWS::Serverless::Function
  Properties:
    Role: arn:aws:iam::123456789012:role/agarmart-buyer-role
    # or with CloudFormation Sub:
    Role: !Sub 'arn:aws:iam::${AWS::AccountId}:role/agarmart-${StageName}'

lambda-local replicates this by calling STS AssumeRole before each function invocation and injecting the temporary credentials as environment variables — so your function can call S3, SQS, DynamoDB, or any AWS service with real permissions.

How credentials are resolved

Request arrives
      │
      ▼
  Read role_arn from FunctionRoute
  (extracted from template.yml Role: property)
      │
      ▼
  --role-arn override? → use that instead
      │
      ▼
  Resolve CloudFormation Sub variables:
    ${AWS::AccountId}  →  real account ID via STS GetCallerIdentity
    ${AWS::Region}     →  boto3 default region
    ${AWS::Partition}  →  aws
      │
      ▼
  Check credential cache (cached for 1 hour)
  Cache hit → use cached credentials
  Cache miss → STS AssumeRole(...)
      │
      ▼
  Inject into environment:
    AWS_ACCESS_KEY_ID     = <temp>
    AWS_SECRET_ACCESS_KEY = <temp>
    AWS_SESSION_TOKEN     = <temp>
      │
      ▼
  Call lambda_handler(event, context)
      │
      ▼
  Restore original credentials

CLI options

# Per-function roles from template.yml (default behaviour)
lambda-local start --all

# Override: assume one role for ALL functions
lambda-local start --all \
  --role-arn arn:aws:iam::123456789012:role/my-dev-role

# Skip role assumption entirely
lambda-local start --all --no-role

# Set via environment variable (persists across reloads)
export LAMBDA_LOCAL_ROLE_ARN=arn:aws:iam::123456789012:role/my-dev-role
lambda-local start --all

# invoke command also supports role options
lambda-local invoke BuyerAuthFunction \
  --role-arn arn:aws:iam::123456789012:role/agarmart-dev

CloudFormation Sub variables resolved

Variable Resolved to
${AWS::AccountId} Real account ID via sts:GetCallerIdentity
${AWS::Region} boto3 default region or AWS_REGION env var
${AWS::Partition} aws
${StageName} (custom) Stripped (empty string — override with --role-arn)

Required developer permissions

To assume a role, your IAM user/role needs:

{
  "Effect": "Allow",
  "Action": "sts:AssumeRole",
  "Resource": "arn:aws:iam::123456789012:role/agarmart-*"
}

And the target role must list your IAM principal in its Trust Policy.

Graceful fallback

If AssumeRole fails (no permission, unresolvable ARN, no AWS credentials), lambda-local logs a warning and continues with ambient credentials. The server never crashes on a role assumption failure. Use --no-role to suppress the warning.


Auth Mock

Most Lambda functions check event["requestContext"]["authorizer"] for user identity (injected by the Lambda Authorizer in production). In local dev you don't want to generate real JWTs.

Use --mock-user-id to inject a fake user into every request:

lambda-local start --all \
  --mock-user-id user_abc123 \
  --mock-role buyer \
  --mock-email test@example.com

Every request will contain:

{
  "requestContext": {
    "authorizer": {
      "user_id": "user_abc123",
      "role":    "buyer",
      "email":   "test@example.com"
    }
  }
}

Your handler's get_current_user(event) succeeds without any JWT validation.

Test different roles simultaneously:

# Terminal 1 — buyer on port 8001
lambda-local start --group buyer-api --port 8001 \
  --mock-user-id buyer_001 --mock-role buyer

# Terminal 2 — admin on port 8002
lambda-local start --group admin-api --port 8002 \
  --mock-user-id admin_001 --mock-role admin

Environment Variables & Configuration

Auto-detection

lambda-local scans the template's directory automatically:

AgarMart-serverless-api/
├── template.yml
├── .env.local       ← checked first
├── .env             ← checked second
└── env.json         ← checked third

Explicit paths

lambda-local start --env-file .env.staging --env-json env.staging.json

.env format

MONGODB_URI=mongodb://localhost:27017
MONGODB_DB_NAME=agarmart
REDIS_URL=redis://localhost:6379
JWT_SECRET=dev-only-secret-256-bits
JWT_ACCESS_EXPIRE_MINUTES=15
JWT_REFRESH_EXPIRE_DAYS=30
AWS_S3_BUCKET=agarmart-dev-media
AWS_REGION=ap-south-1
CDN_BASE_URL=https://cdn.innobuz.com/app
BREVO_API_KEY=xkeysib-...
FAST2SMS_API_KEY=...
GOOGLE_CLIENT_ID=...
SQS_EMAIL_QUEUE_URL=https://sqs.ap-south-1.amazonaws.com/123456789012/agarmart-email-dev
SQS_SMS_QUEUE_URL=https://sqs.ap-south-1.amazonaws.com/123456789012/agarmart-sms-dev
SQS_PUSH_QUEUE_URL=https://sqs.ap-south-1.amazonaws.com/123456789012/agarmart-push-dev

env.json — SAM Parameters format

{
  "Parameters": {
    "MONGODB_URI": "mongodb://localhost:27017",
    "JWT_SECRET": "dev-secret",
    "REDIS_URL": "redis://localhost:6379"
  }
}

env.json — Per-function format

Different values per Lambda function:

{
  "BuyerAuthFunction": {
    "MONGODB_URI": "mongodb://localhost:27017/buyer_test"
  },
  "AdminAuthFunction": {
    "MONGODB_URI": "mongodb://localhost:27017/admin_test",
    "JWT_SECRET": "admin-specific-secret"
  }
}

CloudFormation intrinsic handling

template.yml often contains SSM references that can't be resolved locally:

JWT_SECRET: !Sub '{{resolve:ssm:/agarmart/prod/jwt_secret}}'

lambda-local strips these automatically and replaces them with empty strings — the server starts without errors. Override with real local values in your .env.

Priority order (highest wins)

1. Actual process environment (export MY_VAR=x before starting)
2. --env-file / .env.local / .env
3. --env-json / env.json (Parameters key)
4. Per-function env.json block

Hot Reload

Hot reload is on by default. The server watches:

File change Effect
Any **/*.py handler file Module reloaded; next request uses new code
template.yml Routes re-discovered; Swagger UI updates
env.json / .env Environment re-loaded
requirements.txt No auto-install; run lambda-local install then restart
# Reload is on by default
lambda-local start --all

# Disable (useful in CI)
lambda-local start --all --no-reload

Swagger UI & API Documentation

URL Interface
http://localhost:8000/docs Swagger UI — interactive API explorer
http://localhost:8000/redoc ReDoc — clean reference docs
http://localhost:8000/openapi.json Raw OpenAPI 3.0 schema

Routes are tagged by group (folder prefix) so Swagger UI shows:

► buyer-api
    POST  /api/v1/auth/signup
    POST  /api/v1/auth/login
    GET   /api/v1/buyer/profile

► seller-api
    POST  /api/v1/seller/register
    GET   /api/v1/seller/products

► admin-api
    POST  /api/v1/admin/auth/login
    GET   /api/v1/admin/dashboard/stats

If you started with --mock-user-id, protected routes work directly in the Swagger UI without needing a Bearer token.


How It Works Internally

lambda-local start --all
        │
        ▼
┌─────────────────────────────────────────────────────────┐
│  CLI  (cli.py)                                          │
│  Parse args → preview table → set LAMBDA_LOCAL_* env   │
│  → start uvicorn with factory=True reload=True          │
└────────────────────┬────────────────────────────────────┘
                     │ on every (re)start
                     ▼
┌─────────────────────────────────────────────────────────┐
│  Gateway  (gateway.py)  create_app()                    │
│  Read LAMBDA_LOCAL_* env vars                           │
│  → discover_routes()                                    │
│  → detect Lambda layer paths (shared/python/)           │
│  → create FastAPI app with Swagger tags                 │
│  → register one route per HTTP event                    │
└────────────────────┬────────────────────────────────────┘
                     │
                     ▼
┌─────────────────────────────────────────────────────────┐
│  Discovery  (discovery.py)                              │
│  CF-aware YAML loader (handles !Ref, !Sub, !GetAtt)     │
│  Extract: logical_id handler code_uri path method       │
│           group role_arn                                │
│  Apply --function / --group / --match / --exclude       │
└─────────────────────────────────────────────────────────┘

HTTP request arrives
        │
        ▼
┌─────────────────────────────────────────────────────────┐
│  Route handler closure  (gateway.py)                    │
│  → Invoker.invoke(route, request)                       │
└────────────────────┬────────────────────────────────────┘
                     │
                     ▼
┌─────────────────────────────────────────────────────────┐
│  Invoker  (invoker.py)                                  │
│  Build API Gateway event dict from FastAPI request      │
│  Submit to ThreadPoolExecutor                           │
│  (asyncio.run() inside handlers is safe in threads)     │
└────────────────────┬────────────────────────────────────┘
                     │ in thread
                     ▼
┌─────────────────────────────────────────────────────────┐
│  _invoke_in_thread                                      │
│                                                         │
│  1. Inject per-function env vars (env.json)             │
│                                                         │
│  2. IAM credential injection  (iam.py)                  │
│     • Resolve ${AWS::AccountId} etc. in role ARN        │
│     • STS AssumeRole (cached 1 hr, refresh 5 min early) │
│     • Set AWS_ACCESS_KEY_ID / SECRET / SESSION_TOKEN    │
│                                                         │
│  3. sys.path: prepend code_uri + layer paths            │
│                                                         │
│  4. importlib.util.spec_from_file_location              │
│     unique module key per logical_id                    │
│     (no naming conflicts across handlers)               │
│                                                         │
│  5. handler_fn(event, context)                          │
│                                                         │
│  6. Restore IAM credentials + env vars                  │
└─────────────────────────────────────────────────────────┘

Project Structure

lambda-local/
├── pyproject.toml              ← packaging, deps, tool config (Ruff, MyPy, pytest)
├── README.md
│
├── lambda_local/
│   ├── __init__.py
│   ├── cli.py                  ← Typer CLI (start, list, routes, invoke, install)
│   ├── config.py               ← .env / env.json loading + CF intrinsic stripping
│   ├── discovery.py            ← SAM template parser + FunctionRoute builder
│   ├── gateway.py              ← FastAPI app factory (create_app)
│   ├── iam.py                  ← STS AssumeRole + credential caching + injection
│   ├── installer.py            ← Per-group venv creator (lambda-local install)
│   ├── invoker.py              ← Lambda handler runner (thread pool executor)
│   ├── models.py               ← Pydantic models: FunctionRoute, InvocationResult
│   └── watcher.py              ← watchdog FileWatcher (debounced 500ms)
│
└── tests/
    ├── conftest.py             ← Fixtures: templates, handler files
    ├── test_discovery.py       ← Route discovery + filtering
    ├── test_invoker.py         ← Event building + handler invocation
    ├── test_gateway.py         ← FastAPI app + HTTP integration tests
    └── test_cli.py             ← CLI command tests (Typer TestClient)

Testing

pip install -e ".[dev]"

# Full suite with coverage
pytest

# Coverage HTML report
pytest --cov=lambda_local --cov-report=html
open htmlcov/index.html

# Specific file
pytest tests/test_discovery.py -v

# Single test
pytest tests/test_invoker.py::TestBuildEvent::test_mock_user_injected -v

Target coverage: > 90% (enforced by pytest config).


Troubleshooting

requires a different Python: 3.x not in '>=3.10'

Switch to Python 3.10+:

pyenv install 3.12.0 && pyenv local 3.12.0
pip install lambda-local

Template not found: template.yml

Run from the directory containing your template, or pass --template:

lambda-local start --all --template /path/to/AgarMart-serverless-api/template.yml

Handler file not found

Check that CodeUri is correct relative to template.yml and that Handler matches the actual filename:

CodeUri: buyer-api/auth/     # directory must exist
Handler: handler.lambda_handler  # buyer-api/auth/handler.py must exist

ImportError: No module named 'agarmart'

The shared layer isn't on the Python path. Declare it in template.yml:

SharedLayer:
  Type: AWS::Serverless::LayerVersion
  Properties:
    ContentUri: shared/   # shared/python/ is added to sys.path automatically

Or install it directly:

pip install -r shared/requirements.txt

ModuleNotFoundError: No module named 'motor'

Install the function's runtime deps:

pip install -r buyer-api/requirements.txt
pip install -r shared/requirements.txt

Or use the isolation approach:

lambda-local install --group buyer-api

iam_assume_role_failed warning

Causes and fixes:

Cause Fix
No AWS credentials aws configure or set AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY
No sts:AssumeRole permission Add the permission to your IAM user/role
Unresolvable ARN (${StageName} etc.) Pass --role-arn arn:aws:iam::ACCOUNT:role/NAME explicitly

To suppress the warning and use ambient credentials:

lambda-local start --all --no-role

Routes not updating after template edit

  1. Confirm --reload is active (it is by default)
  2. Check you're not in the .lambda-local/ directory (excluded from watching)
  3. Save the file again — some editors write atomically without triggering inotify
  4. Hard restart: Ctrl+Clambda-local start --all

License

MIT

[pypi] username = innux password = pypi-AgEIcHlwaS5vcmcCJDc5MjE1YTQ5LTEwNWItNDdhMS05MTI4LWFjNzY3MWVlYTFjMAACKlszLCJkMTI3ZGYyNS0wYjZhLTRiNjItODE4Yi1lODI5ODczOGRkMDUiXQAABiApc8NZEWfv0eaHXv1t1DeZv1Yix46pSmTkLPMq9a1CMA

Project details


Download files

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

Source Distribution

local_lambda_playground-0.1.0.tar.gz (42.7 kB view details)

Uploaded Source

Built Distribution

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

local_lambda_playground-0.1.0-py3-none-any.whl (36.0 kB view details)

Uploaded Python 3

File details

Details for the file local_lambda_playground-0.1.0.tar.gz.

File metadata

  • Download URL: local_lambda_playground-0.1.0.tar.gz
  • Upload date:
  • Size: 42.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.17

File hashes

Hashes for local_lambda_playground-0.1.0.tar.gz
Algorithm Hash digest
SHA256 6f75d994fe6bc66bed096ff61ae09f6401cc9ea2993e2b60798604f91c900d7c
MD5 d2d353c578d1ce4b0734c47a3e5b83b8
BLAKE2b-256 96a5b445f6c6145f7c1cf1f3793d47be6491f91ea0188fab931478fee87f9186

See more details on using hashes here.

File details

Details for the file local_lambda_playground-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for local_lambda_playground-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 90f55abfed2ce69e54e90a501ddc76f0ce53abbf6f87d43c4c4484304342e849
MD5 6bdbf6a383541e40cba2cdbd358d3857
BLAKE2b-256 6a784989af66fe557303ff7f3c273b3c9bed5c3163ea16e3ac2fad4464faa754

See more details on using hashes here.

Supported by

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