diff --git a/cloud_docs/concepts/logs.md b/cloud_docs/concepts/logs.md index 7595185e..b3f40cce 100644 --- a/cloud_docs/concepts/logs.md +++ b/cloud_docs/concepts/logs.md @@ -132,6 +132,8 @@ Two environment variables control session logging. With a database enabled (the To change either default, set the value with `scloud variable set`. See [Passwords, secrets, and environment variables](/cloud/concepts/passwords-secrets-env-vars) for variable management. +For what the server records, which tables it writes to, and how retention works, see [Logging](https://docs.serverpod.dev/concepts/operations/logging) in the framework documentation. + ## Troubleshooting **Invalid timestamp format.** Use ISO 8601 form (`YYYY-MM-DDTHH:MM:SSZ`) or a supported duration string (`5m`, `2h`, `1d`). diff --git a/cloud_docs/guides/redis.md b/cloud_docs/guides/redis.md index c655baa9..ae9105a0 100644 --- a/cloud_docs/guides/redis.md +++ b/cloud_docs/guides/redis.md @@ -73,7 +73,7 @@ After a successful deploy, your server will use Upstash for Redis-backed caching ## Related -- [Serverpod configuration](https://docs.serverpod.dev/concepts/configuration) for Redis options and environment variables. -- [Serverpod caching](https://docs.serverpod.dev/concepts/caching) for local and Redis-backed caches. +- [Serverpod configuration](https://docs.serverpod.dev/concepts/server-fundamentals/configuration) for Redis options and environment variables. +- [Serverpod caching](https://docs.serverpod.dev/concepts/endpoints-and-apis/caching) for local and Redis-backed caches. - [Upstash: Connect your client](https://upstash.com/docs/redis/howto/connectclient) for connection details and TLS. - [Passwords, secrets, and environment variables](/cloud/concepts/passwords-secrets-env-vars) for how Serverpod Cloud injects passwords and variables. diff --git a/docs/06-concepts/01-server-fundamentals/02-running-your-server.md b/docs/06-concepts/01-server-fundamentals/02-running-your-server.md index 5e9d88c5..08117326 100644 --- a/docs/06-concepts/01-server-fundamentals/02-running-your-server.md +++ b/docs/06-concepts/01-server-fundamentals/02-running-your-server.md @@ -75,9 +75,33 @@ The server accepts arguments that control how it starts: `--mode` selects the ru In production, the `--role` argument controls which parts of the server run: - **`monolith`** (default) runs everything: the API, Insights, and web servers, plus [future calls](../scheduling/overview) and [health checks](../operations/health-checks). -- **`serverless`** serves requests only. Future calls and health checks are disabled, which fits platforms that start and stop instances on demand. +- **`serverless`** serves requests only. Future calls and [health metric collection](../operations/health-checks#health-metrics) are disabled, which fits platforms that start and stop instances on demand. The health probes still answer. - **`maintenance`** starts no servers. It performs one-shot work, applying migrations when passed `--apply-migrations` and running any due future calls, then exits. The exit code reports success or failure, which makes it fit for CI jobs and scheduled maintenance tasks. +## Run code on shutdown + +Register a shutdown task to do cleanup work when the server stops, such as flushing state or releasing an external resource. Tasks run after the server stops accepting requests but before the database and Redis connections close, so they can still use them. + +:::warning +Shutdown tasks are an experimental API and can change in a breaking way in any minor release. +::: + +Register them on the `Serverpod` instance in `lib/server.dart`, where you create it: + +```dart +pod.experimental.shutdownTasks.addTask(#taskIdentifier, () async { + // Your shutdown logic here. +}); +``` + +Each task is registered under an identifier you choose, used in log messages and to remove the task again. Any object works; `#taskIdentifier` above is a Dart symbol, which is a convenient way to write a constant name. + +```dart +pod.experimental.shutdownTasks.removeTask(#taskIdentifier); +``` + +Registering two tasks under the same identifier throws a `StateError`. All tasks run concurrently, and the server waits for them all before shutting down. A task that throws does not stop the shutdown, but the error is logged and the process exits with a non-zero code, which a host reading exit status will treat as a failed shutdown. + ## Related - [`serverpod start` reference](../cli/commands/start): every command-line option. diff --git a/docs/06-concepts/01-server-fundamentals/03-configuration.md b/docs/06-concepts/01-server-fundamentals/03-configuration.md index c9f78df2..1f853051 100644 --- a/docs/06-concepts/01-server-fundamentals/03-configuration.md +++ b/docs/06-concepts/01-server-fundamentals/03-configuration.md @@ -338,7 +338,7 @@ experimental_features: all: true ``` -See the [experimental features documentation](../operations/experimental-features) for details. +See [Exception monitoring](../operations/exception-monitoring) for the experimental features Serverpod currently exposes. :::warning Experimental features may change or be removed in future versions. diff --git a/docs/06-concepts/02-endpoints-and-apis/02-sessions.md b/docs/06-concepts/02-endpoints-and-apis/02-sessions.md index bdcec55a..23332f72 100644 --- a/docs/06-concepts/02-endpoints-and-apis/02-sessions.md +++ b/docs/06-concepts/02-endpoints-and-apis/02-sessions.md @@ -42,7 +42,7 @@ class ExampleEndpoint extends Endpoint { | Member | What it is | | --------------------------- | ----------------------------------------------------------------------------------------------- | | `db` | Database access. See the [CRUD docs](../data-and-the-database/database/crud). | -| `caches` | Local and distributed caching. See [caching](../operations/caching). | +| `caches` | Local and distributed caching. See [caching](./caching). | | `storage` | File storage. See [file uploads](./file-uploads). | | `messages` | Server events for real-time communication. See [server events](./server-events). | | `passwords` | Secrets from config and environment. See [configuration](../server-fundamentals/configuration). | diff --git a/docs/06-concepts/02-endpoints-and-apis/06-caching.md b/docs/06-concepts/02-endpoints-and-apis/06-caching.md new file mode 100644 index 00000000..6b7684c5 --- /dev/null +++ b/docs/06-concepts/02-endpoints-and-apis/06-caching.md @@ -0,0 +1,153 @@ +--- +description: A cache stores a result under a key so the next request can read it back, held in server memory or in Redis and reached through session.caches. +--- + +# Caching + +Some work is expensive to repeat: a query that joins several tables, a result assembled from many rows, or a value fetched from another service. Caching stores the result under a key so the next request can read it back instead of doing the work again. + +Serverpod's caches are reached from the [`Session`](./sessions) object, alongside the database and messaging. Anything you cache must be serializable: the primitives (`int`, `String`, `bool`, `double`, `DateTime`, `Duration`, `ByteData`, `UuidValue`, `Uri`, `BigInt`), collections of them, and your own [models](../data-and-the-database/models). Use a model rather than a hand-written class: one with a `toJson` stores fine and throws on read, one without fails immediately with `JsonUnsupportedObjectError`. + +## The caches + +| Cache | Where it lives | Use it for | +| --- | --- | --- | +| `session.caches.local` | Memory of the server handling this call | The default choice for cached values. | +| `session.caches.localPrio` | Memory of the same server, kept separate | Values you want to keep hot, held apart from ordinary entries. | +| `session.caches.global` | Redis, shared by every server instance | Values that must be the same on every instance. | + +There is a fourth cache, `session.caches.query`, which Serverpod manages for cacheable database queries. It is not a place to put your own objects. + +Each local cache holds at most 10,000 entries. When a cache is full, the oldest entry is dropped to make room, based on when it was inserted rather than when it was last read, so a frequently read key can still be evicted. + +## Cache an object + +Read a value with `get`, and write one with `put`: + +```dart +Future getUserData(Session session, int userId) async { + var cacheKey = 'UserData-$userId'; + + // Try the cache first. + var userData = await session.caches.local.get(cacheKey); + if (userData != null) return userData; + + // Not cached, so load it and store it for five minutes. + userData = await UserData.db.findById(session, userId); + if (userData != null) { + await session.caches.local.put( + cacheKey, + userData, + lifetime: const Duration(minutes: 5), + ); + } + + return userData; +} +``` + +The `lifetime` sets how long the entry stays valid. After it elapses, the next read misses and returns `null`. Omit `lifetime` and the entry never expires on its own, so it stays until you invalidate it or it is evicted to make room. + +Cache `DateTime` values as UTC, since that is how they come back: + +```dart +await session.caches.local.put('lastUpdate', DateTime.now().toUtc()); + +var cached = await session.caches.local.get('lastUpdate'); +``` + +Collections work the same way. Give `get` the full type, including what the collection holds: + +```dart +await session.caches.local.put('users', [alice, bob]); + +var users = await session.caches.local.get>('users'); +``` + +Serverpod generates the code to read a collection type back only when your project uses it, such as in an endpoint signature or a model field. Caching a shape that appears nowhere else in your project throws on read. + +### Load on a miss + +The `CacheMissHandler` class folds "read it, and compute it if it is missing" into one call. Serverpod stores whatever the handler returns: + +```dart +Future getUserData(Session session, int userId) async { + return session.caches.local.get( + 'UserData-$userId', + CacheMissHandler( + () async => UserData.db.findById(session, userId), + lifetime: const Duration(minutes: 5), + ), + ); +} +``` + +The call returns `null` when the handler does, and nothing is stored in that case. If several requests miss the same key at once, the local caches run the handler once and let the others wait for that result. The Redis-backed global cache runs it per request. + +## Invalidate entries + +A cached value goes stale as soon as the underlying data changes, so remove it when you write: + +```dart +await UserData.db.updateRow(session, userData); +await session.caches.local.invalidateKey('UserData-${userData.id}'); +``` + +To drop several related entries at once, put them in a group and invalidate the group: + +```dart +await session.caches.local.put( + 'UserData-$userId', + userData, + group: 'user-$userId', +); + +// Later, remove every entry in the group. +await session.caches.local.invalidateGroup('user-$userId'); +``` + +Use `containsKey` to check for an entry without reading it, and `clear` to empty the cache. + +:::warning +Groups are a local-cache feature. On `session.caches.global`, passing `group:` to `put` or calling `invalidateGroup` throws `UnimplementedError`, so code that groups entries breaks when you move it to the global cache. +::: + +## The global cache and Redis + +The global cache uses Redis, which is what makes it shared between server instances. When Redis is not configured, it falls back to an in-memory cache that belongs to a single server process. + +:::warning +The fallback applies in every run mode, production included. It exists so you can run code that uses the global cache without a Redis server during development, not as a substitute for Redis in production. In that state each instance keeps its own copy, nothing is shared between instances, and everything is lost on restart. + +The server does not warn you about this at normal logging levels, and the [readiness probe](../operations/health-checks) still passes, so confirm your Redis configuration before running more than one instance. If Redis is configured but unreachable, the readiness probe fails, so that case does get caught. +::: + +Redis counts as configured when your config has a `redis` section that is enabled. A `redis` section that omits `enabled` is treated as enabled. If the section is present but its password is missing, the server fails to start rather than falling back. + +Serverpod does not manage the size of the global cache or decide what it evicts. Redis does, according to how you have configured it, so set a memory limit and an eviction policy there if you cache a large number of objects globally. + +Serverpod Cloud does not provide a managed Redis instance, so a deployed app uses the in-memory fallback unless you connect your own. See the [Redis guide](/cloud/guides/redis) for how to set one up. + +## Send Redis commands directly + +Redis supports operations the cache API does not expose, such as atomic counters and sorted sets. You can borrow the connection Serverpod manages: + +```dart +Future incrementCounter(Session session, String counterName) async { + var command = await session.serverpod.redisController?.getConnection(); + if (command == null) return null; + + var result = await command.send_object(['INCR', 'my_app:$counterName']); + return result is int ? result : null; +} +``` + +The connection is `null` when Redis is disabled or unreachable, including when the global cache is on its in-memory fallback. Responses come back in the shape Redis defines for each command, so check the result before casting it. + +The returned `RedisCommand` is Serverpod's own connection: do not close it, and do not replace `Serverpod.redisController`. Namespace your keys so they cannot collide with Serverpod's own cache entries. + +## Related + +- [Sessions](./sessions): the object the caches hang off. +- [Server events](./server-events): the other Redis-backed feature, for messaging between instances. +- [Configuration](../server-fundamentals/configuration): where the `redis` section lives. diff --git a/docs/06-concepts/02-endpoints-and-apis/06-file-uploads.md b/docs/06-concepts/02-endpoints-and-apis/07-file-uploads.md similarity index 100% rename from docs/06-concepts/02-endpoints-and-apis/06-file-uploads.md rename to docs/06-concepts/02-endpoints-and-apis/07-file-uploads.md diff --git a/docs/06-concepts/02-endpoints-and-apis/07-endpoint-inheritance.md b/docs/06-concepts/02-endpoints-and-apis/08-endpoint-inheritance.md similarity index 100% rename from docs/06-concepts/02-endpoints-and-apis/07-endpoint-inheritance.md rename to docs/06-concepts/02-endpoints-and-apis/08-endpoint-inheritance.md diff --git a/docs/06-concepts/02-endpoints-and-apis/08-backward-compatibility.md b/docs/06-concepts/02-endpoints-and-apis/09-backward-compatibility.md similarity index 100% rename from docs/06-concepts/02-endpoints-and-apis/08-backward-compatibility.md rename to docs/06-concepts/02-endpoints-and-apis/09-backward-compatibility.md diff --git a/docs/06-concepts/02-endpoints-and-apis/09-endpoint-middleware.md b/docs/06-concepts/02-endpoints-and-apis/10-endpoint-middleware.md similarity index 100% rename from docs/06-concepts/02-endpoints-and-apis/09-endpoint-middleware.md rename to docs/06-concepts/02-endpoints-and-apis/10-endpoint-middleware.md diff --git a/docs/06-concepts/02-endpoints-and-apis/10-configure-http-calls.md b/docs/06-concepts/02-endpoints-and-apis/11-configure-http-calls.md similarity index 100% rename from docs/06-concepts/02-endpoints-and-apis/10-configure-http-calls.md rename to docs/06-concepts/02-endpoints-and-apis/11-configure-http-calls.md diff --git a/docs/06-concepts/07-operations/01-caching.md b/docs/06-concepts/07-operations/01-caching.md deleted file mode 100644 index c8065633..00000000 --- a/docs/06-concepts/07-operations/01-caching.md +++ /dev/null @@ -1,123 +0,0 @@ ---- -description: Caching in Serverpod stores frequently requested objects in local server memory or a distributed Redis cache to reduce expensive database queries. ---- - -# Caching - -Accessing the database can be expensive for complex queries or if you need to run many different queries for a specific task. Serverpod makes it easy to cache frequently requested objects in the memory of your server. Any object can be cached, including primitive types (`int`, `String`, `DateTime`, `Duration`, `ByteData`, `UuidValue`), lists, maps, and serializable models. Objects can be stored in the Redis cache if your Serverpod is hosted across multiple servers in a cluster. - -:::info -Objects must be serializable to be cached. Non-serializable objects will throw an error when attempting to cache them. Most Dart types are serializable, including primitives, collections, and custom objects with `toJson`/`fromJson` methods. All objects that can be used with endpoints or the database are supported. -::: - -## Caching objects - -Caches can be accessed through the `Session` object. This is an example of an endpoint method for requesting data about a user: - -```dart -Future getUserData(Session session, int userId) async { - // Define a unique key for the UserData object - var cacheKey = 'UserData-$userId'; - - // Try to retrieve the object from the cache - var userData = await session.caches.local.get(cacheKey); - - // If the object wasn't found in the cache, load it from the database and - // save it in the cache. Make it valid for 5 minutes. - if (userData == null) { - userData = await UserData.db.findById(session, userId); - await session.caches.local.put(cacheKey, userData!, lifetime: Duration(minutes: 5)); - } - - // Return the user data to the client - return userData; -} -``` - -There are three caches where you can store your objects, all reached through the `Session` object. Two are local to the server handling the current session: a regular cache (`session.caches.local`) and a priority cache (`session.caches.localPrio`) for frequently accessed objects. The third, `session.caches.global`, is distributed across the server cluster through Redis. - -Depending on the type and number of objects that are cached in the global cache, you may want to specify custom caching rules in Redis. This is currently not handled automatically by Serverpod. - -:::info -During development, you can run code that uses the global cache without a running Redis server. If Redis is unreachable (or not configured) in a non-production run mode, the global cache falls back to an isolated in-memory cache. -::: - -### Caching primitive objects - -To cache primitive objects, call the `put` method with the object. For the `get`, specify the object type as the generic parameter, as for `SerializableModel` objects: - -```dart -await session.caches.local.put('userCount', 17, lifetime: Duration(minutes: 5)); - -var count = await session.caches.local.get('userCount'); -``` - -For `DateTime` objects, it is recommended to always cache them as UTC. Otherwise, you may get unexpected results when retrieving the object from the cache. - -```dart -var lastUpdate = DateTime.now().toUtc(); - -await session.caches.local.put('lastUpdate', lastUpdate); - -// Retrieved `DateTime` object will always be in UTC. -var cached = await session.caches.local.get('lastUpdate'); -``` - -### Caching lists and collections - -Lists and collections can also be cached directly: - -```dart -var users = [UserData(name: 'Alice'), UserData(name: 'Bob')]; - -await session.caches.local.put('users', users); - -var cachedUsers = await session.caches.local.get>('users'); -``` - -### Cache miss handler - -If you want to handle cache misses in a specific way, you can pass in a `CacheMissHandler` to the `get` method. The `CacheMissHandler` makes it possible to store an object in the cache when a cache miss occurs. - -The above example rewritten using the `CacheMissHandler`: - -```dart -Future getUserData(Session session, int userId) async { - // Define a unique key for the UserData object - var cacheKey = 'UserData-$userId'; - - // Try to retrieve the object from the cache - var userData = await session.caches.local.get( - cacheKey, - // If the object wasn't found in the cache, load it from the database and - // save it in the cache. Make it valid for 5 minutes. - CacheMissHandler( - () async => UserData.db.findById(session, userId), - lifetime: Duration(minutes: 5), - ), - ); - - // Return the user data to the client - return userData; -} -``` - -If the `CacheMissHandler` returns `null`, no object will be stored in the cache. - -## Sending custom Redis commands - -Redis supports operations that the global cache does not expose, such as atomic counters and sorted sets. If you want to use these operations, you can borrow the connection Serverpod already manages and send the command using `getConnection`: - -```dart -Future incrementCounter(Session session, String counterName) async { - var command = await session.serverpod.redisController?.getConnection(); - if (command == null) return null; - - var result = await command.send_object(['INCR', 'my_app:$counterName']); - return result is int ? result : null; -} -``` - -The connection is `null` when Redis is disabled or unreachable, including when the global cache is running on its in-memory development fallback. Responses come back in the shape Redis defines for the command, so check the result before casting it. - -The returned `RedisCommand` is Serverpod's own connection. Do not close it or replace `Serverpod.redisController`. It is also a good practice to namespace your keys so they cannot collide with Serverpod's cache entries. diff --git a/docs/06-concepts/07-operations/01-overview.md b/docs/06-concepts/07-operations/01-overview.md new file mode 100644 index 00000000..1ebe8ee8 --- /dev/null +++ b/docs/06-concepts/07-operations/01-overview.md @@ -0,0 +1,24 @@ +--- +description: Operations covers running a Serverpod server in production, seeing what it is doing through logs, proving it is healthy, securing its traffic, and catching exceptions. +--- + +# Overview + +Once your server is written, the work shifts from building features to running them. This section covers what you need after deployment: seeing what the server is doing, proving to a host that it is healthy, securing the traffic it accepts, and finding out when something breaks. + +Everything here works the same whether you deploy to [Serverpod Cloud](../../deployments/deploy-to-serverpod-cloud) or [host it yourself](../../deployments/custom-hosting/choosing-a-strategy), though the two differ in how much is set up for you. + +## What each page covers + +- **[Logging](logging)**: what the server records for every call, where those records go, and how to keep the log tables from growing without bound. +- **[Health checks](health-checks)**: the HTTP endpoints a host calls to decide whether your server is alive and ready for traffic, plus the metrics Serverpod collects about itself. +- **[Security and TLS](security-and-tls)**: how traffic to your server is encrypted, and when you need to configure that yourself. +- **[Exception monitoring](exception-monitoring)**: reporting exceptions to a monitoring service as they happen. This one is an experimental API. + +## Related + +- [Configuration](../server-fundamentals/configuration): the config files and environment variables every setting on these pages is read from. +- [Sessions](../endpoints-and-apis/sessions): the object that produces most of what ends up in your logs. +- [Caching](../endpoints-and-apis/caching): storing values in server memory or Redis. +- [Run code on shutdown](../server-fundamentals/running-your-server#run-code-on-shutdown): cleanup work when the server stops. +- [Insights](../../tools/insights): the companion app for reading logs and metrics. diff --git a/docs/06-concepts/07-operations/02-logging.md b/docs/06-concepts/07-operations/02-logging.md index 8273b5a0..15c577a1 100644 --- a/docs/06-concepts/07-operations/02-logging.md +++ b/docs/06-concepts/07-operations/02-logging.md @@ -1,16 +1,27 @@ --- -description: Serverpod logging records custom messages, exceptions, and queries via the session log method and stores them in the database, with configurable retention and console output. +description: Serverpod records a log entry for calls, queries, and your own messages, stores them in the database or prints them to the console, and can purge old entries. --- # Logging -Serverpod uses the database for storing logs; this makes it easy to search for errors, slow queries, or debug messages. To log custom messages during the execution of a session, use the `log` method of the `session` object. When the session is closed, either from successful execution or by failing from throwing an exception, the messages are written to the log. By default, session log entries are written for every completed session. +Logging is how you find out what your server did after it did it: which calls ran, which queries were slow, and what failed. Serverpod records this for you, and you add your own messages on top. + +There are four kinds of record, and the difference matters for everything below: + +- **Session records** describe one unit of work, such as an endpoint call: how long it took, whether it failed, and which endpoint it hit. +- **Query records** describe individual database queries run during that session. +- **Log messages** are the entries you write yourself with `session.log`. +- **Stream messages** are the messages passed by [streaming](../endpoints-and-apis/streaming) sessions. + +## Write your own messages + +Call `log` on the [session](../endpoints-and-apis/sessions) you were given: ```dart session.log('This is working well'); ``` -You can also pass exceptions and stack traces to the `log` method or set the logging level. +Pass a level, an exception, and a stack trace when something goes wrong: ```dart session.log( @@ -21,86 +32,93 @@ session.log( ); ``` -Log entries are stored in the following tables of the database: `serverpod_log` for text messages, `serverpod_query_log` for queries, and `serverpod_session_log` for completed sessions. +Messages are collected while the session runs and written when it closes, whether it finished normally or threw. -## Controlling session logs with environment variables or configuration files +## Where logs go -You can control whether session logs are written to the database, the console, both, or neither, using environment variables or configuration files. **Environment variables take priority** over configuration file settings if both are provided. +Records are written to the database, to the console, to both, or to neither. -For the default values when environment variables are not set, see the [default behavior for session logs](#default-behavior-for-session-logs). +In the database they land in four tables: -### Environment variables +| Table | Holds | +| --- | --- | +| `serverpod_session_log` | One row per completed session. | +| `serverpod_log` | Your `session.log` messages. | +| `serverpod_query_log` | Database queries. | +| `serverpod_message_log` | Stream messages from streaming sessions. | -- `SERVERPOD_SESSION_PERSISTENT_LOG_ENABLED`: Controls whether session logs are written to the database. -- `SERVERPOD_SESSION_LOG_CLEANUP_INTERVAL`: How often to run the log cleanup job (duration string, e.g. `6h`, `24h`). Set to empty to disable automated purging. -- `SERVERPOD_SESSION_LOG_RETENTION_PERIOD`: How long to keep session log entries (duration string, e.g. `30d`, `6h`). Set to empty or omit to use the default (90 days). -- `SERVERPOD_SESSION_LOG_RETENTION_COUNT`: Maximum number of session log entries to keep. Set to empty or omit to use the default (100,000). -- `SERVERPOD_SESSION_CONSOLE_LOG_ENABLED`: Controls whether session logs are output to the console. -- `SERVERPOD_SESSION_CONSOLE_LOG_FORMAT`: The format for console logging (`text` or `json`). See the [Configuration reference](../lookups/configuration-reference). +The last three reference the session row, so deleting a session row removes its queries and messages with it. -### Configuration file example +:::info +The companion app [Serverpod Insights](../../tools/insights) reads and searches these tables, and can change the runtime settings described below. +::: -You can also configure logging behavior directly in the configuration file: +## Which sessions get recorded -```yaml -sessionLogs: - persistentEnabled: true # Logs are stored in the database - cleanupInterval: 6h # Run cleanup every 6 hours - retentionPeriod: 30d # Keep entries for 30 days - retentionCount: 5000 # Keep at most 5,000 entries - consoleEnabled: true # Logs are output to the console -``` +Not every session produces a row, and the default depends on the run mode. -Duration strings for the cleanup interval and retention period use the same format as in [models](../data-and-the-database/models#supported-default-values): e.g. `30d`, `6h`, `1d 2h 30min`. +In `development`, every completed session is recorded. In `staging`, `production`, and `test`, a session is recorded only when it ran longer than one second, it failed, or it produced a log, query, or message entry. Ordinary fast calls leave no row, which keeps the table to the sessions worth looking at. -## Default behavior for session logs +These thresholds are runtime settings stored in the `serverpod_runtime_settings` table, so you can change them on a running server through Insights without redeploying. They control whether all sessions are logged, whether all queries are logged, what counts as slow, and the minimum level a message must have to be kept. You can also override them per endpoint and per method. -By default, session logging behavior depends on whether the project has database support: +## Configure logging -- **When a database is present** +Session logging is configured under `sessionLogs:` in your config file for the run mode, or through environment variables. Environment variables win over the config file, key by key. - - `persistentEnabled` is set to `true`, meaning logs are stored in the database. - - `consoleEnabled` defaults to `true` in the `development` run mode and `false` in the other run modes, so local runs print to the console while deployed runs rely on the database logs unless console logging is explicitly enabled. +| Setting | Environment variable | Default | +| --- | --- | --- | +| `persistentEnabled` | `SERVERPOD_SESSION_PERSISTENT_LOG_ENABLED` | `true` when a database is configured | +| `consoleEnabled` | `SERVERPOD_SESSION_CONSOLE_LOG_ENABLED` | `true` in `development` or when there is no database, otherwise `false` | +| `consoleLogFormat` | `SERVERPOD_SESSION_CONSOLE_LOG_FORMAT` | `text` in `development`, otherwise `json` | -- **When no database is present** +Three more settings control [purging](#purge-old-records), which behaves differently enough to be worth reading before you rely on it. - - `persistentEnabled` is set to `false` since persistent logging requires a database. - - `consoleEnabled` is set to `true`, meaning logs are printed to the console by default. +```yaml +sessionLogs: + persistentEnabled: true # Store records in the database + consoleEnabled: true # Also print them + cleanupInterval: 6h # Purge every 6 hours + retentionPeriod: 30d # Keep 30 days + retentionCount: 5000 # Keep at most 5,000 sessions +``` + +Durations use the same format as [model default values](../data-and-the-database/models#supported-default-values), such as `30d`, `6h`, or `1d 2h 30min`. :::warning -If `persistentEnabled` is set to `true` but **no database is configured**, a `StateError` will be thrown. Persistent logging requires database support, and Serverpod ensures that misconfigurations are caught early by raising this error. +Setting `persistentEnabled` to `true` without a configured database throws a `StateError` on startup. Persistent logging needs somewhere to persist to. ::: -:::info -You can use the companion app **[Serverpod Insights](../../tools/insights)** to read, search, and configure the logs. +:::warning +Persistent logging is unavailable on SQLite, which cannot handle the concurrent writes it needs. The server warns and skips it, and nothing takes its place, so enable `consoleEnabled` if you want records in the run modes where it defaults to off. ::: -### Log retention and automated purging +:::info +Every environment variable in the table takes a real value. Setting one to an empty string is not a way to unset it: the server fails to start. To turn a policy off, set the key to `null` in the config file instead. +::: -Since log entries are stored in the database when persistent logging is enabled, the logs table can grow without bound if not purged. Serverpod can automatically purge logs based on configurable retention policies to prevent unchecked storage growth. +## Purge old records -#### Default values +Log tables grow with every call your server handles, so Serverpod can delete old records for you. Cleanup runs on the `cleanupInterval`, and removes session rows that are either older than `retentionPeriod` or beyond the newest `retentionCount`, whichever applies first. Deleting a session row takes its query, message, and log rows with it. -- **Cleanup interval**: 24 hours; the cleanup job runs once per day. -- **Retention period**: 90 days; removes entries older than this. -- **Retention count**: 100,000 entries; removes entries that exceed this count. +| Setting | Environment variable | Default | +| --- | --- | --- | +| `cleanupInterval` | `SERVERPOD_SESSION_LOG_CLEANUP_INTERVAL` | Unset, so no purging | +| `retentionPeriod` | `SERVERPOD_SESSION_LOG_RETENTION_PERIOD` | Unset, so no age limit | +| `retentionCount` | `SERVERPOD_SESSION_LOG_RETENTION_COUNT` | Unset, so no count limit | -If both time-based (retention period) and count-based (retention count) limits are set, entries are removed if they are either too old or beyond the maximum count, whichever is reached first. +:::warning +These three fall back to `24h`, `90d`, and `100000` only when `sessionLogs` is absent from your config entirely. Set any one session-log key, in the file or through an environment variable, and the ones you did not set resolve to unset rather than to those values, switching that policy off with no warning. -:::note -Automatic cleanup is only available when persistent logging is enabled and the cleanup interval is configured. +Generated projects ship a `sessionLogs` block in the `development`, `test`, and `production` configs, so purging is off in those run modes until you set all three explicitly. Set `cleanupInterval`, `retentionPeriod`, and `retentionCount` together whenever you configure any part of `sessionLogs`. ::: -#### Customizing retention policies +The `cleanupInterval` setting is the switch for the whole job. With no interval set, nothing is purged whatever the two retention values say. Purging also requires `persistentEnabled`, since there is nothing to delete otherwise. -All three settings are optional and can be set to `null` to disable the respective policy. If the `cleanupInterval` is set to `null`, no purging will run regardless of the other settings, and log tables can grow without bound until you run cleanup manually or re-enable the interval. +Cleanup is triggered by log writes rather than by a timer, so a server that is not logging anything does not purge. A single pass gives up after an hour, and the next interval starts a fresh one. -Configure retention and cleanup via [environment variables](#environment-variables) or the [configuration file](#configuration-file-example). For example, to keep 30 days and at most 5,000 entries, with cleanup every 6 hours: +## Related -```yaml -sessionLogs: - persistentEnabled: true - cleanupInterval: 6h - retentionPeriod: 30d - retentionCount: 5000 -``` +- [Configuration](../server-fundamentals/configuration): the config files these settings live in. +- [Configuration reference](../lookups/configuration-reference): every session-log key with its environment variable. +- [Sessions](../endpoints-and-apis/sessions): what a session is, and the `log` method. +- [Insights](../../tools/insights): reading logs and editing runtime settings. diff --git a/docs/06-concepts/07-operations/03-health-checks.md b/docs/06-concepts/07-operations/03-health-checks.md index 918e43d9..68e2f491 100644 --- a/docs/06-concepts/07-operations/03-health-checks.md +++ b/docs/06-concepts/07-operations/03-health-checks.md @@ -1,96 +1,62 @@ --- -description: Health checks in Serverpod expose Kubernetes-style HTTP endpoints (/livez, /readyz, /startupz) for liveness, readiness, and startup probing, with support for custom health indicators and metrics. +description: Serverpod answers liveness, readiness, and startup probes at /livez, /readyz, and /startupz, supports custom health indicators, and collects health metrics about itself. --- # Health checks -Serverpod provides a complete health check system that allows you to monitor the health of your server and your dependencies through Kubernetes-style HTTP endpoints (`/livez`, `/readyz`, `/startupz`) - each with a specific purpose that helps orchestrators (like Kubernetes) make informed decisions about container lifecycle and traffic routing. +When your server runs behind a host that can restart it or route traffic away from it, that host needs a way to ask how the server is doing. Serverpod answers three such questions over HTTP, at URLs that match what container platforms like Kubernetes expect. -## Endpoints +This page covers two separate things that share the word "health": -### Liveness Probe `/livez` +- **Health probes**, the HTTP endpoints something else calls to decide whether to send you traffic or restart you. +- **Health metrics**, numbers Serverpod records about itself into your database for you to look at later. -The liveness probe answers: "Should this container be killed and restarted?". +## Health probes -- Returns `200 OK` if the server process can respond. -- Only fails if the process is fundamentally broken. -- A failed liveness check triggers a pod restart. -- Does not check dependencies (database, Redis, etc.). +The three endpoints are always available, with no setup. Every server your Serverpod instance runs answers them, so in a default development configuration they respond on the API server's port `8080`, the Insights server's `8081`, and the web server's `8082` when your project has one. Point your host at whichever port it can reach. -This endpoint is intentionally permissive. It should only fail when the process is truly unrecoverable (deadlocks, memory corruption, infinite loops). Transient issues like slow database queries or temporary network blips should not trigger restarts. +These three paths are reserved. A [web server route](../web-server/routing) registered on them would never be reached. -**Example:** - -```bash -curl http://localhost:8080/livez -# Returns: 200 OK -``` - -### Readiness Probe `/readyz` - -The readiness probe answers: "Should traffic be routed to this container?". - -- Returns `200 OK` if all dependencies are healthy. -- Returns `503 Service Unavailable` if any critical dependency is unavailable. -- Checks database connectivity, Redis connectivity (if configured), and custom health indicators. - -A failed readiness check stops traffic routing without restarting the pod. This allows the pod to recover from temporary issues without receiving extra pressure from new traffic. - -**Example:** +| Endpoint | Question it answers | Returns | +| --- | --- | --- | +| `/livez` | Is the process still working, or should it be restarted? | `200` if the server can respond at all. | +| `/readyz` | Should traffic be sent here right now? | `200` when dependencies are healthy, `503` when they are not. | +| `/startupz` | Has the server finished starting? | `200` once startup is complete. | ```bash curl http://localhost:8080/readyz -# Returns: 200 OK or 503 Service Unavailable ``` -### Startup Probe `/startupz` +**Liveness** is deliberately permissive. It only reports failure when the process is broken beyond recovery, because failing it means a restart. A slow database or a brief network problem should not restart your server, so `/livez` does not check dependencies at all. -The startup probe answers: "Has this container finished initializing?". +**Readiness** is the one that controls traffic. It checks the database, Redis when it is enabled, and any custom indicators you add. Failing readiness stops new traffic without restarting the process, which gives a struggling server room to recover. -- Returns `200 OK` once server initialization (including migrations) is complete. -- Prevents premature liveness/readiness checks during boot. -- Kubernetes waits for this to pass before starting liveness/readiness probes. +**Startup** exists so the other two are not consulted too early. While it is failing, a platform holds off its liveness and readiness probes. In practice Serverpod opens its HTTP listeners as the last step of starting, so a probe sent during startup gets a refused connection rather than a `503`. -This endpoint will determine when the pod is ready to receive traffic. While this endpoint is failing, the orchestrator will not route any traffic to the pod. +### Response format -**Example:** +The probes follow the [draft standard for health check responses](https://datatracker.ietf.org/doc/html/draft-inadarei-api-health-check-06). -```bash -curl http://localhost:8080/startupz -# Returns: 200 OK once startup is complete -``` - -## Response format - -Health endpoints return JSON responses following the [RFC draft for Health Check Response Format](https://datatracker.ietf.org/doc/html/draft-inadarei-api-health-check-06). - -- **Unauthenticated requests** receive only HTTP status codes (no body) for security. -- **Authenticated requests** receive detailed JSON responses. - -The format of the response is as follows: +Requests without valid authentication get the status code and an empty body, so nothing about your dependencies is exposed publicly. An authenticated request gets the same status code plus a body. Any credential your server's authentication handler accepts unlocks the body, with no particular scope required. ```json { - "status": "pass", // or "fail" + "status": "pass", + "time": "2026-01-14T10:30:00Z", "checks": { - "database:connection": [ // The name of the check. + "database:connection": [ { - "componentId": "primary-db", // The ID of the component. - "componentType": "datastore", // The type of the component. - "status": "pass", // or "fail" - "observedValue": 12, // Optional value of the check. - "observedUnit": "ms", // Optional unit of the check. - "output": "Connection normal", // Optional output of the check. - "time": "2026-01-14T10:30:00Z" // The time of the check. + "componentType": "datastore", + "status": "pass", + "observedValue": 12, + "observedUnit": "ms", + "time": "2026-01-14T10:30:00Z" } ], - "redis:latency": [ + "redis:connection": [ { - "componentId": "cache-cluster", "componentType": "datastore", "status": "pass", - "observedValue": 3, - "observedUnit": "ms", "time": "2026-01-14T10:30:00Z" } ] @@ -98,21 +64,19 @@ The format of the response is as follows: } ``` -## Built-in health indicators - -Serverpod automatically registers health indicators based on your configuration: +When a check fails, the response also carries `notes` listing which ones. The `checks` object is left out when there is nothing to report, which is the normal case for `/livez`. -- **ServerpodStartupIndicator** - Tracks server initialization completion. -- **DatabaseHealthIndicator** - Checks database connectivity (if database is configured). -- **RedisHealthIndicator** - Checks Redis connectivity (if Redis is enabled). +### Built-in indicators -## Custom health indicators +Serverpod registers these based on your configuration: -You can add custom health indicators to check external services, microservices, or other dependencies. +- `serverpod:startup` records that the server has begun starting. +- `database:connection` checks the database, when one is configured. +- `redis:connection` checks Redis, when it is enabled. -### Creating a custom indicator +### Add your own indicator -Create a class that extends `HealthIndicator`: +Extend `HealthIndicator` to check something your server depends on, such as an external API: ```dart import 'package:serverpod/serverpod.dart'; @@ -134,13 +98,9 @@ class StripeApiIndicator extends HealthIndicator { Future check() async { final stopwatch = Stopwatch()..start(); try { - // Perform your health check await stripeClient.ping(); stopwatch.stop(); - - return pass( - observedValue: stopwatch.elapsedMilliseconds.toDouble(), - ); + return pass(observedValue: stopwatch.elapsedMilliseconds.toDouble()); } catch (e) { return fail(output: 'Stripe API unavailable: $e'); } @@ -148,9 +108,9 @@ class StripeApiIndicator extends HealthIndicator { } ``` -### Registering custom indicators +The type parameter is the type of `observedValue`, which is what the check reports alongside pass or fail. Use `output` to attach a message, as the failure branch above does. Override `componentId` when several instances of the same component exist, such as `primary-db` and `replica-db`, and the response should say which one answered. The built-in indicators leave it unset. -Register your indicators when creating the Serverpod instance: +Register it through `healthConfig` when you create the server, choosing the list by which probe should run it: ```dart final pod = Serverpod( @@ -159,42 +119,34 @@ final pod = Serverpod( Endpoints(), healthConfig: HealthConfig( cacheTtl: Duration(seconds: 2), - additionalReadinessIndicators: [ - StripeApiIndicator(), - InventoryServiceIndicator(), - ], - additionalStartupIndicators: [ - CacheWarmupIndicator(), - ], + additionalReadinessIndicators: [StripeApiIndicator()], + additionalStartupIndicators: [CacheWarmupIndicator()], ), ); ``` -### Configuration options +- `additionalReadinessIndicators` are checked by `/readyz`, so use them for dependencies that must be available to serve traffic. +- `additionalStartupIndicators` are checked by `/startupz`, so use them for work that has to finish before the server is ready at all, such as warming a cache. -The `HealthConfig` class provides the following options: +The `cacheTtl` option sets how long a result is reused before the check runs again, which keeps frequent probing from hammering your dependencies. It defaults to one second. Each indicator can set its own `timeout`, five seconds by default, so one slow check cannot hold up the whole response. -- **`cacheTtl`** - How long to cache health check results (default: 1 second). Prevents "thundering herd" during high-frequency probing. -- **`additionalReadinessIndicators`** - Custom indicators checked by `/readyz`. -- **`additionalStartupIndicators`** - Custom indicators checked by `/startupz`. +## Health metrics -Each indicator can specify its own timeout via the `timeout` getter (default: 5 seconds). This prevents slow checks from blocking the entire health endpoint. +Separately from the probes, Serverpod records numbers about itself: CPU, memory, and how long the database takes to respond. These go into the `serverpod_health_metric` and `serverpod_health_connection_info` tables, and Insights charts them. -## Health metrics collection +Collection runs once per `healthCheckInterval`, one minute by default. Setting the interval to zero turns collection off. A few conditions apply: a cycle writes nothing when the database has not been used since the last one, collection only runs in the `monolith` and `maintenance` [server roles](../server-fundamentals/running-your-server#choose-a-server-role), and it does not run on Windows. -Independently from the health check endpoints, Serverpod also collects health metrics about the server and its dependencies while running. Metrics like CPU, memory usage and response time to the database are stored in the database every minute in the `serverpod_health_metric` and `serverpod_health_connection_info` tables. Such metrics can be graphically visualized through Serverpod Insights. +Older rows are folded up rather than kept forever: minute rows become hourly after two days, and hourly rows become daily after about a month. -### Adding custom metrics +### Record your own metric -Add custom health metrics to monitor external services or internal processes. To set up your custom metrics, you must create a `HealthCheckHandler` and register it with your Serverpod. +A `HealthCheckHandler` runs on the same schedule as the built-in metrics and returns whatever you want recorded: ```dart -// Create your custom health metric handler. Future> myHealthCheckHandler( - Serverpod pod, DateTime timestamp) async { - // Actually perform some checks. - - // Return a list of health metrics for the given timestamp. + Serverpod pod, + DateTime timestamp, +) async { return [ ServerHealthMetric( name: 'MyMetric', @@ -202,20 +154,31 @@ Future> myHealthCheckHandler( timestamp: timestamp, isHealthy: true, value: 1.0, + granularity: 1, ), ]; } ``` -Register your handler when you create your Serverpod object. +The `granularity` field is the period the value covers, in minutes. Use `1` for values recorded on the normal cycle, since Serverpod produces the hourly and daily rows itself. + +Register the handler on the server: ```dart final pod = Serverpod( - args, - Protocol(), - Endpoints(), - healthCheckHandler: myHealthCheckHandler, - ); + args, + Protocol(), + Endpoints(), + healthCheckHandler: myHealthCheckHandler, +); ``` -Once registered, your health check handler will be called once a minute to perform any health checks that you have configured. You can view the status of your checks in Serverpod Insights or in the database. +:::note +The two APIs read alike but are not related. Use `healthConfig` with `HealthIndicator` for the HTTP probes, and `healthCheckHandler` with `ServerHealthMetric` for the recorded metrics. +::: + +## Related + +- [Logging](logging): the other half of knowing what your server is doing. +- [Insights](../../tools/insights): charts for the collected metrics. +- [Custom hosting](../../deployments/custom-hosting/choosing-a-strategy): wiring the probes up to your host. diff --git a/docs/06-concepts/07-operations/04-security-and-tls.md b/docs/06-concepts/07-operations/04-security-and-tls.md index edf787d1..89db776a 100644 --- a/docs/06-concepts/07-operations/04-security-and-tls.md +++ b/docs/06-concepts/07-operations/04-security-and-tls.md @@ -1,23 +1,18 @@ --- -description: Security configuration in Serverpod lets you enable TLS/SSL directly on the server or configure the client to trust a certificate, using SecurityContextConfig. +description: How traffic to a Serverpod server is encrypted, when something else handles that for you, and how to serve HTTPS directly from the server instead. --- # Security and TLS -Serverpod can terminate TLS/SSL directly on the server and configure the client to trust your certificate. +Traffic between your app and your server should be encrypted, which on the web means HTTPS. HTTPS is HTTP wrapped in TLS, and somewhere in the chain a piece of software has to hold your certificate and do the encrypting. That job is called terminating TLS. -:::info +Most of the time it is not your server doing it. On Serverpod Cloud, TLS is handled for you and there is nothing to configure. On your own infrastructure it is normally handled in front of the server, by a load balancer or reverse proxy such as Nginx, a cloud load balancer, or Cloudflare, which forwards plain HTTP to Serverpod on an internal network. -In a production environment, TLS termination is normally handled by a load balancer or reverse proxy (e.g., Nginx, AWS ALB, or Cloudflare). -However, Serverpod also supports setting up TLS/SSL directly on the server, allowing you to provide your own certificates if needed. +Serverpod can also terminate TLS itself, which is useful when there is nothing in front of it to do the job. -::: +## Serve HTTPS from the server -## Server security configuration - -To enable TLS/SSL, pass a `SecurityContextConfig` to the `Serverpod` constructor. - -### Dart configuration example +Pass a `SecurityContextConfig` when you create the server, with a certificate chain and private key for each server you want to secure: ```dart final securityContext = SecurityContext() @@ -36,13 +31,13 @@ Serverpod( ); ``` -## Client security configuration +A Serverpod instance runs [three servers](../server-fundamentals/your-serverpod-project#the-three-servers), and each takes its own context, so you can secure them independently. -When connecting to a Serverpod server over HTTPS, the client must be configured to trust the server's certificate. +## Trust the server's certificate from your app -### Dart configuration example +Your app only needs configuring when it cannot verify your certificate on its own. Certificates from a public authority, including the ones Serverpod Cloud provisions, are trusted automatically and need nothing here. -To enable SSL/TLS, pass a `SecurityContext` to the `Client` constructor. +Self-signed certificates and private certificate authorities are the exception. There, name the certificate you want trusted by passing a `SecurityContext` to the generated `Client`: ```dart final securityContext = SecurityContext() @@ -51,15 +46,14 @@ final securityContext = SecurityContext() final client = Client( 'https://yourserver.com', securityContext: securityContext, - ... ); ``` -#### Using `SecurityContext` with `httpClientOverride` +### With an HTTP client override -If you use the [`httpClientOverride` parameter](../endpoints-and-apis/configure-http-calls), provide the security context through the HTTP client you pass in. You cannot set `securityContext` and `httpClientOverride` on the same `Client` instance. +The `securityContext` and [`httpClientOverride`](../endpoints-and-apis/configure-http-calls) parameters cannot both be set on the same `Client`, since the override replaces the HTTP client the security context would have configured. Supply the certificates through the client you pass in instead. -For example, on `dart:io` platforms you can create an `HttpClient` with your trusted certificates and wrap it in an `IOClient`: +On `dart:io` platforms, build an `HttpClient` with your trusted certificates and wrap it: ```dart import 'dart:io'; @@ -76,3 +70,9 @@ final client = Client( ), ); ``` + +## Related + +- [Configure HTTP calls](../endpoints-and-apis/configure-http-calls): certificates and HTTP client overrides on the app side. +- [Configuration](../server-fundamentals/configuration): request size limits and header settings. +- [Custom hosting](../../deployments/custom-hosting/choosing-a-strategy): where a proxy fits when you host it yourself. diff --git a/docs/06-concepts/07-operations/05-exception-monitoring.md b/docs/06-concepts/07-operations/05-exception-monitoring.md new file mode 100644 index 00000000..f4e68947 --- /dev/null +++ b/docs/06-concepts/07-operations/05-exception-monitoring.md @@ -0,0 +1,68 @@ +--- +description: Diagnostic event handlers report exceptions from your code and from the framework to a monitoring service as they happen. +--- + +# Exception monitoring + +When something throws in production you want to hear about it without reading log tables. Serverpod can hand every exception to code you provide, so you can forward it to a monitoring service such as [Sentry](https://sentry.io/), [Highlight](https://www.highlight.io/), or [Datadog](https://www.datadoghq.com/). + +This works for exceptions thrown in your own code and for exceptions the framework raises itself, including failures during startup and shutdown. + +:::warning +This is an experimental feature, reached through `serverpod.experimental`. Its API can change in a breaking way in any minor release, so pin your Serverpod version if you depend on it and re-check this page when you upgrade. Experimental APIs move to `Serverpod` proper once they settle. +::: + +## Add a handler + +Handlers are registered through the `experimentalFeatures` argument on the `Serverpod` constructor. A handler receives the event, the space it came from, and its context: + +```dart +var pod = Serverpod( + args, + Protocol(), + Endpoints(), + experimentalFeatures: ExperimentalFeatures( + diagnosticEventHandlers: [ + AsEventHandler((event, {required space, required context}) { + print('$event Origin is $space\n Context is ${context.toJson()}'); + }), + ], + ), +); +``` + +The `AsEventHandler` class wraps a plain function as a handler, which is the shortest way to get started. For anything longer, implement `DiagnosticEventHandler` yourself. The `space` tells you whether the event came from your application code or from the framework, and the `context` carries details such as the server ID and run mode. + +Add as many handlers as you like. Serverpod runs them concurrently and does not wait for them, so a slow handler does not slow down the request that triggered it. Each invocation is abandoned after `experimentalDiagnosticHandlerTimeout`, which defaults to 30 seconds. See the [Configuration reference](../lookups/configuration-reference). + +Handlers are for observation only. They cannot suppress an exception, change it, or alter the response. If a handler throws, the error is logged and otherwise ignored. + +## Report an exception yourself + +Call `submitDiagnosticEvent` on the `experimental` member of the server to report something you caught: + +```dart +class OrderEndpoint extends Endpoint { + Future placeOrder(Session session) async { + try { + throw Exception('An exception is thrown'); + } catch (e, stackTrace) { + session.serverpod.experimental.submitDiagnosticEvent( + ExceptionEvent(e, stackTrace), + session: session, + ); + } + return 'success'; + } +} +``` + +The `ExceptionEvent` class is the built-in event for a thrown exception and a stack trace. Passing the session attaches its context to the event, so the handler knows which call the exception came from. This works anywhere you have a session, including endpoint methods, web calls, and future calls. + +## Related + +- [Logging](logging): what the server records without any handler. +- [Error handling and exceptions](../endpoints-and-apis/error-handling-and-exceptions): how exceptions reach your app. +- [Run code on shutdown](../server-fundamentals/running-your-server#run-code-on-shutdown): the other experimental API Serverpod exposes. +- [Configuration](../server-fundamentals/configuration#experimental-features): opting in to experimental features. +- [Testing](../testing/advanced-examples): testing that your handlers receive the events you expect. diff --git a/docs/06-concepts/07-operations/05-experimental-features.md b/docs/06-concepts/07-operations/05-experimental-features.md deleted file mode 100644 index 64fb35d0..00000000 --- a/docs/06-concepts/07-operations/05-experimental-features.md +++ /dev/null @@ -1,198 +0,0 @@ ---- -description: Experimental features in Serverpod are opt-in additions with unstable APIs, including diagnostic event handlers for exception monitoring and shutdown task registration. ---- - -# Experimental features - -:::warning -Be cautious when using experimental features in production environments, as their stability is uncertain and they may receive breaking changes in upcoming releases. -::: - -Experimental features are opt-in additions to Serverpod. They are disabled by default; enable them through the `experimentalFeatures` argument on the `Serverpod` constructor or via `config/generator.yaml`. - -:::note -To make the LSP server understand the usage of experimental flags and avoid complaints about unknown syntax on model files, configure experimental features in the `config/generator.yaml` file. See the [configuration documentation](../server-fundamentals/configuration#experimental-features) for more details. -::: - -## Experimental internal APIs - -Experimental internal APIs are placed under the `experimental` sub-API of the `Serverpod` class. -When an experimental feature matures it is moved from `experimental` to `Serverpod` proper. -If possible, the experimental API will remain for some time as `@deprecated`, and then removed. - -## Command-line enabled features - -Some of the experimental features are enabled by including the `--experimental-features` flag when running the serverpod command: - -```bash -$ serverpod generate --experimental-features=all -``` - -The current options you can pass are: - -| **Feature** | Description | -| :----------------- | :------------------------------------------------------------- | -| **all** | Enables all available experimental features. | - -## Exception monitoring - -Serverpod allows you to monitor exceptions in a central and flexible way by using diagnostic event handlers. -These work both for exceptions thrown in application code and from the framework (e.g. server startup or shutdown errors). - -This can be used to get all exceptions reported in realtime to services for monitoring and diagnostics, -such as [Sentry](https://sentry.io/), [Highlight](https://www.highlight.io/), and [Datadog](https://www.datadoghq.com/). - -It is easy to implement handlers and define custom filters within them. -Any number of handlers can be added. -They are run asynchronously and should not affect the behavior or response times of the server. - -These event handlers are for diagnostics only, -they do not allow any behavior-changing action such as suppressing exceptions or converting them to another exception type. - -### Setup - -This feature is enabled by providing one or more `DiagnosticEventHandler` implementations -to the Serverpod constructor's `experimentalFeatures` specification. - -Example: - -```dart - var serverpod = Serverpod( - ... - experimentalFeatures: ExperimentalFeatures( - diagnosticEventHandlers: [ - AsEventHandler((event, {required space, required context}) { - print('$event Origin is $space\n Context is ${context.toJson()}'); - }), - ], - ), - ); -``` - -### Submitting diagnostic events - -The API for submitting diagnostic events from user code, e.g. from endpoint methods, web calls, and future calls, -is the `submitDiagnosticEvent` method under the `experimental` member of the Serverpod class. - -```dart - void submitDiagnosticEvent( - DiagnosticEvent event, { - required Session session, - }) -``` - -Usage example: - -```dart -class DiagnosticEventTestEndpoint extends Endpoint { - Future submitExceptionEvent(Session session) async { - try { - throw Exception('An exception is thrown'); - } catch (e, stackTrace) { - session.serverpod.experimental.submitDiagnosticEvent( - ExceptionEvent(e, stackTrace), - session: session, - ); - } - return 'success'; - } -} -``` - -### Guidelines for handlers - -A `DiagnosticEvent` represents an event that occurs in the server. -Implementations of `DiagnosticEventHandler` can react to these events -in order to gain insights into the behavior of the server. - -As the name suggests the handlers should perform diagnostics only, -and not have any responsibilities that the regular functioning -of the server depends on. - -The registered handlers are typically run concurrently, -can not depend on each other, and asynchronously - -they are not awaited by the operation they are triggered from. - -If a handler throws an exception it will be logged to stderr -and otherwise ignored. - -### Test support - -This feature also includes support via the Serverpod test framework. -This means that the `withServerpod` construct can be used together with diagnostic event handlers to test that the events are submitted and propagated as intended. - -Example: - -```dart -void main() { - var exceptionHandler = TestExceptionHandler(); - - withServerpod('Given withServerpod with a diagnostic event handler', - experimentalFeatures: ExperimentalFeatures( - diagnosticEventHandlers: [exceptionHandler], - ), (sessionBuilder, endpoints) { - test( - 'when calling an endpoint method that submits an exception event ' - 'then the diagnostic event handler gets called', () async { - final result = await endpoints.diagnosticEventTest - .submitExceptionEvent(sessionBuilder); - expect(result, 'success'); - - final record = await exceptionHandler.events.first.timeout(Duration(seconds: 1)); - expect(record.event.exception, isA()); - expect(record.space, equals(OriginSpace.application)); - expect(record.context, isA()); - expect( - record.context.toJson(), - allOf([ - containsPair('serverId', 'default'), - containsPair('serverRunMode', 'test'), - containsPair('serverName', 'Server default'), - ])); - }); - }); -} -``` - -## Shutdown tasks - -Serverpod provides support for registering **shutdown tasks**: asynchronous operations that run when the server is shutting down. This is useful for performing cleanup operations such as saving application state or releasing external resources. - -Shutdown tasks are executed _after_ the server has stopped accepting new requests, but _before_ the Redis and database connections are closed. - -All registered shutdown tasks are executed concurrently, and the server waits for all tasks to complete before fully shutting down. If any task fails, the error is logged, but it does not prevent the server from shutting down. - -### Managing shutdown tasks - -To manage shutdown tasks, use the `experimental.shutdownTasks` API on your `Serverpod` instance. This API offers methods for adding and removing tasks. - -#### Add a shutdown task - -Each shutdown task is identified by a unique `Object` identifier and executes a function that returns a `Future`. - -To add a task, use the `addTask` method: - -```dart -var serverpod = Serverpod( - ... -); -serverpod.experimental.shutdownTasks.addTask( - #taskIdentifier, - () async { - // Your shutdown logic here - }, -); - -``` - -In the example above, a task is added with the identifier `#taskIdentifier`. This identifier is used for logging any errors that occur during task execution. - -#### Remove a shutdown task - -To remove a shutdown task, use the `removeTask` method: - -```dart -serverpod.experimental.shutdownTasks.removeTask(#taskIdentifier); -``` - -This will remove the previously registered task associated with `#taskIdentifier`. diff --git a/docs/06-concepts/08-testing/03-advanced-examples.md b/docs/06-concepts/08-testing/03-advanced-examples.md index 0ad02d23..6ed74d7b 100644 --- a/docs/06-concepts/08-testing/03-advanced-examples.md +++ b/docs/06-concepts/08-testing/03-advanced-examples.md @@ -153,3 +153,71 @@ withServerpod('Given example test', (sessionBuilder, endpoints) { This case should be rare and the above example is not a recommended best practice unless this problem is anticipated, or it has started happening. ::: + +## Testing exception monitoring + +`withServerpod` accepts the same `experimentalFeatures` argument as the server, so you can register a [diagnostic event handler](../operations/exception-monitoring) in a test and assert that your code reports the exceptions you expect. + +Write a handler that records what it receives, so the test can wait for an event: + +```dart +import 'dart:async'; + +import 'package:serverpod/serverpod.dart'; + +class TestExceptionHandler extends ExceptionHandler { + final eventsStreamController = + StreamController>(); + + Stream> get events => + eventsStreamController.stream; + + @override + Future handleTypedEvent( + ExceptionEvent event, { + required OriginSpace space, + required DiagnosticEventContext context, + }) async { + eventsStreamController.add(DiagnosticEventRecord(event, space, context)); + } +} +``` + +Then register it for the test run: + +```dart +void main() { + var exceptionHandler = TestExceptionHandler(); + + withServerpod( + 'Given withServerpod with a diagnostic event handler', + experimentalFeatures: ExperimentalFeatures( + diagnosticEventHandlers: [exceptionHandler], + ), + (sessionBuilder, endpoints) { + test( + 'when calling an endpoint method that submits an exception event ' + 'then the diagnostic event handler gets called', () async { + final result = await endpoints.order.placeOrder(sessionBuilder); + expect(result, 'success'); + + final record = + await exceptionHandler.events.first.timeout(Duration(seconds: 1)); + expect(record.event.exception, isA()); + expect(record.space, equals(OriginSpace.application)); + expect(record.context, isA()); + expect( + record.context.toJson(), + allOf([ + containsPair('serverId', 'default'), + containsPair('serverRunMode', 'test'), + containsPair('serverName', 'Server default'), + ]), + ); + }); + }, + ); +} +``` + +Handlers run asynchronously and are not awaited by the code that triggers them, so wait for the event rather than asserting immediately after the call. diff --git a/docs/06-concepts/lookups/configuration-reference.md b/docs/06-concepts/lookups/configuration-reference.md index b7007360..2cd47f0b 100644 --- a/docs/06-concepts/lookups/configuration-reference.md +++ b/docs/06-concepts/lookups/configuration-reference.md @@ -75,7 +75,7 @@ These options have no environment variable or config-file key. Set them on the ` | ServerpodConfig field | Default | Description | | ------------------------------------ | ------- | ------------------------------------------------------------------------------------------------------ | | healthCheckInterval | 1m | How often the server collects health metrics. Set to zero to disable health checks. | -| experimentalDiagnosticHandlerTimeout | 30s | The timeout for [diagnostic event handlers](../operations/experimental-features#exception-monitoring). | +| experimentalDiagnosticHandlerTimeout | 30s | The timeout for [diagnostic event handlers](../operations/exception-monitoring). | ### Password environment variables