Skip to main content

<object-storage-proxy ⚡> Yet Another Object Storage Proxy

Project description

CI PyPI - Downloads PyPI - version

<object-storage-proxy ⚡> Yet Another Object Storage Reverse Proxy

📌 Note: This project is still under heavy development, and its APIs are subject to change.

Introduction

A fast and safe in-process reverse proxy server, based on Cloudflare's pingora, to reverse proxy AWS and IBM Cloud Object Storage buckets and integrate your Authentication and Authorization services.

  • Compatible with AWS SDK -> aws cli/boto3, polars, spark, datafusion, ...
  • Decouples frontend from backend authentication and authorization.
  • Python interface: pass in callables for credentials fetching, validation, lookup secret for access_key (with cache).
  • Compatibility Gateway between systems that support only 1 hmac credentials pair, and multi-credentials buckets backend.

Implementation

The Server Config accepts the following:

proxy_server_config = ProxyServerConfig(
    cos_map=cos_map,
    bucket_creds_fetcher=do_hmac_creds,
    validator=do_validation,
    http_port=6190,
    https_port=8443,
    threads=1,
    verify=False,
    hmac_keystore=hmac_keys,
    skip_signature_validation=False,
    hmac_fetcher=lookup_secret_key
)
argument description optional default value
cos_map bucket configuration, see below NA
bucket_creds_fetcher python callable to retrieve credentials for a given bucket, to return either api key or hmac key pair NA
validator python callable, validates access for a given token/bucket combination NA
http_port server listener port on http ✅ at least http_port or https_port, or both NA
https_port server listener port on https ✅ at least http_port or https_port, or both NA
threads number of service threads 1
verify ignore ssl verification errors on backend storage (IBM/AWS) (for dev purposes) False
hmac_keystore
skip_signature_validation ignore ssl verification errors on frontend (for dev purposes) False

The bucket dict contains for each bucket:

- endpoint host
- port
- api key (optional)
- hmac access key (optional)
- hmac secret key (optional)
- ttl (optional, default 300) -> keep this reasonably short, but size to your needs
cos_map = {
    "bucket1": {
        "host": "s3.eu-de.cloud-object-storage.appdomain.cloud",
        "port": 443,
        "ttl": 0
    },
    "bucket2": {
        "host": "s3.eu-de.cloud-object-storage.appdomain.cloud",
        "port": 443,
        "apikey": "apikey"
    },
    "proxy-bucket01": {
        "host": "s3.eu-de.cloud-object-storage.appdomain.cloud",
        "port": 443,
        "access_key": "<redacted>",
        "secret_key": "<redacted>",
        "ttl": 300
    },
    "proxy-aws-bucket01": {
        "host": "s3.eu-west-3.amazonaws.com",
        "region": "eu-west-3",
        "access_key": os.getenv("AWS_ACCESS_KEY"),
        "secret_key": os.getenv("AWS_SECRET_KEY"),
        "port": 443,
        "ttl": 300
    }    
}

The Python callables take two arguments: TODO: add prefix for fine-grained validation

- token: parsed from the original aws request's authorization header
- bucket: parsed from the uri path
    def your_credentials_fetcher(token: str, bucket: str) -> str
    def your_request_authorizer(token: str, bucket: str) -> bool

The Problem

secrets

IBM COS Storage is built in a way where buckets are grouped by a cos (Cloud Object Storage) instance. Access to a bucket is managed by either an api key or hmac secrets, configured on the cos instance.

endpoint

Each bucket has its own endpoint: <bucket_name>.s3..cloud-object-storage.appdomain.cloud:.

The port is not always different, though, but it might be. Depends on your implementation.

You can imagine managing multiple buckets across instances can become quite cumbersome, even with aws profiles etc.

solution

There are two ways to access a bucket: through virtual addressing style (bucket.ibm-cos-host:port) and path style (ibm-cos-host/bucket).

your client (aws s3 compatible) -> http(s)://this-proxy/bucket01 -> https://bucket01.s3.eu-de.cloud-object-storage.appdomain.cloud:443

  1. translate path style to virtual style
  2. abstract authentication & authorization

Pass in a function which maps bucket to instance (credentials), and a function to map bucket to port (endpoint)

request lifecycle

request stages

authentication & authorization

The advantage is we can plug in a python authentication function and another function for authorization, allowing for fine-grained control.

authentication

We use the standard aws hmac header and aws v4 request signing algorithm.

authorization

Pass in a callable from python which will be called from rust. This will be cached (ttl) for consecutive requests.

