Skip to content

Commit 68b6005

Browse files
lenardt-gerhardtsLenardt Gerhardtslovasoa
authored
Feature logger component (#1013)
* implemented logger Component * changed target for logger to "sqlpage::logger" and made the log::Level case insensitive * changed error message on missing message key, to be more precise * changed custom method for String to log::Level conversion to builtin method log::Level::from_str() * switched to utility method get_object_str and inlined constants * dynamically target based on file and statement * disabled ci error (large difference in enum variants) for ResponseWithWriter<S> * added functionality to work in Header context * Refactor log component to use compact error handling * Rename comp_str variable to component_name * Documented log component * fixxed missing values statement * fixxed pipeline errors * very simple test case for logger --------- Co-authored-by: Lenardt Gerhardts <[email protected]> Co-authored-by: lovasoa <[email protected]>
1 parent 1fec208 commit 68b6005

File tree

5 files changed

+129
-16
lines changed

5 files changed

+129
-16
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
INSERT INTO component(name, icon, description) VALUES
2+
('log', 'logs', 'A Component to log a message to the Servers STDOUT or Log file on page load');
3+
4+
INSERT INTO parameter(component, name, description, type, top_level, optional) SELECT 'log', * FROM (VALUES
5+
-- top level
6+
('message', 'The message that needs to be logged', 'TEXT', TRUE, FALSE),
7+
('priority', 'The priority which the message should be logged with. Possible values are [''trace'', ''debug'', ''info'', ''warn'', ''error''] and are not case sensitive. If this value is missing or not matching any possible values, the default priority will be ''info''.', 'TEXT', TRUE, TRUE)
8+
) x;
9+
10+
INSERT INTO example(component, description) VALUES
11+
('log', '
12+
### Hello World
13+
14+
Log a simple ''Hello, World!'' message on page load.
15+
16+
```sql
17+
SELECT ''log'' as component,
18+
''Hello, World!'' as message
19+
```
20+
21+
Output example:
22+
23+
```
24+
[2025-09-12T08:33:48.228Z INFO sqlpage::log from file "index.sql" in statement 3] Hello, World!
25+
```
26+
27+
### Priority
28+
29+
Change the priority to error.
30+
31+
```sql
32+
SELECT ''log'' as component,
33+
''This is a error message'' as message,
34+
''error'' as priority
35+
```
36+
37+
Output example:
38+
39+
```
40+
[2025-09-12T08:33:48.228Z ERROR sqlpage::log from file "index.sql" in header] This is a error message
41+
```
42+
43+
### Retrieve user data
44+
45+
```sql
46+
set username = ''user'' -- (retrieve username from somewhere)
47+
48+
select ''log'' as component,
49+
''403 - failed for '' || coalesce($username, ''None'') as output,
50+
''error'' as priority;
51+
```
52+
53+
Output example:
54+
55+
```
56+
[2025-09-12T08:33:48.228Z ERROR sqlpage::log from file "403.sql" in statement 7] 403 - failed for user
57+
```
58+
')

src/render.rs

Lines changed: 60 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,10 @@ use serde::Serialize;
5757
use serde_json::{json, Value};
5858
use std::borrow::Cow;
5959
use std::convert::TryFrom;
60+
use std::fmt::Write as _;
6061
use std::io::Write;
62+
use std::path::Path;
63+
use std::str::FromStr;
6164
use std::sync::Arc;
6265

6366
pub enum PageContext {
@@ -119,6 +122,7 @@ impl HeaderContext {
119122
Some(HeaderComponent::Cookie) => self.add_cookie(&data).map(PageContext::Header),
120123
Some(HeaderComponent::Authentication) => self.authentication(data).await,
121124
Some(HeaderComponent::Download) => self.download(&data),
125+
Some(HeaderComponent::Log) => self.log(&data),
122126
None => self.start_body(data).await,
123127
}
124128
}
@@ -360,6 +364,11 @@ impl HeaderContext {
360364
))
361365
}
362366

