···1-# For documentation on how to configure this file,
2-# see https://diesel.rs/guides/configuring-diesel-cli
3-4-[print_schema]
5-file = "src/schema.rs"
6-custom_type_derives = ["diesel::query_builder::QueryId", "Clone", "std::fmt::Debug", "jacquard::IntoStatic"]
7-patch_file = "src/schema.patch"
8-9-10-[migrations_directory]
11-dir = "/home/orual/Projects/weaver.sh/crates/weaver-index/migrations"
···1--- This file should undo anything in `up.sql`
2-drop index if exists idx_oauth_auth_requests_did;
3-4-drop index if exists idx_oauth_auth_requests_expires;
5-6-drop index if exists idx_oauth_sessions_did;
7-8-drop index if exists idx_oauth_sessions_did_session;
9-10-drop table if exists oauth_auth_requests;
11-12-drop table if exists oauth_sessions;
13-14-drop table if exists profile_pronouns;
15-16-drop table if exists profile_links;
17-18-drop table if exists profile;
19-20-drop table if exists emails;
21-22-drop table if exists _jetstream;
23-24-drop table if exists follows;
25-26-drop table if exists public_keys;
27-28-drop table if exists registrations;
···1-create table if not exists registrations (
2- id integer not null primary key autoincrement,
3- domain text not null unique,
4- did text not null,
5- secret text not null,
6- created timestamp not null default (datetime('now')),
7- registered text
8-);
9-10-create table if not exists public_keys (
11- id integer not null primary key autoincrement,
12- did text not null,
13- name text not null,
14- key_contents text not null,
15- rkey text not null,
16- created timestamp not null default (datetime('now')),
17- unique (did, name, key_contents)
18-);
19-20-create table if not exists follows (
21- user_did text not null,
22- subject_did text not null,
23- rkey text not null,
24- followed_at timestamp not null default (datetime('now')),
25- primary key (user_did, subject_did),
26- check (user_did <> subject_did)
27-);
28-29-create table if not exists _jetstream (
30- id integer not null primary key autoincrement,
31- last_time_us integer not null
32-);
33-34-create table if not exists emails (
35- id integer not null primary key autoincrement,
36- did text not null,
37- email text not null,
38- verified boolean not null default false,
39- verification_code text not null,
40- last_sent timestamp not null default (datetime('now')),
41- is_primary boolean not null default false,
42- created timestamp not null default (datetime('now')),
43- unique (did, email)
44-);
45-46-create table if not exists profile (
47- -- id
48- id integer not null primary key autoincrement,
49- did text not null,
50- -- data
51- avatar text,
52- description text not null,
53- include_bluesky boolean not null default false,
54- include_tangled boolean not null default false,
55- location text,
56- pinned_post text,
57- created_at timestamp default (datetime('now')),
58- -- constraints
59- unique (did)
60-);
61-62-create table if not exists profile_links (
63- -- id
64- id integer not null primary key autoincrement,
65- did text not null,
66- -- data
67- link text not null,
68- -- constraints
69- foreign key (did) references profile (did) on delete cascade
70-);
71-72-create table if not exists profile_pronouns (
73- -- id
74- id integer not null primary key autoincrement,
75- did text not null,
76- -- data
77- pronoun text not null,
78- -- constraints
79- foreign key (did) references profile (did) on delete cascade
80-);
81-82--- OAuth sessions table for jacquard ClientSessionData
83-create table if not exists oauth_sessions (
84- id integer not null primary key autoincrement,
85- -- Extracted from ClientSessionData for indexing
86- did text not null,
87- session_id text not null,
88- -- Full ClientSessionData as JSON
89- session_data blob not null,
90- created_at timestamp not null default (datetime('now')),
91- updated_at timestamp not null default (datetime('now')),
92- unique (did, session_id)
93-);
94-95--- OAuth authorization requests table for jacquard AuthRequestData
96-create table if not exists oauth_auth_requests (
97- id integer not null primary key autoincrement,
98- -- Extracted from AuthRequestData for indexing
99- state text not null unique,
100- -- Optional DID if known at auth request time
101- account_did text,
102- -- Full AuthRequestData as JSON
103- auth_req_data blob not null,
104- created_at timestamp not null default (datetime('now')),
105- expires_at timestamp not null default (datetime('now', '+10 minutes'))
106-);
107-108--- Index for quick session lookups
109-create index if not exists idx_oauth_sessions_did_session on oauth_sessions(did, session_id);
110-111--- Index for DID lookups
112-create index if not exists idx_oauth_sessions_did on oauth_sessions(did);
113-114--- Index for auth request cleanup
115-create index if not exists idx_oauth_auth_requests_expires on oauth_auth_requests(expires_at);
116-117--- Index for DID lookups in auth requests
118-create index if not exists idx_oauth_auth_requests_did on oauth_auth_requests(account_did) where account_did is not null;
···1+-- Migration tracking table
2+-- Tracks which migrations have been applied
3+4+CREATE TABLE IF NOT EXISTS _migrations (
5+ -- Migration filename (e.g., '001_raw_records.sql')
6+ name String,
7+8+ -- When this migration was applied
9+ applied_at DateTime64(3) DEFAULT now64(3)
10+)
11+ENGINE = MergeTree()
12+ORDER BY (name);
···1+-- Raw records from firehose/jetstream
2+-- Core table for all AT Protocol records before denormalization
3+--
4+-- Uses ReplacingMergeTree to deduplicate on (collection, did, rkey) keeping latest indexed_at
5+-- JSON column stores full record, extract fields only when needed for ORDER BY/WHERE/JOINs
6+7+CREATE TABLE IF NOT EXISTS raw_records (
8+ -- Decomposed AT URI components (at://did/collection/rkey)
9+ did String,
10+ collection LowCardinality(String),
11+ rkey String,
12+13+ -- Content identifier from the record
14+ cid String,
15+16+ -- Full record as native JSON (schema-flexible, queryable with record.field.subfield)
17+ record JSON,
18+19+ -- Operation: 'create', 'update', 'delete'
20+ operation LowCardinality(String),
21+22+ -- Firehose sequence number (metadata only, not for ordering - can jump on relay restart)
23+ seq UInt64,
24+25+ -- Event timestamp from firehose
26+ event_time DateTime64(3),
27+28+ -- When we indexed this record
29+ indexed_at DateTime64(3) DEFAULT now64(3),
30+31+ -- Materialized AT URI for convenience
32+ uri String MATERIALIZED concat('at://', did, '/', collection, '/', rkey)
33+)
34+ENGINE = ReplacingMergeTree(indexed_at)
35+ORDER BY (collection, did, rkey, indexed_at);
···1+-- Identity events from firehose (#identity messages)
2+-- Tracks handle changes, key rotation, etc.
3+4+CREATE TABLE IF NOT EXISTS raw_identity_events (
5+ -- The DID this identity event is about
6+ did String,
7+8+ -- Handle (may be empty if cleared)
9+ handle String,
10+11+ -- Sequence number from firehose
12+ seq UInt64,
13+14+ -- Event timestamp from firehose
15+ event_time DateTime64(3),
16+17+ -- When we indexed this event
18+ indexed_at DateTime64(3) DEFAULT now64(3)
19+)
20+ENGINE = MergeTree()
21+ORDER BY (did, indexed_at);
···1+-- Account events from firehose (#account messages)
2+-- Tracks account status changes: active, deactivated, deleted, suspended, takendown
3+4+CREATE TABLE IF NOT EXISTS raw_account_events (
5+ -- The DID this account event is about
6+ did String,
7+8+ -- Whether the account is active
9+ active UInt8,
10+11+ -- Account status: 'active', 'deactivated', 'deleted', 'suspended', 'takendown'
12+ status LowCardinality(String),
13+14+ -- Sequence number from firehose
15+ seq UInt64,
16+17+ -- Event timestamp from firehose
18+ event_time DateTime64(3),
19+20+ -- When we indexed this event
21+ indexed_at DateTime64(3) DEFAULT now64(3)
22+)
23+ENGINE = MergeTree()
24+ORDER BY (did, indexed_at);
···1+-- Dead-letter queue for malformed events
2+-- Events that couldn't be parsed or processed land here for debugging
3+4+CREATE TABLE IF NOT EXISTS raw_events_dlq (
5+ -- Event type we attempted to parse (if known)
6+ event_type LowCardinality(String),
7+8+ -- Raw event data (JSON string of whatever we received)
9+ raw_data String,
10+11+ -- Error message describing why parsing failed
12+ error_message String,
13+14+ -- Sequence number from firehose (if available)
15+ seq UInt64,
16+17+ -- When we received this event
18+ received_at DateTime64(3) DEFAULT now64(3)
19+)
20+ENGINE = MergeTree()
21+ORDER BY (received_at);
···1+-- Firehose cursor persistence
2+-- Tracks our position in the firehose stream for resumption after restart
3+4+CREATE TABLE IF NOT EXISTS firehose_cursor (
5+ -- Consumer identifier (allows multiple consumers with different cursors)
6+ consumer_id String,
7+8+ -- Last successfully processed sequence number
9+ seq UInt64,
10+11+ -- Timestamp of the last processed event
12+ event_time DateTime64(3),
13+14+ -- When we saved this cursor
15+ updated_at DateTime64(3) DEFAULT now64(3)
16+)
17+ENGINE = ReplacingMergeTree(updated_at)
18+ORDER BY (consumer_id);
-90
crates/weaver-index/src/api_error.rs
···1-use axum::{
2- Json,
3- extract::rejection::JsonRejection,
4- response::{IntoResponse, Response},
5-};
6-use hyper::StatusCode;
7-use miette::Diagnostic;
8-use serde::{Deserialize, Serialize};
9-use thiserror::Error;
10-use tracing::error;
11-12-/// Custom error type for the API.
13-/// The `#[from]` attribute allows for easy conversion from other error types.
14-#[derive(Error, Debug, Diagnostic)]
15-pub enum ApiError {
16- /// Converts from an Axum built-in extractor error.
17- #[diagnostic_source]
18- #[error("Invalid payload.")]
19- InvalidJsonBody(#[from] JsonRejection),
20-21- /// For errors that occur during manual validation.
22- #[error("Invalid request: {0}")]
23- #[diagnostic()]
24- InvalidRequest(String),
25-26- /// Converts from `sqlx::Error`.
27- #[error("A database error has occurred.")]
28- #[diagnostic_source]
29- DatabaseError(#[from] diesel::result::Error),
30-31- #[error("A Weaver error has occurred.")]
32- #[diagnostic(transparent)]
33- WeaverError(#[from] weaver_common::error::WeaverError),
34- /// Converts from any `anyhow::Error`.
35- #[error("An internal server error has occurred.")]
36- #[diagnostic(transparent)]
37- InternalError(miette::Report),
38-}
39-40-impl From<miette::Report> for ApiError {
41- fn from(err: miette::Report) -> Self {
42- ApiError::InternalError(err)
43- }
44-}
45-46-#[derive(Serialize, Deserialize)]
47-pub struct ApiErrorResp {
48- pub message: String,
49-}
50-51-// The IntoResponse implementation for ApiError logs the error message.
52-//
53-// To avoid exposing implementation details to API consumers, we separate
54-// the message that we log from the API response message.
55-impl IntoResponse for ApiError {
56- fn into_response(self) -> Response {
57- // Log detailed error for telemetry.
58- let error_to_log = match &self {
59- ApiError::InvalidJsonBody(err) => match err {
60- JsonRejection::JsonDataError(e) => e.body_text(),
61- JsonRejection::JsonSyntaxError(e) => e.body_text(),
62- JsonRejection::MissingJsonContentType(_) => {
63- "Missing `Content-Type: application/json` header".to_string()
64- }
65- JsonRejection::BytesRejection(_) => "Failed to buffer request body".to_string(),
66- _ => "Unknown error".to_string(),
67- },
68- ApiError::InvalidRequest(_) => format!("{}", self),
69- ApiError::WeaverError(err) => format!("{}", err),
70- ApiError::DatabaseError(err) => format!("{}", err),
71- ApiError::InternalError(err) => format!("{}", err),
72- };
73- error!("{}", error_to_log);
74-75- // Create a generic response to hide specific implementation details.
76- let resp = ApiErrorResp {
77- message: self.to_string(),
78- };
79-80- // Determine the appropriate status code.
81- let status = match self {
82- ApiError::InvalidJsonBody(_) | ApiError::InvalidRequest(_) => StatusCode::BAD_REQUEST,
83- ApiError::WeaverError(_) | ApiError::DatabaseError(_) | ApiError::InternalError(_) => {
84- StatusCode::INTERNAL_SERVER_ERROR
85- }
86- };
87-88- (status, Json(resp)).into_response()
89- }
90-}
···1-use diesel::prelude::*;
2-use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations};
3-pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!();
4-use diesel_async::RunQueryDsl;
5-use diesel_async::pooled_connection::AsyncDieselConnectionManager;
6-use diesel_async::pooled_connection::deadpool::Pool;
7-use diesel_async::sync_connection_wrapper::SyncConnectionWrapper;
8-9-#[derive(Clone)]
10-pub struct Db {
11- pub pool: Pool<SyncConnectionWrapper<SqliteConnection>>,
12-}
13-14-impl Db {
15- /// Yes, this fuction can and WILL panic if it can't create the connection pool
16- /// for some reason. We just want to bail because the appview
17- /// does not work without a database.
18- pub async fn new(db_path: Option<String>) -> Self {
19- let database_url = if let Some(db_path) = db_path {
20- db_path
21- } else {
22- std::env::var("DATABASE_URL").expect("DATABASE_URL must be set")
23- };
24- let config = AsyncDieselConnectionManager::<SyncConnectionWrapper<SqliteConnection>>::new(
25- database_url,
26- );
27- let pool = Pool::builder(config)
28- .build()
29- .expect("Failed to create pool");
30- Self { pool }
31- }
32-}
33-34-pub fn run_migrations(
35- db_path: Option<String>,
36-) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
37- let database_url = if let Some(db_path) = db_path {
38- db_path
39- } else {
40- std::env::var("DATABASE_URL").expect("DATABASE_URL must be set")
41- };
42- let mut connection = SqliteConnection::establish(&database_url)
43- .unwrap_or_else(|_| panic!("Error connecting to {}", database_url));
44- // This will run the necessary migrations.
45- //
46- // See the documentation for `MigrationHarness` for
47- // all available methods.
48- println!("Attempting migrations...");
49- let result = connection.run_pending_migrations(MIGRATIONS);
50- println!("{:?}", result);
51- if result.is_err() {
52- println!("Failed to run migrations");
53- return result.map(|_| ());
54- }
55- println!("Migrations Applied:");
56- let applied_migrations = connection.applied_migrations()?;
57- for migration in applied_migrations {
58- println!(" * {}", migration);
59- }
60- Ok(())
61-}
62-63-pub struct Runtime;
···1+use crate::error::{CarError, IndexError};
2+use bytes::Bytes;
3+use jacquard_repo::car::reader::parse_car_bytes;
4+use smol_str::{SmolStr, ToSmolStr};
5+6+use super::consumer::Commit;
7+8+/// An extracted record from a firehose commit
9+#[derive(Debug, Clone)]
10+pub struct ExtractedRecord {
11+ /// DID of the repo owner
12+ pub did: SmolStr,
13+ /// Collection NSID (e.g., "app.bsky.feed.post")
14+ pub collection: SmolStr,
15+ /// Record key within the collection
16+ pub rkey: SmolStr,
17+ /// Content identifier
18+ pub cid: String,
19+ /// Operation type: "create", "update", or "delete"
20+ pub operation: SmolStr,
21+ /// Raw DAG-CBOR bytes of the record (None for deletes)
22+ pub cbor_bytes: Option<Bytes>,
23+ /// Sequence number from the firehose event
24+ pub seq: i64,
25+ /// Event timestamp (milliseconds since epoch)
26+ pub event_time_ms: i64,
27+}
28+29+impl ExtractedRecord {
30+ /// Decode the CBOR bytes to JSON string
31+ ///
32+ /// Uses jacquard's RawData type which properly handles CID links
33+ /// and other AT Protocol specific types.
34+ pub fn to_json(&self) -> Result<Option<String>, IndexError> {
35+ use jacquard_common::types::value::{RawData, from_cbor};
36+37+ match &self.cbor_bytes {
38+ Some(bytes) => {
39+ // RawData handles CID links and other IPLD types correctly
40+ let value: RawData<'static> =
41+ from_cbor::<RawData>(bytes).map_err(|e| CarError::RecordDecode {
42+ message: format!("failed to decode DAG-CBOR: {}", e),
43+ })?;
44+ let json = serde_json::to_string(&value).map_err(|e| CarError::RecordDecode {
45+ message: format!("failed to encode JSON: {}", e),
46+ })?;
47+ Ok(Some(json))
48+ }
49+ None => Ok(None),
50+ }
51+ }
52+}
53+54+/// Extract records from a firehose commit
55+///
56+/// Parses the CAR data and extracts each record referenced by the operations.
57+pub async fn extract_records(commit: &Commit<'_>) -> Result<Vec<ExtractedRecord>, IndexError> {
58+ let parsed_car = parse_car_bytes(&commit.blocks)
59+ .await
60+ .map_err(|e| CarError::Parse {
61+ message: e.to_string(),
62+ })?;
63+64+ let event_time_ms = commit.time.as_ref().timestamp_millis();
65+ let mut records = Vec::with_capacity(commit.ops.len());
66+67+ for op in &commit.ops {
68+ let path: &str = op.path.as_ref();
69+70+ // Path format: "collection/rkey"
71+ let (collection, rkey) = match path.split_once('/') {
72+ Some((c, r)) => (c.to_smolstr(), r.to_smolstr()),
73+ None => {
74+ tracing::warn!(path = %path, "invalid op path format, skipping");
75+ continue;
76+ }
77+ };
78+79+ let operation = op.action.to_smolstr();
80+ let cid_str = op.cid.as_ref().map(|c| c.to_string()).unwrap_or_default();
81+82+ // For creates/updates, look up the record in the CAR blocks
83+ let cbor_bytes = if let Some(cid_link) = &op.cid {
84+ match cid_link.0.to_ipld() {
85+ Ok(ipld_cid) => parsed_car.blocks.get(&ipld_cid).cloned(),
86+ Err(_) => {
87+ tracing::warn!(cid = %cid_str, "failed to convert CID to IPLD format");
88+ None
89+ }
90+ }
91+ } else {
92+ None
93+ };
94+95+ records.push(ExtractedRecord {
96+ did: commit.repo.to_smolstr(),
97+ collection,
98+ rkey,
99+ cid: cid_str,
100+ operation,
101+ cbor_bytes,
102+ seq: commit.seq,
103+ event_time_ms,
104+ });
105+ }
106+107+ Ok(records)
108+}
+7
crates/weaver-index/src/lib.rs
···0000000
···1+pub mod clickhouse;
2+pub mod config;
3+pub mod error;
4+pub mod firehose;
5+6+pub use config::Config;
7+pub use error::{IndexError, Result};
-134
crates/weaver-index/src/main.rs
···1-pub mod api_error;
2-3-pub mod config;
4-pub mod db;
5-pub mod middleware;
6-pub mod models;
7-pub mod oauth;
8-pub mod routes;
9-pub mod schema;
10-pub mod state;
11-pub mod telemetry;
12-13-use axum::Router;
14-use clap::Parser;
15-use config::*;
16-use db::*;
17-use dotenvy::dotenv;
18-use miette::IntoDiagnostic;
19-use miette::miette;
20-use state::*;
21-use std::env;
22-23-use tokio::net::TcpListener;
24-use tracing::{debug, error, info};
25-26-#[derive(Parser)]
27-#[command(author, version, about, long_about = None)]
28-struct Cli {
29- #[arg(
30- short,
31- long,
32- value_name = "FILE",
33- default_value = "appview-config.toml"
34- )]
35- config: String,
36-}
37-38-#[tokio::main]
39-async fn main() -> miette::Result<()> {
40- let config = initialize()?;
41- // Run any migrations before we do anything else.
42- let db_path = config.core.db_path.clone();
43- let _ = tokio::task::spawn_blocking(|| db::run_migrations(Some(db_path)))
44- .await
45- .into_diagnostic()?;
46- let db = Db::new(Some(config.core.db_path.clone())).await;
47- debug!("Connected to database");
48- // Spin up our server.
49- info!("Starting server on {}", config.core.listen_addr);
50- let listener = TcpListener::bind(&config.core.listen_addr)
51- .await
52- .expect("Failed to bind address");
53- let router = router(config, db);
54- axum::serve(listener, router)
55- .await
56- .expect("Failed to start server");
57- Ok(())
58-}
59-60-pub fn router(cfg: Config, db: Db) -> Router {
61- let app_state = AppState::new(cfg, db);
62-63- // Middleware that adds high level tracing to a Service.
64- // Trace comes with good defaults but also supports customizing many aspects of the output:
65- // https://docs.rs/tower-http/latest/tower_http/trace/index.html
66- let trace_layer = telemetry::trace_layer();
67-68- // Sets 'x-request-id' header with randomly generated uuid v7.
69- let request_id_layer = middleware::request_id_layer();
70-71- // Propagates 'x-request-id' header from the request to the response.
72- let propagate_request_id_layer = middleware::propagate_request_id_layer();
73-74- // Layer that applies the Cors middleware which adds headers for CORS.
75- let cors_layer = middleware::cors_layer();
76-77- // Layer that applies the Timeout middleware, which sets a timeout for requests.
78- // The default value is 15 seconds.
79- let timeout_layer = middleware::timeout_layer();
80-81- // Any trailing slashes from request paths will be removed. For example, a request with `/foo/`
82- // will be changed to `/foo` before reaching the internal service.
83- let normalize_path_layer = middleware::normalize_path_layer();
84-85- // Create the router with the routes.
86- let router = routes::router();
87-88- // Combine all the routes and apply the middleware layers.
89- // The order of the layers is important. The first layer is the outermost layer.
90- Router::new()
91- .merge(router)
92- .layer(normalize_path_layer)
93- .layer(cors_layer)
94- .layer(timeout_layer)
95- .layer(propagate_request_id_layer)
96- .layer(trace_layer)
97- .layer(request_id_layer)
98- .with_state(app_state)
99-}
100-101-pub fn initialize() -> miette::Result<Config> {
102- miette::set_hook(Box::new(|_| {
103- Box::new(
104- miette::MietteHandlerOpts::new()
105- .terminal_links(true)
106- //.rgb_colors(miette::RgbColors::)
107- .with_cause_chain()
108- .with_syntax_highlighting(miette::highlighters::SyntectHighlighter::default())
109- .color(true)
110- .context_lines(5)
111- .tab_width(2)
112- .break_words(true)
113- .build(),
114- )
115- }))
116- .map_err(|e| miette!("Failed to set miette hook: {}", e))?;
117- miette::set_panic_hook();
118- dotenv().ok();
119- let cli = Cli::parse();
120- let config = config::Config::load(&cli.config);
121- let config = if let Err(e) = config {
122- error!("{}", e);
123- config::Config::load(
124- &env::var("APPVIEW_CONFIG").expect("Either set APPVIEW_CONFIG to the path to your config file, pass --config FILE to specify the path, or create a file called appview-config.toml in the directory where you are running the binary from."),
125- )
126- .map_err(|e| miette!(e))
127- } else {
128- config
129- }?;
130- let log_dir = env::var("LOG_DIR").unwrap_or_else(|_| "/tmp/appview".to_string());
131- std::fs::create_dir_all(&log_dir).unwrap();
132- let _guard = telemetry::setup_tracing(&log_dir);
133- Ok(config)
134-}
···1-use std::time::Duration;
2-3-use axum::http::HeaderName;
4-use hyper::Request;
5-use tower_http::{
6- cors::{AllowHeaders, Any, CorsLayer},
7- normalize_path::NormalizePathLayer,
8- request_id::{MakeRequestId, PropagateRequestIdLayer, RequestId, SetRequestIdLayer},
9- timeout::TimeoutLayer,
10-};
11-12-#[derive(Clone, Default)]
13-pub struct Id;
14-15-impl MakeRequestId for Id {
16- fn make_request_id<B>(&mut self, _: &Request<B>) -> Option<RequestId> {
17- let id = uuid::Uuid::now_v7().to_string().parse().unwrap();
18- Some(RequestId::new(id))
19- }
20-}
21-22-/// Sets the 'x-request-id' header with a randomly generated UUID v7.
23-///
24-/// SetRequestId will not override request IDs if they are already present
25-/// on requests or responses.
26-pub fn request_id_layer() -> SetRequestIdLayer<Id> {
27- let x_request_id = HeaderName::from_static("x-request-id");
28- SetRequestIdLayer::new(x_request_id.clone(), Id)
29-}
30-31-// Propagates 'x-request-id' header from the request to the response.
32-///
33-/// PropagateRequestId wont override request ids if its already
34-/// present on requests or responses.
35-pub fn propagate_request_id_layer() -> PropagateRequestIdLayer {
36- let x_request_id = HeaderName::from_static("x-request-id");
37- PropagateRequestIdLayer::new(x_request_id)
38-}
39-40-/// Layer that applies the Cors middleware which adds headers for CORS.
41-pub fn cors_layer() -> CorsLayer {
42- CorsLayer::new()
43- .allow_origin(Any)
44- .allow_methods(Any)
45- .allow_headers(AllowHeaders::mirror_request())
46- .max_age(Duration::from_secs(600))
47-}
48-49-/// Layer that applies the Timeout middleware which apply a timeout to requests.
50-/// The default timeout value is set to 15 seconds.
51-pub fn timeout_layer() -> TimeoutLayer {
52- TimeoutLayer::new(Duration::from_secs(15))
53-}
54-55-/// Middleware that normalizes paths.
56-///
57-/// Any trailing slashes from request paths will be removed. For example, a request with `/foo/`
58-/// will be changed to `/foo` before reaching the inner service.
59-pub fn normalize_path_layer() -> NormalizePathLayer {
60- NormalizePathLayer::trim_trailing_slash()
61-}
···1-use tower_http::{
2- classify::{ServerErrorsAsFailures, SharedClassifier},
3- trace::{DefaultMakeSpan, DefaultOnRequest, DefaultOnResponse, TraceLayer},
4-};
5-use tracing::Level;
6-use tracing_appender::{self, non_blocking, non_blocking::WorkerGuard, rolling::daily};
7-use tracing_subscriber::{
8- EnvFilter,
9- fmt::{self, layer, writer::MakeWriterExt},
10- layer::SubscriberExt,
11- registry,
12- util::SubscriberInitExt,
13-};
14-/// The `EnvFilter` type is used to filter log events based on the value of an environment variable.
15-/// In this case, we are using the `try_from_default_env` method to attempt to read the `RUST_LOG` environment variable,
16-/// which is used to set the log level for the application.
17-/// If the environment variable is not set, we default to the log level of `debug`.
18-/// The `RUST_LOG` environment variable is set in the Dockerfile and .env files.
19-pub fn setup_tracing<S: AsRef<str>>(logdir: S) -> WorkerGuard {
20- let (non_blocking_appender, guard) = non_blocking(daily(logdir.as_ref(), "general.log"));
21- let env_filter_layer = EnvFilter::try_from_default_env().unwrap_or_else(|_| {
22- format!(
23- "debug,{}=debug,tower_http=debug,axum=debug,hyper=debug,axum::rejection=trace,markdown=info",
24- env!("CARGO_PKG_NAME"),
25- ).into()
26- });
27- let formatting_layer = fmt::layer().json();
28- tracing_subscriber::registry()
29- .with(env_filter_layer)
30- .with(formatting_layer)
31- .with(
32- layer()
33- .with_writer(std::io::stdout.with_max_level(Level::DEBUG))
34- .event_format(tracing_subscriber::fmt::format().pretty()),
35- )
36- .with(layer().with_writer(non_blocking_appender.with_max_level(Level::INFO)))
37- .init();
38- guard
39-}
40-41-/// Returns a `TraceLayer` for HTTP requests and responses.
42-/// The `TraceLayer` is used to trace requests and responses in the application.
43-pub fn trace_layer() -> TraceLayer<SharedClassifier<ServerErrorsAsFailures>> {
44- TraceLayer::new_for_http()
45- .make_span_with(DefaultMakeSpan::new().level(Level::INFO))
46- .on_request(DefaultOnRequest::new().level(Level::INFO))
47- .on_response(DefaultOnResponse::new().level(Level::INFO))
48-}