Skip to content

Commit

Permalink
RPKG-4: add SQLite logging functionality (#1)
Browse files Browse the repository at this point in the history
* Research.

* Factored out helper function.

* Working on SQLite feature.

* Dev.

* Added support for database.

* Update vignette.

* Added SQLite support.

* Rebuilt readme.

* Removed simpleError.

* Fixed how log object is created.

* Final rebuild of readme.
  • Loading branch information
dereckmezquita authored Aug 18, 2024
1 parent d8727d2 commit 8a3a3b2
Show file tree
Hide file tree
Showing 11 changed files with 492 additions and 82 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ dev/
temp*
*.ignore.*
*html
*.sqlite

*.log

Expand Down
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Package: Logger
Type: Package
Title: Flexible and Customisable Logging System for R
Version: 0.0.21
Version: 0.0.3
URL: https://github.com/dereckmezquita/R-Logger
Authors@R:
person(given = "Dereck",
Expand Down
130 changes: 101 additions & 29 deletions R/Logger.R
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ LogLevel <- list(
)

#' @title Logger
#' @description An R6 class for flexible logging with customisable output and message formatting.
#' @description An R6 class for flexible logging with customisable output, message formatting, and context.
#'
#' @examples
#' # Create a basic logger
Expand All @@ -19,19 +19,21 @@ LogLevel <- list(
#' logger$warn("This is a warning")
#' logger$error("This is an error")
#'
#' # Create a logger with custom settings and message formatting
#' # Create a logger with custom settings, message formatting, and context
#' custom_logger <- Logger$new(
#' level = LogLevel$WARNING,
#' file_path = tempfile("log_"),
#' print_fn = function(x) message(paste0("Custom: ", x)),
#' format_fn = function(level, msg) paste0("Hello prefix: ", msg)
#' format_fn = function(level, msg) paste0("Hello prefix: ", msg),
#' context = list(program = "MyApp")
#' )
#' custom_logger$info("This won't be logged")
#' custom_logger$warn("This will be logged with a custom prefix")
#'
#' # Change log level
#' # Change log level and update context
#' custom_logger$set_level(LogLevel$INFO)
#' custom_logger$info("Now this will be logged with a custom prefix")
#' custom_logger$update_context(list(user = "John"))
#' custom_logger$info("Now this will be logged with a custom prefix and context")
#' @export
Logger <- R6::R6Class(
"Logger",
Expand All @@ -40,32 +42,37 @@ Logger <- R6::R6Class(
#' Create a new Logger object.
#' @param level The minimum log level to output. Default is LogLevel$INFO.
#' @param file_path Character; the path to a file to save log entries to. Default is NULL.
#' @param db_conn DBI connection object; an existing database connection. Default is NULL.
#' @param table_name Character; the name of the table to log to in the database. Default is "LOGS".
#' @param print_fn Function; custom print function to use for console output.
#' Should accept a single character string as input. Default uses cat with a newline.
#' @param format_fn Function; custom format function to modify the log message.
#' Should accept level and msg as inputs and return a formatted string.
#' @param context List; initial context for the logger. Default is an empty list.
#' @return A new `Logger` object.
#' @examples
#' logger <- Logger$new(
#' level = LogLevel$WARNING,
#' file_path = "log.txt",
#' print_fn = function(x) message(paste0("Custom: ", x)),
#' format_fn = function(level, msg) paste0("Hello prefix: ", msg)
#' )
initialize = function(
level = LogLevel$INFO,
file_path = NULL,
db_conn = NULL,
table_name = "LOGS",
print_fn = function(x) cat(x, "\n"),
format_fn = function(level, msg) msg
format_fn = function(level, msg) msg,
context = list()
) {
private$level <- level
private$file_path <- file_path
private$db_conn <- db_conn
private$table_name <- table_name
private$print_fn <- print_fn
private$format_fn <- format_fn
private$context <- context

if (!is.null(private$file_path)) {
private$ensure_log_file_exists()
}
if (!is.null(private$db_conn)) {
private$ensure_log_table_exists()
}
},

#' @description
Expand All @@ -78,20 +85,38 @@ Logger <- R6::R6Class(
private$level <- level
},

#' @description
#' Update the logger's context
#' @param new_context A list of new context items to add or update
update_context = function(new_context) {
private$context <- modifyList(private$context, new_context)
},

#' @description
#' Clear the logger's context
clear_context = function() {
private$context <- list()
},

#' @description
#' Get the current context
get_context = function() {
return(private$context)
},

#' @description
#' Log an error message.
#' @param msg Character; the error message to log.
#' @param data Optional; additional data to include in the log entry.
#' @param error Optional; an error object to include in the log entry.
#' @examples
#' logger <- Logger$new()
#' logger$error("An error occurred", data = list(x = 1), error = simpleError("Oops!"))
#' logger$error("An error occurred", data = list(x = 1), error = "Oops!")
error = function(msg, data = NULL, error = NULL) {
if (private$level >= LogLevel$ERROR) {
formatted_msg <- private$format_fn("ERROR", msg)
entry <- private$create_log_entry("ERROR", formatted_msg, data, error)
private$print_fn(private$format_console_output(entry))
private$log_to_file(entry)
private$log_entry(entry)
}
},

Expand All @@ -106,8 +131,7 @@ Logger <- R6::R6Class(
if (private$level >= LogLevel$WARNING) {
formatted_msg <- private$format_fn("WARNING", msg)
entry <- private$create_log_entry("WARNING", formatted_msg, data)
private$print_fn(private$format_console_output(entry))
private$log_to_file(entry)
private$log_entry(entry)
}
},

Expand All @@ -122,17 +146,19 @@ Logger <- R6::R6Class(
if (private$level >= LogLevel$INFO) {
formatted_msg <- private$format_fn("INFO", msg)
entry <- private$create_log_entry("INFO", formatted_msg, data)
private$print_fn(private$format_console_output(entry))
private$log_to_file(entry)
private$log_entry(entry)
}
}
),

private = list(
level = NULL,
file_path = NULL,
db_conn = NULL,
table_name = NULL,
print_fn = NULL,
format_fn = NULL,
context = NULL,

ensure_log_file_exists = function() {
dir <- fs::path_dir(private$file_path)
Expand All @@ -144,6 +170,22 @@ Logger <- R6::R6Class(
}
},

ensure_log_table_exists = function() {
if (!DBI::dbExistsTable(private$db_conn, private$table_name)) {
DBI::dbExecute(private$db_conn, sprintf("
CREATE TABLE %s (
id INTEGER PRIMARY KEY AUTOINCREMENT,
datetime TEXT,
level TEXT,
context TEXT,
msg TEXT,
data TEXT,
error TEXT
)
", private$table_name))
}
},

log_to_file = function(entry) {
if (!is.null(private$file_path)) {
cat(
Expand All @@ -155,18 +197,41 @@ Logger <- R6::R6Class(
}
},

log_to_db = function(entry) {
if (!is.null(private$db_conn)) {
db_entry <- entry
if (!is.null(db_entry$data)) {
db_entry$data <- jsonlite::toJSON(db_entry$data)
}
if (!is.null(db_entry$error)) {
db_entry$error <- jsonlite::toJSON(db_entry$error)
}
if (length(private$context) > 0) {
db_entry$context <- jsonlite::toJSON(private$context)
} else {
db_entry$context <- NULL
}
# Remove NULL elements from `db_entry`
db_entry <- db_entry[!sapply(db_entry, is.null)]
DBI::dbWriteTable(private$db_conn, private$table_name, as.data.frame(db_entry), append = TRUE)
}
},

log_entry = function(entry) {
private$print_fn(private$format_console_output(entry))
private$log_to_file(entry)
private$log_to_db(entry)
},

create_log_entry = function(level, msg, data = NULL, error = NULL) {
entry <- list(
datetime = format(Sys.time(), "%Y-%m-%dT%H:%M:%OS3Z"),
level = level,
msg = msg
msg = msg,
data = if (!is.null(data)) jsonlite::toJSON(data) else NULL,
error = if (!is.null(error)) jsonlite::toJSON(private$serialise_error(error)) else NULL,
context = if (length(private$context) > 0) jsonlite::toJSON(private$context) else NULL
)
if (!is.null(data)) {
entry$data <- data
}
if (!is.null(error)) {
entry$error <- private$serialise_error(error)
}
return(entry)
},

Expand Down Expand Up @@ -197,14 +262,21 @@ Logger <- R6::R6Class(
if (!is.null(entry$data)) {
output <- paste0(
output, "\n", crayon::cyan("Data:"), "\n",
jsonlite::toJSON(entry$data, auto_unbox = TRUE, pretty = TRUE)
jsonlite::toJSON(jsonlite::fromJSON(entry$data), auto_unbox = TRUE, pretty = TRUE)
)
}

if (!is.null(entry$error)) {
output <- paste0(
output, "\n", crayon::red("Error:"), "\n",
jsonlite::toJSON(entry$error, auto_unbox = TRUE, pretty = TRUE)
jsonlite::toJSON(jsonlite::fromJSON(entry$error), auto_unbox = TRUE, pretty = TRUE)
)
}

if (!is.null(entry$context)) {
output <- paste0(
output, "\n", crayon::magenta("Context:"), "\n",
jsonlite::toJSON(jsonlite::fromJSON(entry$context), auto_unbox = TRUE, pretty = TRUE)
)
}

Expand Down
36 changes: 35 additions & 1 deletion README.Rmd
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ knitr::opts_chunk$set(

Logger is a flexible and powerful logging system for R applications. It provides a `Logger` class for creating customisable loggers, as well as helper functions for debugging and error reporting.

The latest version includes support for `SQLite` database logging and context management.

## Installation

You can install Logger from [GitHub](https://github.com/) with:
Expand Down Expand Up @@ -63,7 +65,7 @@ custom_log <- Logger$new(
custom_log$info("This won't be logged")
custom_log$warn("This will be logged to console and file")
custom_log$error("This is an error message")
custom_log$error("This is an error message", error = "Some error")
```

Logs are written to the specified file as JSON objects:
Expand All @@ -72,6 +74,38 @@ Logs are written to the specified file as JSON objects:
cat(readLines(log_file), sep = "\n")
```

### Database Logging

Logger now supports logging to a SQLite database and context management so you can easily track application events. The context is useful for filtering and querying logs based on specific criteria from `SQLite`:

```{r}
box::use(RSQLite[ SQLite ])
box::use(DBI[ dbConnect, dbDisconnect, dbGetQuery ])
# Create a database connection
db <- dbConnect(SQLite(), "log.sqlite")
# Create a logger that logs to the database
db_log <- Logger$new(
context = list(app_name = "MyApp", fun = "main"),
db_conn = db,
table_name = "app_logs"
)
# Log some messages
db_log$info("This is logged to the database")
db_log$warn("This is a warning", data = list(code = 101))
db_log$error("An error occurred", error = "Division by zero")
# Example of querying the logs
query <- "SELECT * FROM app_logs WHERE level = 'ERROR'"
result <- dbGetQuery(db, query)
print(result)
# Don't forget to close the database connection when you're done
dbDisconnect(db)
```

### Helper Functions

Logger includes helper functions like `valueCoordinates` and `tableToString` to provide detailed context in log messages:
Expand Down
Loading

0 comments on commit 8a3a3b2

Please sign in to comment.