Skip to main content

Minimal, fast templates for Python

Project description

Nano Templates

Minimal, fast, non-evaluating text templates for Python.

PyPI - License PyPI - Python Version PyPI - Version

Install

Use pip, or your favorite package manager:

python -m pip install nano-template

Example

import nano_template as nt

template = nt.parse("Hello, {{ you }}!")
print(template.render({"you": "World"}))  # Hello, World!

About

Nano Template is a small text templating engine, written as a Python C extension, with syntax familiar to anyone who's used Jinja/Minijinja, Django templates or Liquid.

Unlike those popular template engines, Nano Template forces you to keep application logic out of template text by implementing a reduced feature set. In this scenario, template authors and application developer are likely to be the same person (or team of people).

Syntax

[!NOTE] In Nano templates, there are no filters or tests, no relational or membership operators, and we don't have for loop helpers or {% break %} and {% continue %}.

Instead, you should process your data in Python before rendering a template, or use Minijinja.

Available tags are {% if %}, {% elif %}, {% else %} and {% for %}.

Variables

<div>{{ some.variable }}</div>
<p>{{ other["variable with spaces or special characters"] }}</p>

Conditions

{% if some.variable %}
  more markup
{% elif another.variable %}
  alternative markup
{% else %}
  default markup
{% endif %}

Loops

{% for x in y %}
  more markup with {{ x }}
{% else %}
  default markup (y was empty or not iterable)
{% endfor %}

Logical operators

Logical operators and, or and not use Python truthiness and precedence rules, and terms can be grouped with parentheses.

{% if not a and b %}
  markup with {{ b }} and {{ c }}.
{% endif %}

Logical and and or have last value semantics.

Hello, {{ user.name or "guest" }}!

Strings

String literals and quoted variable path segments can use single or double quotes, and allow JSON-style escape sequences.

{{ greeting or "Hi \uD83D\uDE00!" }}

Output:

Hi 😀!

Whitespace control

Control whitespace before and after markup delimiters with - and ~. ~ will remove newlines but retain space and tab characters. - strips all whitespace.

<ul>
{% for x in y ~%}
  <li>{{ x }}</li>
{% endfor -%}
</ul>

API

render

render(source, data) is a convenience function that parses and immediately renders a template to a string. Use this for testing or when you know you'll be rendering the template just the once.

import nano_template as nt

print(nt.render("Hello, {{ you }}!", {"you", "World"}))

render also accepts serializer and undefined keyword arguments.

parse

parse(source) parses template text and returns an instance of Template for later rendering with the render(self, data) method.

import nano_template as nt

template = nt.parse("Hello, {{ you }}!")
print(template.render({"you": "World"}))  # Hello, World!
print(template.render({"you": "Sue"}))  # Hello, Sue!

parse also accepts serializer and undefined keyword arguments.

Serializing objects

By default, when outputting an object with {{ and }}, lists, dictionaries and tuples are rendered in JSON format. For all other objects we render the result of str(obj).

You can change this behavior by passing a callable to parse or render as the serializer keyword argument. The callable should accept an object and return its string representation suitable for output.

This example shows how one might define a serializer that can dump data classes with json.dumps.

import json
from dataclasses import asdict
from dataclasses import dataclass
from dataclasses import is_dataclass

import nano_template as nt

@dataclass
class SomeData:
    foo: str
    bar: int

def json_default(obj: object) -> object:
    if is_dataclass(obj) and not isinstance(obj, type):
        return asdict(obj)
    raise TypeError(f"Object of type {obj.__class__.__name__} is not JSON serializable")

def my_serializer(obj: object) -> str:
    return (
        json.dumps(obj, default=json_default)
        if isinstance(obj, (list, dict, tuple))
        else str(obj)
    )


template = nt.parse("{{ some_object }}", serializer=my_serializer)
data = {"some_object": [SomeData("hello", 42)]}

print(template.render(data))  # [{"foo": "hello", "bar": 42}]

Undefined variables

When a template variable or property can't be resolved, an instance of the undefined type is used instead. That is, an instance of nano_template.Undefined or a subclass of it.

The default undefined type renders nothing when output, evaluates to False when tested for truthiness and is an empty iterable when looped over. You can pass an alternative undefined type as the undefined keyword argument to parse or render to change this behavior.

Here we use the built-in StrictUndefined.

import nano_template as nt

t = nt.parse("{{ foo.nosuchthing }}", undefined=nt.StrictUndefined)