Examples

With local configuration.

~/.aws/config

[profile osp]
region = eu-west-3
output = json
services = osp-services
s3 =
    addressing_style = path

[services osp-services]
s3 =
  endpoint_url = http://localhost:6190

~/.aws/credentials

[osp]
aws_access_key_id = MYLOCAL123  # <-- this could be an internal client identifier, to fetch openid connect/oauth2 token or anything that makes sense for your business
aws_secret_access_key = nothingmeaningful # <-- private key to sign original request

Set up a minimal server implementation:

import json
import os
import random
import object_storage_proxy as osp

from dotenv import load_dotenv

from object_storage_proxy import start_server, ProxyServerConfig


_TRUES  = {"y", "yes", "t", "true", "on", "1"}
_FALSES = {"n", "no", "f", "false", "off", "0"}


def strtobool(val: str) -> bool:
    """Convert a string to True/False, raise ValueError otherwise."""
    v = val.lower()
    if v in _TRUES:
        return True
    if v in _FALSES:
        return False
    raise ValueError(f"invalid truth value {val!r}")


def do_api_creds(token: str, bucket: str) -> str:
    """Fetch credentials (ro, rw, access_denied) for the given bucket, depending on the token. """
    apikey = os.getenv("COS_API_KEY")
    if not apikey:
        raise ValueError("COS_API_KEY environment variable not set")
    
    print(f"Fetching credentials for {bucket}...")
    return apikey


def do_hmac_creds(token: str, bucket: str) -> str:
    """ Fetch HMAC credentials (ro, rw, access_denied) for the given bucket, depending on the token """
    access_key = os.getenv("ACCESS_KEY")
    secret_key = os.getenv("SECRET_KEY")
    if not access_key or not secret_key:
        raise ValueError("ACCESS_KEY or SECRET_KEY environment variable not set")
        
    print(f"Fetching HMAC credentials for {bucket}...")

    return json.dumps({
        "access_key": access_key,
        "secret_key": secret_key
    })

def lookup_secret_key(access_key: str) -> str | None:
    # get all environment variables ending in ACCESS_KEY
    access_keys = [{key:value} for key, value in os.environ.items() if key.endswith("ACCESS_KEY") and value==access_key ]

    if len(access_keys) > 0:
        access_key_var = next((k for k, v in access_keys[0].items() if v == access_key), None)

        secret_key_var = access_key_var.replace("ACCESS_KEY", "SECRET_KEY")
        return os.getenv(secret_key_var, None)
    else:
        print(f"no access keys found for : {access_key}")


def do_validation(token: str, bucket: str) -> bool:
    """ Authorize the request based on token for the given bucket. 
        You can plug in your own authorization service here.
        The token is a client identifier used to fetch an authorization token and further authenticate/authorize.
        The bucket is the bucket name.
        The function should return True if the request is authorized, False otherwise.
    """

    print(f"PYTHON: Validating headers: {token} for {bucket}...")
    # return random.choice([True, False])
    return True


def main() -> None:
    load_dotenv()

    counting = strtobool(os.getenv("OSP_ENABLE_REQUEST_COUNTING", "false"))

    if counting:
        osp.enable_request_counting()
        print("Request counting enabled")

    apikey = os.getenv("COS_API_KEY")
    if not apikey:
        raise ValueError("COS_API_KEY environment variable not set")

    cos_map = {
        "bucket1": {
            "host": "s3.eu-de.cloud-object-storage.appdomain.cloud",
            "region": "eu-de",
            "port": 443,
            "apikey": apikey,
            "ttl": 0
        },
        "bucket2": {
            "host": "s3.eu-de.cloud-object-storage.appdomain.cloud",
            "region": "eu-de",
            "port": 443,
            "apikey": apikey
        },
        "proxy-bucket01": {
            "host": "s3.eu-de.cloud-object-storage.appdomain.cloud",
            "region": "eu-de",
            # "access_key": os.getenv("ACCESS_KEY"),
            # "secret_key": os.getenv("SECRET_KEY"),
            "port": 443,
            "ttl": 300
        },
        "proxy-bucket05": {
            "host": "s3.eu-de.cloud-object-storage.appdomain.cloud",
            "region": "eu-de",
            "access_key": os.getenv("PROXY_BUCKET05_ACCESS_KEY"),
            "secret_key": os.getenv("PROXY_BUCKET05_SECRET_KEY"),
            "port": 443,
            "ttl": 300
        },
        "proxy-aws-bucket01": {
            "host": "s3.eu-west-3.amazonaws.com",
            "region": "eu-west-3",
            "access_key": os.getenv("AWS_ACCESS_KEY"),
            "secret_key": os.getenv("AWS_SECRET_KEY"),
            "port": 443,
            "ttl": 300
        }
    }

    hmac_keys= [
        # {
        #     "access_key": os.getenv("LOCAL_ACCESS_KEY"),
        #     "secret_key": os.getenv("LOCAL_SECRET_KEY")
        # },
        {
            "access_key": os.getenv("LOCAL2_ACCESS_KEY"),
            "secret_key": os.getenv("LOCAL2_SECRET_KEY")
        },

    ]

    ra = ProxyServerConfig(
        cos_map=cos_map,
        bucket_creds_fetcher=do_hmac_creds,
        validator=do_validation,
        http_port=6190,
        # https_port=8443,
        threads=1,
        # verify=False,
        hmac_keystore=hmac_keys,
        skip_signature_validation=False,
        hmac_fetcher=lookup_secret_key
    )

    start_server(ra)


