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