print(t.render({"foo": {}}))
# nano_template._exceptions.UndefinedVariableError: 'foo.nosuchthing' is undefined
#   -> '{{ foo.nosuchthing }}':1:3
#   |
# 1 | {{ foo.nosuchthing }}
#   |    ^^^ 'foo.nosuchthing' is undefined

Or you can implement you own.

from typing import Iterator
import nano_template as nt


class MyUndefined(nt.Undefined):
    def __str__(self) -> str:
        return "<MISSING>"

    def __bool__(self) -> bool:
        return False

    def __iter__(self) -> Iterator[object]:
        yield from ()


t = nt.parse("{{ foo.nosuchthing }}", undefined=MyUndefined)

print(t.render({"foo": {}}))  # <MISSING>

Preliminary benchmark

TODO: move this

(On an M2 Mac Mini with Python 3.13)

$ python scripts/benchmark.py
(001) 5 rounds with 10000 iterations per round.
parse c ext                   : best = 0.092587s | avg = 0.092743s
parse pure py                 : best = 2.378554s | avg = 2.385293s
just render c ext             : best = 0.061812s | avg = 0.061850s
just render pure py           : best = 0.314468s | avg = 0.315076s
just render jinja2            : best = 0.170373s | avg = 0.170706s
just render minijinja         : best = 0.454723s | avg = 0.457256s
parse and render ext          : best = 0.155797s | avg = 0.156455s
parse and render pure py      : best = 2.733121s | avg = 2.745028s
parse and render jinja2       : <with caching disabled, I got bored waiting>
parse and render minijinja    : best = 0.705995s | avg = 0.707589s
$ python scripts/benchmark_format.py
(002) 5 rounds with 1000000 iterations per round.
render template               : best = 0.413830s | avg = 0.419547s
format string                 : best = 0.375050s | avg = 0.375237s

Contributing

TODO

Notes to self

TODO: move this

Python Debug Build

Build Python in debug mode from source in ~/python-debug:

./configure --prefix=$HOME/python-debug --with-pydebug

Tell uv to use the debug build instead of system or downloaded Python version:

uv sync -p <path/to/python debug binary>

Rebuild the extension in debug mode:

uv run python setup.py build_ext --inplace --force --debug

PYTHONMALLOC=debug

Use PYTHONMALLOC=debug python dev.py to activate Python's debug memory allocator, which inserts guard bytes, fills memory with known patterns, and performs validation to catch buffer overflows, use-after-free, and double frees when using Python memory APIs.

ASAN does not seem to catch these.

Manual reference leak check

(With a debug Python build.)
(A debug build will also report negative reference counts.)

If the delta is small and consistent, it's probably not a ref count leak.

import sys
from nano_template import render

before = sys.gettotalrefcount()

for i in range(10000):
    render("{% if a %}a{% else %}c{% endif %}", {"a": False, "b": False})
    if i % 1000 == 0:
        print(f"Iteration {i}, total refcount={sys.gettotalrefcount()}")

after = sys.gettotalrefcount()
print("Refcount delta:", after - before)

Detecting leaks from reference cycles