if __name__ == "__main__":
    main()

Run with aws-cli (but could be anything compatible with the aws s3 api like polars, spark, presto, ...):

$ aws s3 ls s3://proxy-bucket01/ --recursive --summarize --human-readable --profile osp
2025-04-17 17:45:30   33 Bytes README.md
2025-04-17 17:48:04   33 Bytes README2.md

Total Objects: 2
   Total Size: 66 Bytes
$

Server output:

$ uv run python test_server.py
2025-04-19T13:19:54.402023+02:00  INFO object_storage_proxy: Logger initialized; starting server on http port 6190 and https port 8443
2025-04-19T13:19:54.402361+02:00  INFO object_storage_proxy: Bucket creds fetcher provided: Py(0x100210680)
Fetching credentials for bucket01...
2025-04-19T13:19:54.402485+02:00  INFO object_storage_proxy: Callback returned: Kn2t...
[src/lib.rs:327:5] &run_args.cos_map = Py(
    0x000000010061aa00,
)
2025-04-19T13:19:54.403738+02:00  INFO pingora_core::server: Bootstrap starting
2025-04-19T13:19:54.403852+02:00  INFO pingora_core::server: Bootstrap done
2025-04-19T13:19:54.424489+02:00  INFO pingora_core::server: Server starting
PYTHON: Validating headers: MYLOCAL123 for proxy-bucket01...
2025-04-19T13:19:58.124729+02:00  INFO object_storage_proxy::utils::validator: Callback returned: false
PYTHON: Validating headers: MYLOCAL123 for proxy-bucket01...
2025-04-19T13:20:00.919320+02:00  INFO object_storage_proxy::utils::validator: Callback returned: true
2025-04-19T13:20:01.181775+02:00  INFO object_storage_proxy::credentials::secrets_proxy: No cached token found for proxy-bucket01, fetching ...
2025-04-19T13:20:01.181859+02:00  INFO object_storage_proxy::credentials::secrets_proxy: Fetching bearer token for the API key
2025-04-19T13:20:01.739385+02:00  INFO object_storage_proxy::credentials::secrets_proxy: Received access token
2025-04-19T13:20:01.739600+02:00  INFO object_storage_proxy::credentials::secrets_proxy: Fetched new token for proxy-bucket01
2025-04-19T13:20:01.739668+02:00  INFO object_storage_proxy: Sending request to upstream: https://proxy-bucket01.s3.eu-de.cloud-object-storage.appdomain.cloud/?list-type=2&prefix=&encoding-type=url
2025-04-19T13:20:01.739922+02:00  INFO object_storage_proxy: Request sent to upstream.

test

See the included python test script.

Create self-signed certificates and export the environment variables:

openssl req -x509 -newkey rsa:4096 -sha256 -nodes \
        -keyout key.pem -out cert.pem -days 365 -subj "/CN=localhost"
export TLS_CERT_PATH=/full/path/cert.pem
export TLS_KEY_PATH=/full/path/key.pem

Status

  • pingora proxy implementation
  • pass in credentials handler (which may return either api key string or json string with access_key and secret_key )
  • cache credentials
  • pass in bucket/instance and bucket/port config
  • split in workspace crate with core, cli and python crates (too many specifics for python)
  • config mgmt
  • cache authorization (with optional ttl)
  • http frontend (optional)
  • https frontend (supports HTTP/2) (optional)
  • configurable request counting
  • call the api key fetcher callback only once, save to cos map
  • config for #threads in ProxyServerConfig
  • also pass path and method to python callbacks and cache by token/bucket/path/method (identity based access/cache)
  • option to disable upstream/peer certificate validation (for development, not production!)
  • expose pingora proxy server and services configuration to python
  • drop proxy headers (x-forwarded-proto, x-forwarded-host, ..) for signing

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

