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:
- Serializes information from each subgraph call into a
SubgraphInfo
struct. - Creates an
OperationInfo
struct from each operation call and combines related subgraph calls into it. - 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.