(We shouldn't be creating any reference cycles in this project.)

import json
from nano_template import parse

import gc

# gc.set_debug(gc.DEBUG_LEAK)
# gc.set_debug(gc.DEBUG_SAVEALL)

with open("tests/fixtures/001/data.json") as fd:
    data = json.load(fd)

with open("tests/fixtures/001/template.txt") as fd:
    source = fd.read()

gc.disable()
gc.collect()
before = len(gc.get_objects())

parse(source).render(data)

gc.collect()
after = len(gc.get_objects())
print("Object delta:", after - before)
gc.enable()

# leaked = [o for o in gc.garbage if type(o).__module__ == "_nano_template"]
# for o in leaked:
#     print(o, type(o))

ABI 3 Audit

Note that abi3audit ignores target ABI version when auditing .so files.

  • Build a wheel locally with python setup.py bdist_wheel
  • Run abi3audit dist/<NAME>.whl --verbose

Example successful output:

[17:55:59] 💁 nano_template-0.1.0-cp39-abi3-linux_x86_64.whl: 1 extensions scanned; 0 ABI version mismatches and 0 ABI violations found

Release

  1. Manually run the Build wheels workflow.
  2. Clean ./dist/.
  3. Download workflow artifacts.
  4. Extract artifacts to ./dist.
  5. Build sdist with uv run python -m build --sdist
  6. Upload uv run python -m twine upload dist/*

License

nano-template is distributed under the terms of the MIT license.

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

nano_template-0.1.1.tar.gz (46.4 kB view details)

Uploaded Source

Built Distributions

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

nano_template-0.1.1-cp314-cp314t-win_arm64.whl (35.5 kB view details)

Uploaded CPython 3.14tWindows ARM64

nano_template-0.1.1-cp314-cp314t-win_amd64.whl (38.0 kB view details)

Uploaded CPython 3.14tWindows x86-64

nano_template-0.1.1-cp314-cp314t-win32.whl (35.0 kB view details)

Uploaded CPython 3.14tWindows x86

nano_template-0.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl (150.5 kB view details)

Uploaded CPython 3.14tmusllinux: musl 1.2+ x86-64

nano_template-0.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl (151.9 kB view details)

Uploaded CPython 3.14tmusllinux: musl 1.2+ ARM64

nano_template-0.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl (153.0 kB view details)

Uploaded CPython 3.14tmanylinux: glibc 2.17+ ARM64manylinux: glibc 2.28+ ARM64

nano_template-0.1.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl (149.9 kB view details)

Uploaded CPython 3.14tmanylinux: glibc 2.28+ x86-64manylinux: glibc 2.5+ x86-64

nano_template-0.1.1-cp314-cp314t-macosx_11_0_arm64.whl (35.9 kB view details)

Uploaded CPython 3.14tmacOS 11.0+ ARM64

nano_template-0.1.1-cp314-cp314t-macosx_10_15_x86_64.whl (35.8 kB view details)

Uploaded CPython 3.14tmacOS 10.15+ x86-64

nano_template-0.1.1-cp39-abi3-win_arm64.whl (33.9 kB view details)

Uploaded CPython 3.9+Windows ARM64

nano_template-0.1.1-cp39-abi3-win_amd64.whl (35.5 kB view details)

Uploaded CPython 3.9+Windows x86-64

nano_template-0.1.1-cp39-abi3-win32.whl (32.5 kB view details)

Uploaded CPython 3.9+Windows x86

nano_template-0.1.1-cp39-abi3-musllinux_1_2_x86_64.whl (109.8 kB view details)

Uploaded CPython 3.9+musllinux: musl 1.2+ x86-64

nano_template-0.1.1-cp39-abi3-musllinux_1_2_aarch64.whl (110.5 kB view details)

Uploaded CPython 3.9+musllinux: musl 1.2+ ARM64

nano_template-0.1.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl (113.3 kB view details)

Uploaded CPython 3.9+manylinux: glibc 2.17+ ARM64manylinux: glibc 2.28+ ARM64

nano_template-0.1.1-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl (111.5 kB view details)

Uploaded CPython 3.9+manylinux: glibc 2.28+ x86-64manylinux: glibc 2.5+ x86-64

nano_template-0.1.1-cp39-abi3-macosx_11_0_arm64.whl (34.5 kB view details)

Uploaded CPython 3.9+macOS 11.0+ ARM64

nano_template-0.1.1-cp39-abi3-macosx_10_9_x86_64.whl (34.3 kB view details)

Uploaded CPython 3.9+macOS 10.9+ x86-64

File details

Details for the file nano_template-0.1.1.tar.gz.

File metadata

  • Download URL: nano_template-0.1.1.tar.gz
  • Upload date:
  • Size: 46.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.5

File hashes

Hashes for nano_template-0.1.1.tar.gz
Algorithm Hash digest
SHA256 fc019a975f6cc68289e1469bcd8e0ffa50eca1bb49f8c8e11a5af790cb765905
MD5 b934826a4c2f28e680e41513054edac1
BLAKE2b-256 b9c953feb6a4c45e1847532aa2e008afa4081dbaad9afa2b4a99e2eb9461885c

See more details on using hashes here.

File details

Details for the file nano_template-0.1.1-cp314-cp314t-win_arm64.whl.

File metadata

File hashes

Hashes for nano_template-0.1.1-cp314-cp314t-win_arm64.whl
Algorithm Hash digest
SHA256 11fdb9948282760175e2d778dc3ec96c8c82f28fc6ec87dfad604db3ec8e04a4
MD5 e4238d1294da171d20ad7048ec470b4b
BLAKE2b-256 2d0552a9c9dc2ef4371d943cc179e22793230fae98517bf4338436ffcd5bbe14

See more details on using hashes here.

File details

Details for the file nano_template-0.1.1-cp314-cp314t-win_amd64.whl.

File metadata

File hashes

Hashes for nano_template-0.1.1-cp314-cp314t-win_amd64.whl
Algorithm Hash digest
SHA256 5fb2897d633b374851e8f3ee4d42628af67bb0c1a9dfc227ebbc819b7b52b869
MD5 da1c8c412b7255b3f6a0a5a4025006ab
BLAKE2b-256 e64e489eedf28a71a80b058d7de8471d4141023328bd7dc0462ff7b14e37da85

See more details on using hashes here.

File details

Details for the file nano_template-0.1.1-cp314-cp314t-win32.whl.

File metadata

  • Download URL: nano_template-0.1.1-cp314-cp314t-win32.whl
  • Upload date:
  • Size: 35.0 kB
  • Tags: CPython 3.14t, Windows x86
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.5

File hashes

Hashes for nano_template-0.1.1-cp314-cp314t-win32.whl
Algorithm Hash digest
SHA256 65933b1327f5db2cc2f658a658abdff288d1a4106a77b8c71c164dac5da0c816
MD5 b60f428551afb0bb9d853a9b071df48f
BLAKE2b-256 f56dc032cf59c0265299f2bbb0613dc0fba2edf70383be294109d2086c81d5b5

See more details on using hashes here.

File details

Details for the file nano_template-0.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl.

File metadata

File hashes

Hashes for nano_template-0.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 7465d899bdd49a5186d6aebd48c177ad582fddb49954581ec8c3d00722befdc9
MD5 462706553b0ea124be1d514ba72623c7
BLAKE2b-256 c4c20e9509eedd27fe3499ebbee00c14abd89e26f8cffef8dfc6042be3278f91

See more details on using hashes here.

File details

Details for the file nano_template-0.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl.

File metadata

File hashes

Hashes for nano_template-0.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl
Algorithm Hash digest
SHA256 6c18f927f98ea48c784a85995f437cf5106b66a5486ac8e92a87ab65062d739e
MD5 1a690ac4a5c4d5e3931e382bc2dd930d
BLAKE2b-256 1bd1899ae2ba29945a9537d363db40af84efeb53bf61d65ceb37b3d95bfa34e7

See more details on using hashes here.

File details

Details for the file nano_template-0.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl.

File metadata

File hashes

Hashes for nano_template-0.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 ba38cbeb2194a386033895d53d9e274d5eaf2dc2a547e1e61c349729e6ad3233
MD5 d22b0b55896d813bb28c7d888f04d9fa
BLAKE2b-256 7b03b7adf15eb3474bacc70e914bae07340245df2575e32221689ca5554da325

See more details on using hashes here.

File details

Details for the file nano_template-0.1.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl.

File metadata

File hashes

Hashes for nano_template-0.1.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl
Algorithm Hash digest
SHA256 55f855bda34ad8f23f2408f7d3c03a9c4b6366498e625f8a7d1cbf53e0691ca9
MD5 69465b2ef295188f3af38c7bd86b2679
BLAKE2b-256 d2709f9d798b2e98dfc53c5121e1130bdc81e657df3b7568ea405d48d987ad70

See more details on using hashes here.

File details

Details for the file nano_template-0.1.1-cp314-cp314t-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for nano_template-0.1.1-cp314-cp314t-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 c7b1f380c48c031ef05c8d09636f408ec3a50b036d8a4b3c8fbe3d3b39be6154
MD5 c5410fbdab9072375508d7007c4a7ae2
BLAKE2b-256 59a429ad3fac4709531716e3d2b4cc6a20e07bc01bc8b129701d08796eb70c25

See more details on using hashes here.

File details

Details for the file nano_template-0.1.1-cp314-cp314t-macosx_10_15_x86_64.whl.

File metadata

File hashes

Hashes for nano_template-0.1.1-cp314-cp314t-macosx_10_15_x86_64.whl
Algorithm Hash digest
SHA256 dc0ebee681324244b467bc4837639f03f2f27fd2e425ba2adb5ca213cea8fe7f
MD5 ef1edbb5560c1d2e7074f9a189f1aaeb
BLAKE2b-256 a34c6c5d2259cb4fa4c3244018ce9065f2ba641b0ffe28a81c85c609b10c3801

See more details on using hashes here.

File details

Details for the file nano_template-0.1.1-cp39-abi3-win_arm64.whl.

File metadata

File hashes

Hashes for nano_template-0.1.1-cp39-abi3-win_arm64.whl
Algorithm Hash digest
SHA256 b143c78fb5c35bafb346fcdd3eaaf5740ddf21a41971b93dd2deaaf68f650d97
MD5 d56674d328eed087a6e3bb45d79d5924
BLAKE2b-256 f53e3e6cd71071c842627e5318d8050615b0e79e27d4b38861252656b7aa8088

See more details on using hashes here.

File details

Details for the file nano_template-0.1.1-cp39-abi3-win_amd64.whl.

File metadata

File hashes

Hashes for nano_template-0.1.1-cp39-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 8bc905e11d9c8cb9343b4ce5db20562d0b0b631346b3a62aaf910788cd6014fd
MD5 61b62773b5ac40293af91d929b3f8587
BLAKE2b-256 95392018e60083653df78c6ff4f23e803219edfde290418fe9dc9995c75bb82b

See more details on using hashes here.

File details

Details for the file nano_template-0.1.1-cp39-abi3-win32.whl.

File metadata

  • Download URL: nano_template-0.1.1-cp39-abi3-win32.whl
  • Upload date:
  • Size: 32.5 kB
  • Tags: CPython 3.9+, Windows x86
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.5

File hashes

Hashes for nano_template-0.1.1-cp39-abi3-win32.whl
Algorithm Hash digest
SHA256 9ad016097776114a2c0170f2cb30f0cf68caf2ffc1b05a06fd99cfba7dd3c864
MD5 b96df4ac8f41c938624853229f65569b
BLAKE2b-256 97d52e0ba4eea97d6eef55c0a7ebf2ab68159fb6c370054cd7014aac9fe8ddca

See more details on using hashes here.

File details

Details for the file nano_template-0.1.1-cp39-abi3-musllinux_1_2_x86_64.whl.

File metadata

File hashes

Hashes for nano_template-0.1.1-cp39-abi3-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 ced2a53b3031c3d015580de6d66814304b491b951ff55584807a3ce35d3d8d5d
MD5 86b9f9c154921fae12d5d8057c2ccfd2
BLAKE2b-256 961007365eeee8571b5198c5a839a7dd76f165e0bc9c45e7fad9a43936eb5f0c

See more details on using hashes here.

File details

Details for the file nano_template-0.1.1-cp39-abi3-musllinux_1_2_aarch64.whl.

File metadata

File hashes

Hashes for nano_template-0.1.1-cp39-abi3-musllinux_1_2_aarch64.whl
Algorithm Hash digest
SHA256 399f92a787f752b1fdfce39036cd24db22dab55daf7af2fe45ced8193553da26
MD5 503e3e4cbc0d46659ce76310fe5aa301
BLAKE2b-256 36685a8ed20fa6cc469f4d89110a808f07744a66538b0d3d1a94d164b181166b

See more details on using hashes here.

File details

Details for the file nano_template-0.1.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl.

File metadata

File hashes

Hashes for nano_template-0.1.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 d41be82514d36c1b281de2922d1c650faaf175818b6288cb227ddbea9026c4b4
MD5 5e5b5ee6f5541ff943244ebd4d40c521
BLAKE2b-256 f66bfe4efc39ba720ee12b6d049904cf97ffe1c4ff3d53a1ec006cc88a1bb530

See more details on using hashes here.

File details

Details for the file nano_template-0.1.1-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl.

File metadata

File hashes

Hashes for nano_template-0.1.1-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl
Algorithm Hash digest
SHA256 e0fb2b71d1d554dfe224bc1ae5ff2520caeefec0f7ea798768007d4e4001e03d
MD5 87a24e247c53e1fce205693727350f8f
BLAKE2b-256 0e79f224fde0a5f8b29b9ee2fd7f77e75f6d88477362a9b4769e8f4b7fc34c0a

See more details on using hashes here.

File details

Details for the file nano_template-0.1.1-cp39-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for nano_template-0.1.1-cp39-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 c2bd8e2b1ee938170a5f80e0c5f1ed3c9950d446fdc8f7241d89f54c713e0a1c
MD5 e899c2791e93c75b551403225b157956
BLAKE2b-256 31f76827f83da6611dad436f6173b46e414efbf2af1251e48907479662d32dec

See more details on using hashes here.

File details

Details for the file nano_template-0.1.1-cp39-abi3-macosx_10_9_x86_64.whl.

File metadata

File hashes

Hashes for nano_template-0.1.1-cp39-abi3-macosx_10_9_x86_64.whl
Algorithm Hash digest
SHA256 5cfec1fba73b13f57c664ef5d5b5d4fefe6e3698937c772691dacd8c34a80d6a
MD5 8458618ac4d2e267f632afcc5c94823a
BLAKE2b-256 f26e4aa47e84db0f4cf78d199e2f65db771b248630e8040659558c5e36fc99e8

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