object_storage_proxy-0.3.4.tar.gz (72.3 kB view details)

Uploaded Source

Built Distributions

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

object_storage_proxy-0.3.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (7.8 MB view details)

Uploaded CPython 3.13manylinux: glibc 2.17+ x86-64

object_storage_proxy-0.3.4-cp313-cp313-macosx_11_0_arm64.whl (5.0 MB view details)

Uploaded CPython 3.13macOS 11.0+ ARM64

object_storage_proxy-0.3.4-cp313-cp313-macosx_10_12_x86_64.whl (5.3 MB view details)

Uploaded CPython 3.13macOS 10.12+ x86-64

object_storage_proxy-0.3.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (7.8 MB view details)

Uploaded CPython 3.12manylinux: glibc 2.17+ x86-64

object_storage_proxy-0.3.4-cp312-cp312-macosx_11_0_arm64.whl (5.0 MB view details)

Uploaded CPython 3.12macOS 11.0+ ARM64

object_storage_proxy-0.3.4-cp312-cp312-macosx_10_12_x86_64.whl (5.3 MB view details)

Uploaded CPython 3.12macOS 10.12+ x86-64

object_storage_proxy-0.3.4-cp311-cp311-musllinux_1_2_x86_64.whl (7.6 MB view details)

Uploaded CPython 3.11musllinux: musl 1.2+ x86-64

object_storage_proxy-0.3.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (7.8 MB view details)

Uploaded CPython 3.11manylinux: glibc 2.17+ x86-64

object_storage_proxy-0.3.4-cp311-cp311-macosx_11_0_arm64.whl (5.0 MB view details)

Uploaded CPython 3.11macOS 11.0+ ARM64

object_storage_proxy-0.3.4-cp311-cp311-macosx_10_12_x86_64.whl (5.3 MB view details)

Uploaded CPython 3.11macOS 10.12+ x86-64

object_storage_proxy-0.3.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (7.8 MB view details)

Uploaded CPython 3.10manylinux: glibc 2.17+ x86-64

File details

Details for the file object_storage_proxy-0.3.4.tar.gz.

File metadata

  • Download URL: object_storage_proxy-0.3.4.tar.gz
  • Upload date:
  • Size: 72.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: maturin/1.8.3

File hashes

Hashes for object_storage_proxy-0.3.4.tar.gz
Algorithm Hash digest
SHA256 7671443a6b10ed3cf89b95bece8040a431c4da58ac02b2af80d6639c4527fb6b
MD5 662dedcc58caa856ee95cc31a609572c
BLAKE2b-256 cf891230b2a85de66aac9d1e52e7e7c0c259651f119b46fedd6ac6cb09f8f51c

See more details on using hashes here.

File details

Details for the file object_storage_proxy-0.3.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for object_storage_proxy-0.3.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 1ccb497fb643a35132eff452964e3858f2df2a2dcab03baec45a3ad0b8f9ab60
MD5 85c827c74f1c3c6af50489d8334eedd9
BLAKE2b-256 b7dde1949e36a2bfaadc8a966cb7eaa2863bc646d2ed85513634d12a5d267ce6

See more details on using hashes here.

File details

Details for the file object_storage_proxy-0.3.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for object_storage_proxy-0.3.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 14fdd8f49238386655a1f3b15a4933ac4819cd22da1f824262abc268f4000799
MD5 cbf6418605dfe383c73678c3284c98c2
BLAKE2b-256 b2a5712cf893a22d21692d1d4a5303e182f4d83412c834594b6df208f9bac6e6

See more details on using hashes here.

File details

Details for the file object_storage_proxy-0.3.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for object_storage_proxy-0.3.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 004eef38758da6af45a5bff64bb37eb74d84f103c8c04ba45680c9156e83d2b0
MD5 f8776eb9f08a12c5c4e0aaa3aba2c883
BLAKE2b-256 05ac02d52d32af0c85210ca4b37c32c88ee9851b8d74f60cc11f789fa1c5f131

See more details on using hashes here.

File details

