iota_graphql_rpc/extensions/
logger.rs1use std::{fmt::Write, net::SocketAddr, sync::Arc};
6
7use async_graphql::{
8 PathSegment, Response, ServerError, ServerResult, ValidationResult, Variables,
9 extensions::{
10 Extension, ExtensionContext, ExtensionFactory, NextExecute, NextParseQuery, NextResolve,
11 NextValidation, ResolveInfo,
12 },
13 parser::types::{ExecutableDocument, OperationType, Selection},
14};
15use async_graphql_value::ConstValue;
16use tracing::{debug, error, info, warn};
17use uuid::Uuid;
18
19use crate::{error::code, metrics::Metrics};
20
21#[derive(Clone, Debug)]
22pub struct LoggerConfig {
23 pub log_request_query: bool,
24 pub log_response: bool,
25 pub log_complexity: bool,
26}
27
28impl Default for LoggerConfig {
29 fn default() -> Self {
30 Self {
31 log_request_query: false,
32 log_response: true,
33 log_complexity: true,
34 }
35 }
36}
37
38#[derive(Clone, Debug, Default)]
39pub struct Logger {
40 config: LoggerConfig,
41}
42
43impl ExtensionFactory for Logger {
44 fn create(&self) -> Arc<dyn Extension> {
45 Arc::new(LoggerExtension {
46 config: self.config.clone(),
47 })
48 }
49}
50
51struct LoggerExtension {
52 config: LoggerConfig,
53}
54
55#[async_trait::async_trait]
56impl Extension for LoggerExtension {
57 async fn resolve(
60 &self,
61 ctx: &ExtensionContext<'_>,
62 info: ResolveInfo<'_>,
63 next: NextResolve<'_>,
64 ) -> ServerResult<Option<ConstValue>> {
65 if info.path_node.parent.is_none() {
66 ctx.data_unchecked::<Metrics>()
67 .request_metrics
68 .num_queries_top_level
69 .with_label_values(&[info.name])
70 .inc();
71 }
72 next.run(ctx, info).await
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 let is_schema = document
84 .operations
85 .iter()
86 .filter(|(_, operation)| operation.node.ty == OperationType::Query)
87 .any(|(_, operation)| operation.node.selection_set.node.items.iter().any(|selection| matches!(&selection.node, Selection::Field(field) if field.node.name.node == "__schema")));
88 let query_id: &Uuid = ctx.data_unchecked();
89 let session_id: &SocketAddr = ctx.data_unchecked();
90 if !is_schema && self.config.log_request_query {
91 info!(
92 %query_id,
93 %session_id,
94 "[Query] {}",
95 ctx.stringify_execute_doc(&document, variables)
96 );
97 }
98 Ok(document)
99 }
100
101 async fn validation(
102 &self,
103 ctx: &ExtensionContext<'_>,
104 next: NextValidation<'_>,
105 ) -> Result<ValidationResult, Vec<ServerError>> {
106 let res = next.run(ctx).await?;
107 let query_id: &Uuid = ctx.data_unchecked();
108 let session_id: &SocketAddr = ctx.data_unchecked();
109 if self.config.log_complexity {
110 info!(
111 %query_id,
112 %session_id,
113 complexity = res.complexity,
114 depth = res.depth,
115 "[Validation]",
116 );
117 }
118 Ok(res)
119 }
120
121 async fn execute(
122 &self,
123 ctx: &ExtensionContext<'_>,
124 operation_name: Option<&str>,
125 next: NextExecute<'_>,
126 ) -> Response {
127 let resp = next.run(ctx, operation_name).await;
128 let query_id: &Uuid = ctx.data_unchecked();
129 let session_id: &SocketAddr = ctx.data_unchecked();
130 if resp.is_err() {
131 for err in &resp.errors {
132 let error_code = &err.extensions.as_ref().and_then(|x| x.get("code"));
133 if !err.path.is_empty() {
134 let mut path = String::new();
135 for (idx, s) in err.path.iter().enumerate() {
136 if idx > 0 {
137 path.push('.');
138 }
139 match s {
140 PathSegment::Index(idx) => {
141 let _ = write!(&mut path, "{}", idx);
142 }
143 PathSegment::Field(name) => {
144 let _ = write!(&mut path, "{}", name);
145 }
146 }
147 }
148
149 if let Some(async_graphql_value::ConstValue::String(error_code)) = error_code {
150 if error_code.as_str() == code::INTERNAL_SERVER_ERROR {
151 error!(
152 %query_id,
153 %session_id,
154 error_code,
155 "[Response] path={} message={}",
156 path,
157 err.message,
158 );
159 } else {
160 info!(
161 %query_id,
162 %session_id,
163 error_code,
164 "[Response] path={} message={}",
165 path,
166 err.message,
167 );
168 }
169 } else {
170 warn!(
171 %query_id,
172 %session_id,
173 error_code = code::UNKNOWN,
174 "[Response] path={} message={}",
175 path,
176 err.message,
177 );
178 }
179 } else {
180 let error_code = if let Some(error_code) = error_code {
181 error_code.to_string()
182 } else {
183 code::UNKNOWN.to_string()
184 };
185 info!(
186 %query_id,
187 %session_id,
188 error_code,
189 "[Response] message={}", err.message
190 )
191 }
192 }
193 } else if self.config.log_response {
194 match operation_name {
195 Some("IntrospectionQuery") => {
196 debug!(
197 %query_id,
198 %session_id,
199 "[Schema] {}", resp.data
200 );
201 }
202 _ => info!(
203 %query_id,
204 %session_id,
205 "[Response] {}", resp.data
206 ),
207 }
208 }
209 resp
210 }
211}