367+
fn log(self, data: &JsonValue) -> anyhow::Result<PageContext> {
368+
handle_log_component(&self.request_context.source_path, Option::None, data)?;
369+
Ok(PageContext::Header(self))
370+
}
371+
363372
async fn start_body(self, data: JsonValue) -> anyhow::Result<PageContext> {
364373
let html_renderer =
365374
HtmlRenderContext::new(self.app_state, self.request_context, self.writer, data)
@@ -721,27 +730,43 @@ impl<W: std::io::Write> HtmlRenderContext<W> {
721730
component.starts_with(PAGE_SHELL_COMPONENT)
722731
}
723732

733+
async fn handle_component(
734+
&mut self,
735+
component_name: &str,
736+
data: &JsonValue,
737+
) -> anyhow::Result<()> {
738+
if Self::is_shell_component(component_name) {
739+
bail!("There cannot be more than a single shell per page. You are trying to open the {} component, but a shell component is already opened for the current page. You can fix this by removing the extra shell component, or by moving this component to the top of the SQL file, before any other component that displays data.", component_name);
740+
}
741+
742+
if component_name == "log" {
743+
return handle_log_component(
744+
&self.request_context.source_path,
745+
Some(self.current_statement),
746+
data,
747+
);
748+
}
749+
750+
match self.open_component_with_data(component_name, &data).await {
751+
Ok(_) => Ok(()),
752+
Err(err) => match HeaderComponent::try_from(component_name) {
753+
Ok(_) => bail!("The {component_name} component cannot be used after data has already been sent to the client's browser. \n\
754+
This component must be used before any other component. \n\
755+
To fix this, either move the call to the '{component_name}' component to the top of the SQL file, \n\
756+
or create a new SQL file where '{component_name}' is the first component."),
757+
Err(()) => Err(err),
758+
},
759+
}
760+
}
761+
724762
pub async fn handle_row(&mut self, data: &JsonValue) -> anyhow::Result<()> {
725763
let new_component = get_object_str(data, "component");
726764
let current_component = self
727765
.current_component
728766
.as_ref()
729767
.map(SplitTemplateRenderer::name);
730-
if let Some(comp_str) = new_component {
731-
if Self::is_shell_component(comp_str) {
732-
bail!("There cannot be more than a single shell per page. You are trying to open the {} component, but a shell component is already opened for the current page. You can fix this by removing the extra shell component, or by moving this component to the top of the SQL file, before any other component that displays data.", comp_str);
733-
}
734-
735-
match self.open_component_with_data(comp_str, &data).await {
736-
Ok(_) => (),
737-
Err(err) => match HeaderComponent::try_from(comp_str) {
738-
Ok(_) => bail!("The {comp_str} component cannot be used after data has already been sent to the client's browser. \n\
739-
This component must be used before any other component. \n\
740-
To fix this, either move the call to the '{comp_str}' component to the top of the SQL file, \n\
741-
or create a new SQL file where '{comp_str}' is the first component."),
742-
Err(()) => return Err(err),
743-
},
744-
}
768+
if let Some(component_name) = new_component {
769+
self.handle_component(component_name, data).await?;
745770
} else if current_component.is_none() {
746771
self.open_component_with_data(DEFAULT_COMPONENT, &JsonValue::Null)
747772
.await?;
@@ -885,6 +910,24 @@ impl<W: std::io::Write> HtmlRenderContext<W> {
885910
}
886911
}
887912

913+
fn handle_log_component(
914+
source_path: &Path,
915+
current_statement: Option<usize>,
916+
data: &JsonValue,
917+
) -> anyhow::Result<()> {
918+
let priority = get_object_str(data, "priority").unwrap_or("info");
919+
let log_level = log::Level::from_str(priority).with_context(|| "Invalid log priority value")?;
920+
921+
let mut target = format!("sqlpage::log from \"{}\"", source_path.display());
922+
if let Some(current_statement) = current_statement {
923+
write!(&mut target, " statement {current_statement}")?;
924+
}
925+
926+
let message = get_object_str(data, "message").context("log: missing property 'message'")?;
927+
log::log!(target: &target, log_level, "{message}");
928+
Ok(())
929+
}
930+
888931
pub(super) fn get_backtrace_as_strings(error: &anyhow::Error) -> Vec<String> {
889932
let mut backtrace = vec![];
890933
let mut source = error.source();
@@ -1108,6 +1151,7 @@ enum HeaderComponent {
11081151
Cookie,
11091152
Authentication,
11101153
Download,
1154+
Log,
11111155
}
11121156

11131157
impl TryFrom<&str> for HeaderComponent {
@@ -1122,6 +1166,7 @@ impl TryFrom<&str> for HeaderComponent {
11221166
"cookie" => Ok(Self::Cookie),
11231167
"authentication" => Ok(Self::Authentication),
11241168
"download" => Ok(Self::Download),
1169+
"log" => Ok(Self::Log),
11251170
_ => Err(()),
11261171
}
11271172
}

src/webserver/database/sql.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ use std::str::FromStr;
2525
#[derive(Default)]
2626
pub struct ParsedSqlFile {
2727
pub(super) statements: Vec<ParsedStatement>,
28-
pub(super) source_path: PathBuf,
28+
pub source_path: PathBuf,
2929
}
3030

3131
impl ParsedSqlFile {

src/webserver/http.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ use tokio::sync::mpsc;
4444
#[derive(Clone)]
4545
pub struct RequestContext {
4646
pub is_embedded: bool,
47+
pub source_path: PathBuf,
4748
pub content_security_policy: ContentSecurityPolicy,
4849
}
4950

@@ -147,6 +148,7 @@ async fn build_response_header_and_stream<S: Stream<Item = DbItem>>(
147148
Ok(ResponseWithWriter::FinishedResponse { http_response })
148149
}
149150

151+
#[allow(clippy::large_enum_variant)]
150152
enum ResponseWithWriter<S> {
151153
RenderStream {
152154
http_response: HttpResponse,
@@ -174,9 +176,11 @@ async fn render_sql(
174176
log::debug!("Received a request with the following parameters: {req_param:?}");
175177

176178
let (resp_send, resp_recv) = tokio::sync::oneshot::channel::<HttpResponse>();
179+
let source_path: PathBuf = sql_file.source_path.clone();
177180
actix_web::rt::spawn(async move {
178181
let request_context = RequestContext {
179182
is_embedded: req_param.get_variables.contains_key("_sqlpage_embed"),
183+
source_path,
180184
content_security_policy: ContentSecurityPolicy::with_random_nonce(),
181185
};
182186
let mut conn = None;
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
select 'log' as component,
2+
'Hello, World!' as message,
3+
'info' as priority;
4+
5+
select 'text' as component,
6+
'It works !' as contents;

0 commit comments

Comments
 (0)