Details for the file object_storage_proxy-0.3.4-cp313-cp313-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for object_storage_proxy-0.3.4-cp313-cp313-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 2a0df987609a925f2f226f2aefa70eaa8aa28610c72179024f09092b1c301145
MD5 3f02919a9b04bf53f1d62a0bd3116c3b
BLAKE2b-256 7d7851d0f3da162b4597c982068d77f79ba1630cd2ea2a8c02535b67c4aa5f21

See more details on using hashes here.

File details

Details for the file object_storage_proxy-0.3.4-cp313-cp313-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for object_storage_proxy-0.3.4-cp313-cp313-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 39aa295f4665d0bbb34f9a86dbed95658da3e06732c57b559ebca3fb0d44e8c7
MD5 653ac4b9ba3173b96dfbca3d5597b405
BLAKE2b-256 a490311e81e2ca1412d1545e381db8f11e09983b4fa0996ce6d4a18cb77aea16

See more details on using hashes here.

File details

Details for the file object_storage_proxy-0.3.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for object_storage_proxy-0.3.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 89e94d62027b28eb2c09933d0098e8de14b4d767d5799c1fad944265cc072d12
MD5 33b43b20c589f36cd475da37e15503b8
BLAKE2b-256 29a496312832ae3ee031f1d9d14fd73d7b98a1c952801a104c983329491cf779

See more details on using hashes here.

File details

Details for the file object_storage_proxy-0.3.4-cp312-cp312-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for object_storage_proxy-0.3.4-cp312-cp312-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 8be4398689aaaee39e5d7d5d1902a35d4d38eb778185fc7a7830a2f1622acec2
MD5 f2b7453de6a3b97f86639c3065c7c586
BLAKE2b-256 ab921f5e250593bda0ad7fabe71d54fcdcb1960fb54911cdcb5aca32784e176c

See more details on using hashes here.

File details

Details for the file object_storage_proxy-0.3.4-cp312-cp312-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for object_storage_proxy-0.3.4-cp312-cp312-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 fb59ce55b36494885578690f340289a93485e9c10a21b954492a7c14311cf3a5
MD5 973ad91c9dd9a3684150b883cbade6be
BLAKE2b-256 6546bded41332e4c3122c8d69240844a34c33369aa3e81e77e652515ccd849e0

See more details on using hashes here.

File details

Details for the file object_storage_proxy-0.3.4-cp311-cp311-musllinux_1_2_x86_64.whl.

File metadata

File hashes

Hashes for object_storage_proxy-0.3.4-cp311-cp311-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 7c807419222651e22c4a8107f2452813fa6594f4bb034a1579f4416afb93dd12
MD5 0cc490c740b71484dc5c5bdacd29cc4a
BLAKE2b-256 49b0ce4501f20124aefc2bed6621b1f49efe682862c6e780dd8d6c05d36381ea

See more details on using hashes here.

File details

Details for the file object_storage_proxy-0.3.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for object_storage_proxy-0.3.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 d583ce4fad7280eedf9435a9576907890a3071de87000c18ea9d864907824491
MD5 836fc481087438fc4d8185e75392ee8c
BLAKE2b-256 78a9c2e07a5645768adc409bc05e0ee995f0dba5d74a39761ceffd3e11f79a53

See more details on using hashes here.

File details

Details for the file object_storage_proxy-0.3.4-cp311-cp311-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for object_storage_proxy-0.3.4-cp311-cp311-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 a7e272c3b7f20f53052f811b3435763584e14eb58da29124eafd6876c99f6a4b
MD5 de9270d015289b7b08ec04809493e796
BLAKE2b-256 9c5546ad4c010f54f2a63a2f82e79acffce9744050b71751dd4668d6a76030dc

See more details on using hashes here.

File details

Details for the file object_storage_proxy-0.3.4-cp311-cp311-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for object_storage_proxy-0.3.4-cp311-cp311-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 d75522e90cbfe12a5fed68c0da9eaa0c43d9bd3f02ae0e54895d98242482c21b
MD5 f4495ccab8dc89739d28deb113144d66
BLAKE2b-256 e9094b59f99bc346b8c20279da3e614330f13e5b17930c88a229088926ec713c

See more details on using hashes here.

File details

Details for the file object_storage_proxy-0.3.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for object_storage_proxy-0.3.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 0aaab73ade71d79d49365f982c9a39ff0a05f7a8625ecd9eed88b33ffdd154ce
MD5 356ffcf0ffae77b692693f7c12b9776d
BLAKE2b-256 6246bf8f02594639188848fd32494a3a7272afbfb5ba87254604ca797fbbe1e4

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