Access Logs

Track the activity of the Grafbase Gateway with access logs. These logs require custom configuration and definition through gateway hooks, unlike system logs. Collect data about request execution and response at three points in the request lifecycle: after a subgraph request, after an operation, and right before sending a response to the user.

First, check out our hooks guide to learn the basics of custom hook implementation.

Enable access logs in the Gateway configuration:

[gateway.access_logs] enabled = true path = "/path/to/logs"

Read more on configuration options.

With access logs enabled, invoking the host_io::access_log::send function will append the specified bytes to a file called access.log in the configured path.

The first two hooks can return a set of bytes. The return values of on_subgraph_response hooks appear in the ExecutedOperation of the on_operation_response hook, and the outputs of on_operation_response hooks are in the ExecutedHttpRequest of the on_http_response hook.

The metrics counter grafbase.gateway.access_log.pending increments with each log-access call and decrements once the bytes are written to the access.log. Monitoring this value is crucial. Each access_log::send call consumes memory until data gets written, and the channel can hold a maximum of 128,000 messages. For the blocking access log method, a full channel will block all access_log::send calls, while the non-blocking method returns errors, sending data back to the caller.

Use the hooks guide as the basis for the access logs implementation. The template project is a Rust project with the necessary dependencies and build instructions to compile the project.

The Cargo.toml file provides the dependencies and build instructions to compile the project:

[package] name = "my-hooks" version = "0.1.0" edition = "2021" license = "MIT" [dependencies] grafbase-hooks = { version = "*", features = ["derive"] } [lib] crate-type = ["cdylib"] [profile.release] codegen-units = 1 opt-level = "s" debug = false strip = true lto = true

You need to implement all the response hooks:

use grafbase_hooks::{ grafbase_hooks, SharedContext, ExecutedHttpRequest, ExecutedOperation, ExecutedSubgraphRequest, Hooks, }; struct MyHooks; #[grafbase_hooks] impl Hooks for MyHooks { fn new() -> Self where Self: Sized, { Self } fn on_subgraph_response( &mut self, context: SharedContext, request: ExecutedSubgraphRequest ) -> Vec<u8> { Vec::new() } fn on_operation_response( &mut self, context SharedContext, operation: ExecutedOperation ) -> Vec<u8> { Vec::new() } fn on_http_response( &mut self, context: SharedContext, request: ExecutedHttpRequest ) { } } grafbase_hooks::register_hooks!(Component);

The implementation is the simplest possible and doesn't really do anything.

We start building the access log row in the subgraph response handler. By using the postcard and serde crates to (de-)serialize the data:

#[derive(serde::Serialize, serde::Deserialize)] pub struct SubgraphInfo<'a> { pub subgraph_name: &'a str, pub method: &'a str, pub url: &'a str, pub has_errors: bool, pub cached: bool, } #[derive(serde::Serialize, serde::Deserialize)] pub struct OperationInfo<'a> { pub name: Option<&'a str>, pub document: &'a str, pub subgraphs: Vec<SubgraphInfo<'a>>, } #[derive(serde::Serialize, serde::Deserialize)] pub struct AuditInfo<'a> { pub method: &'a str, pub url: &'a str, pub status_code: u16, pub operations: Vec<OperationInfo<'a>>, }

The first three hooks manage aggregation of the data, and the last hook writes the data to the log file:

#[grafbase_hooks] impl Hooks for MyHooks { fn new() -> Self where Self: Sized, { Self } fn on_subgraph_response( &mut self, _: SharedContext, request: ExecutedSubgraphRequest ) -> Vec<u8> { let info = SubgraphInfo { subgraph_name: &request.subgraph_name, method: &request.method, url: &request.url, has_errors: request.has_errors, cached: matches!(request.cache_status, CacheStatus::Hit), }; postcard::to_stdvec(&info).unwrap() } fn on_operation_response( &mut self, _: SharedContext, operation: ExecutedOperation ) -> Vec<u8> { let info = OperationInfo { name: request.name.as_deref(), document: &request.document, subgraphs: request .on_subgraph_response_outputs .iter() .filter_map(|bytes| postcard::from_bytes(bytes).ok()) .collect(), }; postcard::to_stdvec(&info).unwrap() } fn on_http_response( &mut self, _: SharedContext, request: ExecutedHttpRequest ) { let info = AuditInfo { method: &request.method, url: &request.url, status_code: request.status_code, operations: request .on_operation_response_outputs .iter() .filter_map(|bytes| postcard::from_bytes(bytes).ok()) .collect(), }; grafbase_hooks::host_io::access_log::send(&serde_json::to_vec(&info).unwrap()).unwrap(); } }

This code performs three main tasks:

  1. Serializes information from each subgraph call into a SubgraphInfo struct.
  2. Creates an OperationInfo struct from each operation call and combines related subgraph calls into it.
  3. Builds an AuditInfo struct from each HTTP request and combines related operation calls into it.

The last hook calls the access_log::send method to serialize the final result to JSON, written to the access log.

The serialization of data can vary as long as it returns bytes. As an example, if your access.log contains JSON data, define structured data as Serde structures in the hooks and serialize them into bytes using serde_json.

See a full example project implementing access logs with Grafbase Gateway.