async_graphql_logger/
lib.rs1use async_graphql::{
2 extensions::{
3 Extension, ExtensionContext, ExtensionFactory, NextExecute, NextParseQuery, NextValidation,
4 },
5 parser::types::{ExecutableDocument, OperationType, Selection},
6 Response, ServerError, ServerResult, ValidationResult, Variables,
7};
8use chrono::{DateTime, Utc};
9use rand::Rng;
10use std::sync::Arc;
11use tokio::sync::Mutex;
12
13pub struct Logger;
14
15impl ExtensionFactory for Logger {
16 fn create(&self) -> Arc<dyn Extension> {
17 Arc::new(LoggerExtension {})
18 }
19}
20
21struct LoggerExtension {}
22
23#[derive(Debug, Default)]
24pub struct QueryInfo {
25 inner: Arc<Mutex<QueryInfoInner>>,
26}
27impl QueryInfo {
28 pub fn new() -> QueryInfo {
29 let mut rng = rand::thread_rng();
30 QueryInfo {
31 inner: Arc::new(Mutex::new(QueryInfoInner {
32 id: rng.gen_range(100_000_000..=999_999_999),
33 start_time: Utc::now(),
34 is_schema: false,
35 })),
36 }
37 }
38}
39
40#[derive(Debug, Default)]
41pub struct QueryInfoInner {
42 id: u64,
43 start_time: DateTime<Utc>,
44 is_schema: bool,
45}
46
47#[async_trait::async_trait]
48impl Extension for LoggerExtension {
49 async fn validation(
50 &self,
51 ctx: &ExtensionContext<'_>,
52 next: NextValidation<'_>,
53 ) -> Result<ValidationResult, Vec<ServerError>> {
54 let res = next.run(ctx).await;
55 match res {
56 Ok(_) => res,
57 Err(ref errors) => match ctx.data_opt::<QueryInfo>() {
58 Some(qinfo) => {
59 let qinfo = qinfo.inner.lock().await;
60 for e in errors {
61 log::info!(
62 target: "gql_logger",
63 "[QueryID: {}] Validation is failed with reason: {}",
64 qinfo.id,
65 e.message
66 )
67 }
68 res
69 }
70 None => res,
71 },
72 }
73 }
74
75 async fn parse_query(
76 &self,
77 ctx: &ExtensionContext<'_>,
78 query: &str,
79 variables: &Variables,
80 next: NextParseQuery<'_>,
81 ) -> ServerResult<ExecutableDocument> {
82 let document = next.run(ctx, query, variables).await?;
83 if let Some(qinfo) = ctx.data_opt::<QueryInfo>() {
84 let is_schema = document
85 .operations
86 .iter()
87 .filter(|(_, operation)| operation.node.ty == OperationType::Query)
88 .any(|(_, operation)| operation.node.selection_set.node.items.iter().any(
89 |selection| matches!(&selection.node, Selection::Field(field) if field.node.name.node == "__schema")));
90 let mut qinfo = qinfo.inner.lock().await;
91 qinfo.is_schema = is_schema;
92 if is_schema {
93 return Ok(document);
94 }
95 log::info!(target: "gql_logger", "[QueryID: {}] {}", qinfo.id, ctx.stringify_execute_doc(&document, variables));
96 }
97 Ok(document)
98 }
99
100 async fn execute(
101 &self,
102 ctx: &ExtensionContext<'_>,
103 operation_name: Option<&str>,
104 next: NextExecute<'_>,
105 ) -> Response {
106 let resp = next.run(ctx, operation_name).await;
107 if let Some(qinfo) = ctx.data_opt::<QueryInfo>() {
108 let qinfo = qinfo.inner.lock().await;
109 if qinfo.is_schema {
110 return resp;
111 }
112 let query_id = qinfo.id;
113
114 if resp.is_err() {
115 for err in &resp.errors {
116 log::info!(
117 target: "gql_logger",
118 "[QueryID: {query_id}] [Error] {}", err.message,
119 );
120 }
121 }
122 let query_start_time = qinfo.start_time;
123 let duration = Utc::now() - query_start_time;
124 log::debug!(target: "gql_logger",
125 "[QueryID: {query_id}] Response: {}", resp.data);
126 log::info!(
127 target: "gql_logger",
128 "[QueryID: {query_id}] Duration: {}ms",
129 duration.num_milliseconds()
130 );
131 }
132
133 resp
134 }
135}