Skip to main content

postrust_lambda/
lib.rs

1//! Postrust Lambda library.
2//!
3//! What the `postrust-lambda` binary is made of, for a Lambda binary of your
4//! own: one that serves routes of yours beside the API, added with
5//! [`postrust_server::Builder`].
6//!
7//! ```no_run
8//! use axum::{routing::post, Router};
9//! use postrust_lambda::postrust_server::{AppState, Builder, Host};
10//! use std::sync::Arc;
11//!
12//! #[tokio::main]
13//! async fn main() -> Result<(), postrust_lambda::Error> {
14//!     postrust_lambda::init_tracing();
15//!     let hooks: Router<Arc<AppState>> = Router::new().route("/stripe", post(|| async { "ok" }));
16//!     let app = Builder::new(postrust_lambda::config_from_env()?, Host::LAMBDA)
17//!         .nest("/hooks", hooks)
18//!         .build()
19//!         .await?;
20//!     postrust_lambda::run(app).await
21//! }
22//! ```
23//!
24//! Build the application before calling [`run`], in the init phase rather
25//! than on the first request: the connection and the schema cache are paid
26//! for once per sandbox, and a database that cannot be reached fails the init
27//! -- which Lambda reports and retries -- instead of failing every invocation.
28
29#![warn(missing_docs)]
30
31pub use postrust_auth;
32pub use postrust_core;
33pub use postrust_response;
34pub use postrust_server;
35pub use postrust_sql;
36
37/// The error a Lambda handler returns, as the runtime takes it.
38pub use lambda_http::Error;
39
40/// Log as the server does: `RUST_LOG` if set, otherwise `PGRST_LOG_LEVEL`,
41/// as text or as JSON lines by `PGRST_LOG_FORMAT`.
42///
43/// No timestamps, which CloudWatch adds to every line itself. Installs the
44/// global subscriber, so call it once, first.
45pub fn init_tracing() {
46    let filter = std::env::var("RUST_LOG").unwrap_or_else(|_| {
47        let level = std::env::var("PGRST_LOG_LEVEL")
48            .ok()
49            .and_then(|v| postrust_core::LogLevel::parse_config(&v))
50            .unwrap_or_else(|| postrust_core::AppConfig::default().log_level);
51        format!("postrust={}", level.to_tracing())
52    });
53    // `PGRST_LOG_FORMAT=json` as on the server, which CloudWatch Logs
54    // Insights can query by field. The request span and its `request_id` and
55    // `trace_id` come with the router, which is the server's.
56    let format = std::env::var("PGRST_LOG_FORMAT")
57        .ok()
58        .and_then(|v| postrust_core::LogFormat::parse_config(&v))
59        .unwrap_or_default();
60
61    // With the `otel` feature and an OTLP endpoint -- the ADOT collector
62    // extension, usually -- spans are exported beside the log. The log's
63    // filter then moves onto its own layer, as on the server, so the
64    // request's debug-level spans can be exported while the log stays at its
65    // level. Without one the subscriber is exactly what it always was.
66    #[cfg(feature = "otel")]
67    if let Some(otel) = postrust_server::otel::layer() {
68        use tracing_subscriber::layer::SubscriberExt as _;
69        use tracing_subscriber::util::SubscriberInitExt as _;
70        use tracing_subscriber::Layer as _;
71        let filter = match std::env::var_os("RUST_LOG") {
72            Some(_) => filter,
73            None => format!("{filter},opentelemetry=warn,opentelemetry_sdk=warn"),
74        };
75        let log = tracing_subscriber::fmt::layer()
76            .with_target(false)
77            .without_time();
78        let (text, json) = match format {
79            postrust_core::LogFormat::Text => (Some(log), None),
80            postrust_core::LogFormat::Json => (None, Some(log.json())),
81        };
82        tracing_subscriber::registry()
83            .with(
84                text.with_filter(tracing_subscriber::EnvFilter::new(&filter))
85                    .and_then(json.with_filter(tracing_subscriber::EnvFilter::new(&filter))),
86            )
87            .with(otel)
88            .init();
89        postrust_server::otel::log_startup();
90        return;
91    }
92
93    let builder = tracing_subscriber::fmt()
94        .with_env_filter(tracing_subscriber::EnvFilter::new(filter))
95        .with_target(false)
96        .without_time();
97    match format {
98        postrust_core::LogFormat::Text => builder.init(),
99        postrust_core::LogFormat::Json => builder.json().init(),
100    }
101    #[cfg(feature = "otel")]
102    postrust_server::otel::log_startup();
103}
104
105/// The configuration from the environment, sized for a Lambda sandbox.
106///
107/// A sandbox answers one invocation at a time, so the server's default pool
108/// holds connections nothing will use -- and every concurrent sandbox holds
109/// its own, which is how a burst of Lambdas exhausts `max_connections`. The
110/// pool is two connections unless `PGRST_DB_POOL` or `PGRST_DB_POOL_SIZE`
111/// says otherwise; two rather than one because a GraphQL mutation holds a
112/// transaction while its resolvers read.
113pub fn config_from_env() -> Result<postrust_core::AppConfig, Error> {
114    let mut config = postrust_core::AppConfig::try_from_env()?;
115    if ["PGRST_DB_POOL", "PGRST_DB_POOL_SIZE"]
116        .iter()
117        .all(|var| std::env::var_os(var).is_none())
118    {
119        config.db_pool_size = 2;
120    }
121    Ok(config)
122}
123
124/// Serve `app` to the Lambda runtime until the sandbox is shut down.
125///
126/// With the `otel` feature, metrics are started here (over OTLP, if it is
127/// configured; a function has no admin port to serve `/metrics` on), and
128/// whatever was recorded during an invocation is exported before its response
129/// goes back. See [`flush_after_each_invocation`].
130pub async fn run(app: postrust_server::App) -> Result<(), Error> {
131    #[cfg(feature = "otel")]
132    {
133        postrust_server::otel::init_metrics(false);
134        if postrust_server::otel::exporting() {
135            let router = app.router.layer(FlushLayer);
136            return lambda_http::run(router).await;
137        }
138    }
139    lambda_http::run(app.router).await
140}
141
142/// Lambda freezes a sandbox the moment an invocation's response is handed
143/// back, and may never thaw it. The SDK exports in batches from a thread of
144/// its own, on a timer, so whatever an invocation recorded would sit in a
145/// frozen buffer -- for minutes, or for ever. So each invocation flushes
146/// before it answers: spans and metrics go to the collector (the ADOT
147/// extension, on localhost) first, and the response after.
148///
149/// That puts the export on the invocation's latency: a round trip to a
150/// collector in the same sandbox, a millisecond or two. It is the price the
151/// ADOT documentation describes, and the only way not to lose the data.
152///
153/// The request span closes before the flush -- it belongs to the router
154/// inside this layer -- so the invocation's own trace is in what is sent.
155#[cfg(feature = "otel")]
156#[derive(Clone, Copy, Debug, Default)]
157pub struct FlushLayer;
158
159#[cfg(feature = "otel")]
160impl<S> tower::Layer<S> for FlushLayer {
161    type Service = FlushAfter<S>;
162
163    fn layer(&self, inner: S) -> Self::Service {
164        FlushAfter { inner }
165    }
166}
167
168/// The service [`FlushLayer`] produces.
169#[cfg(feature = "otel")]
170#[derive(Clone, Debug)]
171pub struct FlushAfter<S> {
172    inner: S,
173}
174
175#[cfg(feature = "otel")]
176impl<S, R> tower::Service<R> for FlushAfter<S>
177where
178    S: tower::Service<R>,
179    S::Future: Send + 'static,
180    S::Response: Send + 'static,
181    S::Error: Send + 'static,
182{
183    type Response = S::Response;
184    type Error = S::Error;
185    type Future =
186        std::pin::Pin<Box<dyn std::future::Future<Output = Result<S::Response, S::Error>> + Send>>;
187
188    fn poll_ready(
189        &mut self,
190        cx: &mut std::task::Context<'_>,
191    ) -> std::task::Poll<Result<(), Self::Error>> {
192        self.inner.poll_ready(cx)
193    }
194
195    fn call(&mut self, request: R) -> Self::Future {
196        let answer = self.inner.call(request);
197        Box::pin(async move {
198            // Awaited to completion and dropped before the flush, and with
199            // it the request's span.
200            let response = answer.await;
201            postrust_server::otel::flush().await;
202            response
203        })
204    }
205}
206
207/// Whether [`run`] flushes after each invocation: the `otel` feature is
208/// built in and something is exporting. For a binary that serves the
209/// router itself, through `lambda_http::run`, and wants the same: wrap the
210/// router in [`FlushLayer`] when this is true.
211#[cfg(feature = "otel")]
212pub fn flush_after_each_invocation() -> bool {
213    postrust_server::otel::exporting()
214}