├── .Rbuildignore ├── .github ├── .gitignore └── workflows │ ├── R-CMD-check.yaml │ ├── pkgdown.yaml │ └── rhub.yaml ├── .gitignore ├── DESCRIPTION ├── NAMESPACE ├── NEWS.md ├── R ├── appenders.R ├── deprecated.R ├── layouts.R ├── level.R ├── log4r-package.R ├── logfuncs.R └── logger.R ├── README.Rmd ├── README.md ├── _pkgdown.yml ├── cran-comments.md ├── log4r.Rproj ├── man ├── appenders.Rd ├── figures │ ├── lifecycle-deprecated.svg │ ├── lifecycle-experimental.svg │ ├── lifecycle-stable.svg │ └── lifecycle-superseded.svg ├── http_appender.Rd ├── layouts.Rd ├── level.Rd ├── log4r-deprecated.Rd ├── log4r-package.Rd ├── log_at.Rd ├── logger.Rd ├── syslog_appender.Rd └── tcp_appender.Rd ├── src ├── buf.h ├── log4r.c └── logfmt.c ├── tests ├── testthat.R └── testthat │ ├── _snaps │ └── level.md │ ├── helpers-logging.R │ ├── test-acceptance.R │ ├── test-appenders.R │ ├── test-layouts.R │ └── test-level.R └── vignettes ├── .gitignore ├── logging-beyond-local-files.Rmd ├── performance.Rmd └── structured-logging.Rmd /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^README\.Rmd$ 2 | ^.*\.Rproj$ 3 | ^\.Rproj\.user$ 4 | ^\.travis\.yml$ 5 | ^appveyor\.yml$ 6 | ^Makefile$ 7 | ^cran-comments.md$ 8 | ^[.]gitignore$ 9 | ^[.]Rhistory$ 10 | ^\.github$ 11 | ^CRAN-RELEASE$ 12 | ^CRAN-SUBMISSION$ 13 | ^_pkgdown\.yml$ 14 | ^docs$ 15 | ^pkgdown$ 16 | -------------------------------------------------------------------------------- /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /.github/workflows/R-CMD-check.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | 9 | name: R-CMD-check.yaml 10 | 11 | permissions: read-all 12 | 13 | jobs: 14 | R-CMD-check: 15 | runs-on: ${{ matrix.config.os }} 16 | 17 | name: ${{ matrix.config.os }} (${{ matrix.config.r }}) 18 | 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | config: 23 | - {os: macos-latest, r: 'release'} 24 | - {os: windows-latest, r: 'release'} 25 | - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} 26 | - {os: ubuntu-latest, r: 'release'} 27 | - {os: ubuntu-latest, r: 'oldrel-1'} 28 | 29 | env: 30 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 31 | R_KEEP_PKG_SOURCE: yes 32 | 33 | steps: 34 | - uses: actions/checkout@v4 35 | 36 | - uses: r-lib/actions/setup-pandoc@v2 37 | 38 | - uses: r-lib/actions/setup-r@v2 39 | with: 40 | r-version: ${{ matrix.config.r }} 41 | http-user-agent: ${{ matrix.config.http-user-agent }} 42 | use-public-rspm: true 43 | 44 | - uses: r-lib/actions/setup-r-dependencies@v2 45 | with: 46 | extra-packages: any::rcmdcheck 47 | needs: check 48 | 49 | - uses: r-lib/actions/check-r-package@v2 50 | with: 51 | upload-snapshots: true 52 | build_args: 'c("--no-manual","--compact-vignettes=gs+qpdf")' 53 | -------------------------------------------------------------------------------- /.github/workflows/pkgdown.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | release: 9 | types: [published] 10 | workflow_dispatch: 11 | 12 | name: pkgdown.yaml 13 | 14 | permissions: read-all 15 | 16 | jobs: 17 | pkgdown: 18 | runs-on: ubuntu-latest 19 | # Only restrict concurrency for non-PR jobs 20 | concurrency: 21 | group: pkgdown-${{ github.event_name != 'pull_request' || github.run_id }} 22 | env: 23 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 24 | permissions: 25 | contents: write 26 | steps: 27 | - uses: actions/checkout@v4 28 | 29 | - uses: r-lib/actions/setup-pandoc@v2 30 | 31 | - uses: r-lib/actions/setup-r@v2 32 | with: 33 | use-public-rspm: true 34 | 35 | - uses: r-lib/actions/setup-r-dependencies@v2 36 | with: 37 | extra-packages: any::pkgdown, local::. 38 | needs: website 39 | 40 | - name: Build site 41 | run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE) 42 | shell: Rscript {0} 43 | 44 | - name: Deploy to GitHub pages 🚀 45 | if: github.event_name != 'pull_request' 46 | uses: JamesIves/github-pages-deploy-action@v4.5.0 47 | with: 48 | clean: false 49 | branch: gh-pages 50 | folder: docs 51 | -------------------------------------------------------------------------------- /.github/workflows/rhub.yaml: -------------------------------------------------------------------------------- 1 | # R-hub's generic GitHub Actions workflow file. It's canonical location is at 2 | # https://github.com/r-hub/actions/blob/v1/workflows/rhub.yaml 3 | # You can update this file to a newer version using the rhub2 package: 4 | # 5 | # rhub::rhub_setup() 6 | # 7 | # It is unlikely that you need to modify this file manually. 8 | 9 | name: R-hub 10 | run-name: "${{ github.event.inputs.id }}: ${{ github.event.inputs.name || format('Manually run by {0}', github.triggering_actor) }}" 11 | 12 | on: 13 | workflow_dispatch: 14 | inputs: 15 | config: 16 | description: 'A comma separated list of R-hub platforms to use.' 17 | type: string 18 | default: 'linux,windows,macos' 19 | name: 20 | description: 'Run name. You can leave this empty now.' 21 | type: string 22 | id: 23 | description: 'Unique ID. You can leave this empty now.' 24 | type: string 25 | 26 | jobs: 27 | 28 | setup: 29 | runs-on: ubuntu-latest 30 | outputs: 31 | containers: ${{ steps.rhub-setup.outputs.containers }} 32 | platforms: ${{ steps.rhub-setup.outputs.platforms }} 33 | 34 | steps: 35 | # NO NEED TO CHECKOUT HERE 36 | - uses: r-hub/actions/setup@v1 37 | with: 38 | config: ${{ github.event.inputs.config }} 39 | id: rhub-setup 40 | 41 | linux-containers: 42 | needs: setup 43 | if: ${{ needs.setup.outputs.containers != '[]' }} 44 | runs-on: ubuntu-latest 45 | name: ${{ matrix.config.label }} 46 | strategy: 47 | fail-fast: false 48 | matrix: 49 | config: ${{ fromJson(needs.setup.outputs.containers) }} 50 | container: 51 | image: ${{ matrix.config.container }} 52 | 53 | steps: 54 | - uses: r-hub/actions/checkout@v1 55 | - uses: r-hub/actions/platform-info@v1 56 | with: 57 | token: ${{ secrets.RHUB_TOKEN }} 58 | job-config: ${{ matrix.config.job-config }} 59 | - uses: r-hub/actions/setup-deps@v1 60 | with: 61 | token: ${{ secrets.RHUB_TOKEN }} 62 | job-config: ${{ matrix.config.job-config }} 63 | - uses: r-hub/actions/run-check@v1 64 | with: 65 | token: ${{ secrets.RHUB_TOKEN }} 66 | job-config: ${{ matrix.config.job-config }} 67 | 68 | other-platforms: 69 | needs: setup 70 | if: ${{ needs.setup.outputs.platforms != '[]' }} 71 | runs-on: ${{ matrix.config.os }} 72 | name: ${{ matrix.config.label }} 73 | strategy: 74 | fail-fast: false 75 | matrix: 76 | config: ${{ fromJson(needs.setup.outputs.platforms) }} 77 | 78 | steps: 79 | - uses: r-hub/actions/checkout@v1 80 | - uses: r-hub/actions/setup-r@v1 81 | with: 82 | job-config: ${{ matrix.config.job-config }} 83 | token: ${{ secrets.RHUB_TOKEN }} 84 | - uses: r-hub/actions/platform-info@v1 85 | with: 86 | token: ${{ secrets.RHUB_TOKEN }} 87 | job-config: ${{ matrix.config.job-config }} 88 | - uses: r-hub/actions/setup-deps@v1 89 | with: 90 | job-config: ${{ matrix.config.job-config }} 91 | token: ${{ secrets.RHUB_TOKEN }} 92 | - uses: r-hub/actions/run-check@v1 93 | with: 94 | job-config: ${{ matrix.config.job-config }} 95 | token: ${{ secrets.RHUB_TOKEN }} 96 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | log4r.Rcheck 2 | build_package.sh 3 | upload.sh 4 | .Rproj.user 5 | .Rhistory 6 | .RData 7 | src/*.o 8 | src/*.so 9 | inst/doc 10 | docs 11 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Type: Package 2 | Package: log4r 3 | Title: A Fast and Lightweight Logging System for R, Based on 'log4j' 4 | Version: 0.4.4.9000 5 | Authors@R: c( 6 | person("John Myles", "White", role = c("aut", "cph")), 7 | person("Kenton", "White", role = "ctb"), 8 | person("Kirill", "Müller", email = "krlmlr+r@mailbox.org", 9 | role = "ctb"), 10 | person("Aaron", "Jacobs", email = "atheriel@gmail.com", 11 | role = c("aut", "cre")) 12 | ) 13 | Description: The log4r package is meant to provide a fast, lightweight, 14 | object-oriented approach to logging in R based on the widely-emulated 15 | 'log4j' system and etymology. 16 | License: Artistic-2.0 17 | URL: https://github.com/johnmyleswhite/log4r, https://log4r.r-lib.org 18 | BugReports: https://github.com/johnmyleswhite/log4r/issues 19 | Imports: 20 | cli, 21 | lifecycle, 22 | rlang 23 | Suggests: 24 | futile.logger, 25 | httr, 26 | jsonlite, 27 | knitr, 28 | lgr, 29 | logger, 30 | logging, 31 | loggit, 32 | microbenchmark, 33 | rlog, 34 | rmarkdown, 35 | rsyslog, 36 | testthat (>= 3.0.0) 37 | Encoding: UTF-8 38 | LazyLoad: yes 39 | Roxygen: list(markdown = TRUE) 40 | RoxygenNote: 7.3.2 41 | VignetteBuilder: knitr 42 | Config/testthat/edition: 3 43 | Collate: 44 | 'appenders.R' 45 | 'logfuncs.R' 46 | 'deprecated.R' 47 | 'layouts.R' 48 | 'level.R' 49 | 'log4r-package.R' 50 | 'logger.R' 51 | Config/Needs/website: tidyverse/tidytemplate 52 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | S3method("level<-",logger) 4 | S3method("logfile<-",logger) 5 | S3method(as.character,loglevel) 6 | S3method(as.numeric,loglevel) 7 | S3method(level,logger) 8 | S3method(logfile,logger) 9 | S3method(print,loglevel) 10 | export("level<-") 11 | export("logfile<-") 12 | export("logformat<-") 13 | export(as.loglevel) 14 | export(available.loglevels) 15 | export(bare_log_layout) 16 | export(console_appender) 17 | export(create.logger) 18 | export(debug) 19 | export(default_log_layout) 20 | export(error) 21 | export(fatal) 22 | export(file_appender) 23 | export(http_appender) 24 | export(info) 25 | export(is.loglevel) 26 | export(json_log_layout) 27 | export(level) 28 | export(levellog) 29 | export(log_at) 30 | export(log_debug) 31 | export(log_error) 32 | export(log_fatal) 33 | export(log_info) 34 | export(log_warn) 35 | export(logfile) 36 | export(logfmt_log_layout) 37 | export(logformat) 38 | export(logger) 39 | export(loglevel) 40 | export(simple_log_layout) 41 | export(syslog_appender) 42 | export(tcp_appender) 43 | export(verbosity) 44 | export(warn) 45 | useDynLib(log4r, .registration = TRUE) 46 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # log4r 0.4.4.9000 2 | 3 | * `logfmt_log_layout()` and `json_log_layout()` now use timestamps with 4 | microsecond precision, when possible. 5 | 6 | * `debug()`, `info()`, `warn()`, `error()`, and `fatal()` are deprecated in 7 | favour of the newly-exported `log_debug()`, `log_info()`, `log_warn()`, 8 | `log_error()`, and `log_fatal()`, respectively, though for performance reasons 9 | they do not yet issue deprecation warnings when used. These older functions 10 | tended to cause namespace issues with other packages or base R itself (#29). 11 | 12 | * Much of the pre-0.3.0 logger API is now formally deprecated and issues 13 | warnings when used. This includes `create.logger()` and `logfile()`. 14 | 15 | * `is.loglevel()`, `as.loglevel()`, its alias `loglevel()`, and S3 generics for 16 | the `"loglevel"` class are now considered an implementation detail and are no 17 | longer part of the public API. They now issue deprecation warnings when used. 18 | Hopefully this makes way for adding a `TRACE` level and corresponding 19 | `log_trace()` function in the future. The obscure `verbosity()` function is 20 | also now deprecated, for similar reasons. 21 | 22 | * Errors have been migrated to `cli::cli_abort()` and are now much friendlier 23 | and actionable as a result. 24 | 25 | # log4r 0.4.4 26 | 27 | * Fixes failing unit tests for the HTTP appender. 28 | 29 | * JSON logs now have newlines, as intended (#30, @brooklynbagel). 30 | 31 | * Updates the R CMD check GitHub Action to a modern version (#27, @hadley). 32 | 33 | * Updates the project to `testthat` 3e (#26, @hadley). 34 | 35 | * Updates `roxygen2` documentation to use Markdown syntax (#25, @hadley). 36 | 37 | # log4r 0.4.3 38 | 39 | * Fixes a potential memory corruption issue identified by rchk. Thanks to Tomas 40 | Kalibera for the associated patch. 41 | 42 | # log4r 0.4.2 43 | 44 | * Fixes a crash where `logfmt_log_layout()` would not correctly handle memory 45 | reallocation of the underlying buffer. 46 | 47 | # log4r 0.4.1 48 | 49 | * Fixes a crash when the `logfmt_log_layout()` is passed long fields that also 50 | need escaping. 51 | 52 | * `logfmt_log_layout()` now drops fields with missing names rather than writing 53 | `NA`, which matches the existing handling of other empty/unrepresentable field 54 | names. 55 | 56 | # log4r 0.4.0 57 | 58 | * Support for structured logging by passing additional named parameters to the 59 | existing logging functions. This includes two new structured-logging layouts 60 | for JSON and [logfmt](https://brandur.org/logfmt) and a vignette on using 61 | them: "Structured Logging". 62 | 63 | * New built-in appenders for writing to the Unix system log, via HTTP, and to 64 | TCP connections, plus a vignette on using them: "Logging Beyond Local Files". 65 | 66 | * A new `bare_log_layout()` for when you don't want the level or timestamp 67 | handled automatically. This is most useful for the `syslog_appender()`. 68 | 69 | * Log messages prior to the last entry are no longer lost when a file appender 70 | is created with `append = FALSE`. Instead, the file is truncated only when the 71 | appender is created, as intended. Fixes #17. 72 | 73 | # log4r 0.3.2 (2020-01-17) 74 | 75 | * Fixes an issue where appender functions did not evaluate all their arguments, 76 | leading to surprising behaviour in e.g. loops. Reported by Nicola Farina. 77 | 78 | # log4r 0.3.1 (2019-09-04) 79 | 80 | * There is now a vignette on logger performance. 81 | * Fixes a missing header file on older versions of R (<= 3.4). (#12) 82 | * Fixes an issue where `default_log_layout()` would not validate format strings 83 | correctly. 84 | 85 | # log4r 0.3.0 (2019-06-20) 86 | 87 | * A new system for configuring logging, via "Appenders" and "Layouts". See 88 | `?logger`, `?appenders`, and `?layouts` for details. 89 | * Various fixes and performance improvements. 90 | * New maintainer: Aaron Jacobs (atheriel@gmail.com) 91 | 92 | # log4r 0.2 (2014-09-29) 93 | 94 | * Same as v0.1-5. 95 | 96 | # log4r 0.1-5 (2014-09-28) 97 | 98 | * New maintainer: Kirill Müller (krlmlr+r@mailbox.org) 99 | * Log levels are now objects of class `"loglevel"`, access to the hidden 100 | constants, e.g., `log4r:::DEBUG`, is deprecated (#4). 101 | -------------------------------------------------------------------------------- /R/appenders.R: -------------------------------------------------------------------------------- 1 | #' Send logs to their final destination with Appenders 2 | #' 3 | #' @description 4 | #' 5 | #' In [log4j](https://logging.apache.org/log4j/) etymology, **Appenders** are 6 | #' destinations where logs are written. Appenders have no control over 7 | #' formatting; this is controlled by the **[Layout][layouts]**. 8 | #' 9 | #' The most basic appenders write logs to the console or to a file; these are 10 | #' described below. 11 | #' 12 | #' For implementing your own appenders, see Details. 13 | #' 14 | #' @details 15 | #' 16 | #' Appenders are implemented as functions with the interface `function(level, 17 | #' ...)`. These functions are expected to write their arguments to a destination 18 | #' and return `invisible(NULL)`. 19 | #' 20 | #' @param layout A layout function taking a `level` parameter and additional 21 | #' arguments corresponding to the message. See [layouts()]. 22 | #' 23 | #' @examples 24 | #' # The behaviour of an appender can be seen by using them directly; the 25 | #' # following snippet will write the message to the console. 26 | #' appender <- console_appender() 27 | #' appender("INFO", "Input has length ", 0, ".") 28 | #' @seealso [tcp_appender()], [http_appender()], [syslog_appender()] 29 | #' @name appenders 30 | #' @rdname appenders 31 | #' @export 32 | console_appender <- function(layout = default_log_layout()) { 33 | file_appender(file = "", layout = layout) 34 | } 35 | 36 | #' @param file The file to write messages to. 37 | #' @param append When `TRUE`, the file is not truncated when opening for 38 | #' the first time. 39 | #' 40 | #' @rdname appenders 41 | #' @export 42 | file_appender <- function(file, append = TRUE, layout = default_log_layout()) { 43 | check_layout(layout) 44 | layout <- compiler::cmpfun(layout) 45 | force(file) 46 | if (!append) { 47 | # This should truncate the file, if it exists. 48 | file.create(file) 49 | } 50 | function(level, ...) { 51 | msg <- layout(level, ...) 52 | cat(msg, file = file, sep = "", append = TRUE) 53 | } 54 | } 55 | 56 | #' Send logs over TCP 57 | #' 58 | #' Append messages to arbitrary TCP destinations. 59 | #' 60 | #' @param host Hostname for the socket connection. 61 | #' @param port Port number for the socket connection. 62 | #' @param layout A layout function taking a `level` parameter and 63 | #' additional arguments corresponding to the message. 64 | #' @param timeout Timeout for the connection. 65 | #' 66 | #' @seealso [appenders] for more information on Appenders, and 67 | #' [base::socketConnection()] for the underlying connection object 68 | #' used by [tcp_appender()]. 69 | #' 70 | #' @export 71 | tcp_appender <- function(host, port, layout = default_log_layout(), 72 | timeout = getOption("timeout")) { 73 | check_layout(layout) 74 | layout <- compiler::cmpfun(layout) 75 | # Use a finalizer pattern to make sure we close the connection. 76 | env <- new.env(size = 1) 77 | env$con <- socketConnection( 78 | host = host, port = port, open = "wb", blocking = TRUE, timeout = timeout 79 | ) 80 | reg.finalizer(env, function(e) close(e$con), onexit = TRUE) 81 | function(level, ...) { 82 | msg <- layout(level, ...) 83 | writeBin(msg, con = env$con) 84 | } 85 | } 86 | 87 | #' Send logs over HTTP 88 | #' 89 | #' @description 90 | #' 91 | #' Send logs in the body of HTTP requests. Responses with status code 400 92 | #' or above will trigger errors. 93 | #' 94 | #' Requires the `httr` package. 95 | #' 96 | #' @param url The URL to submit messages to. 97 | #' @param method The HTTP method to use, usually `"POST"` or `"GET"`. 98 | #' @param layout A layout function taking a `level` parameter and 99 | #' additional arguments corresponding to the message. 100 | #' @param ... Further arguments passed on to [httr::POST()]. 101 | #' 102 | #' @examples 103 | #' \dontrun{ 104 | #' # POST messages to localhost. 105 | #' appender <- http_appender("localhost") 106 | #' appender("INFO", "Message.") 107 | #' 108 | #' # POST JSON-encoded messages. 109 | #' appender <- http_appender( 110 | #' "localhost", method = "POST", layout = default_log_layout(), 111 | #' httr::content_type_json() 112 | #' ) 113 | #' appender("INFO", "Message.") 114 | #' } 115 | #' @seealso [appenders] for more information on Appenders. 116 | #' 117 | #' @export 118 | http_appender <- function(url, method = "POST", layout = default_log_layout(), 119 | ...) { 120 | rlang::check_installed("httr", "to use this HTTP appender.") 121 | check_layout(layout) 122 | layout <- compiler::cmpfun(layout) 123 | 124 | tryCatch({ 125 | verb <- get(method, envir = asNamespace("httr")) 126 | }, error = function(e) { 127 | cli::cli_abort("{.str {method}} is not a supported HTTP method.") 128 | }) 129 | args <- c(list(url = url), list(...)) 130 | function(level, ...) { 131 | args$body <- layout(level, ...) 132 | resp <- do.call(verb, args) 133 | 134 | # Treat HTTP errors as actual errors. 135 | if (httr::status_code(resp) >= 400) { 136 | cli::cli_abort("Server responded with error {.code {resp}}.") 137 | } 138 | } 139 | } 140 | 141 | #' Send logs to the local syslog 142 | #' 143 | #' Send messages to the local syslog. Requires the `rsyslog` package. 144 | #' 145 | #' @param identifier A string identifying the application. 146 | #' @param layout A layout function taking a `level` parameter and 147 | #' additional arguments corresponding to the message. 148 | #' @param ... Further arguments passed on to [rsyslog::open_syslog()]. 149 | #' 150 | #' @seealso [appenders] for more information on Appenders. 151 | #' 152 | #' @export 153 | syslog_appender <- function(identifier, layout = bare_log_layout(), ...) { 154 | rlang::check_installed("rsyslog", "to use this syslog appender.") 155 | check_layout(layout) 156 | layout <- compiler::cmpfun(layout) 157 | 158 | rsyslog::open_syslog(identifier = identifier, ...) 159 | 160 | # Override any existing masking. Priority thresholds are handled by the 161 | # package instead of by syslog. 162 | rsyslog::set_syslog_mask("DEBUG") 163 | 164 | function(level, ...) { 165 | msg <- layout(level, ...) 166 | # Translate between log4j and syslog priority levels. Using a switch 167 | # statement turns out to be faster than a lookup. 168 | syslog_level <- switch( 169 | level, "TRACE" = "DEBUG", "DEBUG" = "DEBUG", "INFO" = "INFO", 170 | "WARN" = "WARNING", "ERROR" = "ERR", "FATAL" = "CRITICAL" 171 | ) 172 | rsyslog::syslog(msg, level = syslog_level) 173 | } 174 | } 175 | 176 | check_layout <- function(x, arg = rlang::caller_arg(x), 177 | call = rlang::caller_env()) { 178 | if (is.function(x)) { 179 | return(invisible(NULL)) 180 | } 181 | cli::cli_abort("{.arg {arg}} must be a function.", call = call) 182 | } 183 | -------------------------------------------------------------------------------- /R/deprecated.R: -------------------------------------------------------------------------------- 1 | #' @include logfuncs.R 2 | NULL 3 | 4 | #' Deprecated logger functions 5 | #' 6 | #' @description 7 | #' `r lifecycle::badge("deprecated")` 8 | #' 9 | #' * [create.logger()] and [logfile()] are deprecated in favour of [logger()]. 10 | #' They issue deprecation warnings when used. 11 | #' 12 | #' * [debug()], [info()], [warn()], [error()], and [fatal()] are deprecated in 13 | #' favour of [log_debug()], [log_info()], [log_warn()], [log_error()], and 14 | #' [log_fatal()], respectively. For performance reasons they do not yet issue 15 | #' deprecation warnings when used. 16 | #' 17 | #' * [levellog()] is deprecated in favour of [log_at()]. It issues a deprecation 18 | #' warning when used. 19 | #' 20 | #' * [logformat()] is incompatible with **[Layouts][layouts]** and has been 21 | #' nonfunctional for many years. It issues a deprecation error when used. 22 | #' 23 | #' * [is.loglevel()], [as.loglevel()], its alias [loglevel()], and S3 generics 24 | #' for the `"loglevel"` class are now considered an implementation detail and 25 | #' are no longer part of the public API. They issue deprecation warnings when 26 | #' used. 27 | #' 28 | #' * [verbosity()] is similar, in that there is no longer a stable mapping 29 | #' between priority integers and levels. It issues a deprecation warning when 30 | #' used. 31 | #' @export 32 | #' @keywords internal 33 | #' @rdname log4r-deprecated 34 | create.logger <- function(logfile = "logfile.log", level = "FATAL", 35 | logformat = NULL) 36 | { 37 | lifecycle::deprecate_warn("0.5.0", "create.logger()", "logger()") 38 | out <- logger( 39 | threshold = level, appenders = file_appender(file = logfile) 40 | ) 41 | out$logfile <- logfile 42 | out 43 | } 44 | 45 | #' @rdname log4r-deprecated 46 | #' @export 47 | logfile <- function(x) { 48 | lifecycle::deprecate_warn("0.5.0", "logfile()", "logger()") 49 | UseMethod("logfile", x) 50 | } 51 | 52 | #' @rdname log4r-deprecated 53 | #' @export 54 | `logfile<-` <- function(x, value) { 55 | lifecycle::deprecate_warn("0.5.0", "logfile()", "logger()") 56 | UseMethod("logfile<-", x) 57 | } 58 | 59 | #' @rdname log4r-deprecated 60 | #' @export 61 | logfile.logger <- function(x) x$logfile 62 | 63 | #' @rdname log4r-deprecated 64 | #' @export 65 | `logfile<-.logger` <- function(x, value) { 66 | # For loggers created with the old API, change the appender. Otherwise, do 67 | # nothing. 68 | if (!is.null(x$logfile)) { 69 | x$appenders <- list(file_appender(file = value)) 70 | x$logfile <- value 71 | } 72 | x 73 | } 74 | 75 | #' @rdname log4r-deprecated 76 | #' @export 77 | logformat <- function(x) { 78 | lifecycle::deprecate_stop( 79 | "0.3.0", 80 | "logformat()", 81 | details = c( 82 | "x" = "It is incompatible with Layouts and has been nonfunctional for \\ 83 | many years." 84 | ) 85 | ) 86 | } 87 | 88 | #' @rdname log4r-deprecated 89 | #' @export 90 | `logformat<-` <- function(x, value) { 91 | lifecycle::deprecate_stop( 92 | "0.3.0", 93 | "logformat()", 94 | details = c( 95 | "x" = "It is incompatible with Layouts and has been nonfunctional for \\ 96 | many years." 97 | ) 98 | ) 99 | } 100 | 101 | #' @rdname log4r-deprecated 102 | #' @export 103 | is.loglevel <- function(x, ...) { 104 | lifecycle::deprecate_warn( 105 | "0.5.0", 106 | "is.loglevel()", 107 | details = c( 108 | "x" = "The internals of log levels are no longer part of the public API." 109 | ) 110 | ) 111 | inherits(x, "loglevel") 112 | } 113 | 114 | #' @rdname log4r-deprecated 115 | #' @export 116 | loglevel <- function(i) { 117 | lifecycle::deprecate_warn( 118 | "0.5.0", 119 | "loglevel()", 120 | details = c( 121 | "x" = "The internals of log levels are no longer part of the public API." 122 | ) 123 | ) 124 | as_level(i) 125 | } 126 | 127 | #' @rdname log4r-deprecated 128 | #' @export 129 | as.loglevel <- function(i) { 130 | lifecycle::deprecate_warn( 131 | "0.5.0", 132 | "as.loglevel()", 133 | details = c( 134 | "x" = "The internals of log levels are no longer part of the public API." 135 | ) 136 | ) 137 | as_level(i) 138 | } 139 | 140 | #' @rdname log4r-deprecated 141 | #' @export 142 | print.loglevel <- function(x, ...) { 143 | cat(LEVEL_NAMES[[x]], "\n") 144 | } 145 | 146 | #' @rdname log4r-deprecated 147 | #' @export 148 | as.numeric.loglevel <- function(x, ...) { 149 | lifecycle::deprecate_warn( 150 | "0.5.0", 151 | "as.numeric.loglevel()", 152 | details = c( 153 | "x" = "The internals of log levels are no longer part of the public API." 154 | ) 155 | ) 156 | unclass(unname(x)) 157 | } 158 | 159 | #' @rdname log4r-deprecated 160 | #' @export 161 | as.character.loglevel <- function(x, ...) { 162 | lifecycle::deprecate_warn( 163 | "0.5.0", 164 | "as.character.loglevel()", 165 | details = c( 166 | "x" = "The internals of log levels are no longer part of the public API." 167 | ) 168 | ) 169 | LEVEL_NAMES[[x]] 170 | } 171 | 172 | #' @rdname log4r-deprecated 173 | #' @export 174 | verbosity <- function(v) { 175 | if (!is.numeric(v)) { 176 | stop("numeric expected") 177 | } 178 | lifecycle::deprecate_warn( 179 | "0.5.0", 180 | "verbosity()", 181 | details = c( 182 | "x" = "The internals of log levels are no longer part of the public API." 183 | ) 184 | ) 185 | as_level(length(LEVELS) + 1 - v) 186 | } 187 | 188 | #' @rdname log4r-deprecated 189 | #' @export 190 | levellog <- log_at 191 | 192 | #' @rdname log4r-deprecated 193 | #' @export 194 | debug <- log_debug 195 | 196 | #' @rdname log4r-deprecated 197 | #' @export 198 | info <- log_info 199 | 200 | #' @rdname log4r-deprecated 201 | #' @export 202 | warn <- log_warn 203 | 204 | #' @rdname log4r-deprecated 205 | #' @export 206 | error <- log_error 207 | 208 | #' @rdname log4r-deprecated 209 | #' @export 210 | fatal <- log_fatal 211 | -------------------------------------------------------------------------------- /R/layouts.R: -------------------------------------------------------------------------------- 1 | #' Format logs with Layouts 2 | #' 3 | #' @description 4 | #' 5 | #' In [log4j](https://logging.apache.org/log4j/) etymology, **Layouts** are how 6 | #' **[Appenders][appenders]** control the format of messages. Most users will 7 | #' use one of the general-purpose layouts provided by the package: 8 | #' 9 | #' * [default_log_layout()] formats messages much like the original log4j 10 | #' library. [simple_log_layout()] does the same, but omits the timestamp. 11 | #' 12 | #' * [bare_log_layout()] emits only the log message, with no level or timestamp 13 | #' fields. 14 | #' 15 | #' * [logfmt_log_layout()] and [json_log_layout()] format structured logs in the 16 | #' two most popular machine-readable formats. 17 | #' 18 | #' For implementing your own layouts, see Details. 19 | #' 20 | #' @details 21 | #' 22 | #' Layouts return a function with the signature `function(level, ...)` that 23 | #' itself returns a single newline-terminated string. Anything that meets this 24 | #' interface can be passed as a layout to one of the existing [appenders]. 25 | #' 26 | #' @param time_format A valid format string for timestamps. See 27 | #' [base::strptime()]. 28 | #' 29 | #' @examples 30 | #' # The behaviour of a layout can be seen by using them directly: 31 | #' simple <- simple_log_layout() 32 | #' simple("INFO", "Input has length ", 0, ".") 33 | #' 34 | #' with_timestamp <- default_log_layout() 35 | #' with_timestamp("INFO", "Input has length ", 0, ".") 36 | #' 37 | #' logfmt <- logfmt_log_layout() 38 | #' logfmt("INFO", msg = "got input", length = 24) 39 | #' @name layouts 40 | #' @rdname layouts 41 | #' @export 42 | default_log_layout <- function(time_format = "%Y-%m-%d %H:%M:%S") { 43 | check_time_format(time_format) 44 | timestamp <- fmt_current_time(time_format, FALSE) 45 | 46 | function(level, ...) { 47 | msg <- paste0(..., collapse = "") 48 | sprintf("%-5s [%s] %s\n", level, timestamp(), msg) 49 | } 50 | } 51 | 52 | #' @rdname layouts 53 | #' @aliases simple_log_layout 54 | #' @export 55 | simple_log_layout <- function() { 56 | function(level, ...) { 57 | msg <- paste0(..., collapse = "") 58 | sprintf("%-5s - %s\n", level, msg) 59 | } 60 | } 61 | 62 | #' @rdname layouts 63 | #' @aliases bare_log_layout 64 | #' @export 65 | bare_log_layout <- function() { 66 | function(level, ...) { 67 | msg <- paste0(..., collapse = "") 68 | sprintf("%s\n", msg) 69 | } 70 | } 71 | 72 | #' @rdname layouts 73 | #' @aliases logfmt_log_layout 74 | #' @export 75 | logfmt_log_layout <- function() { 76 | timestamp <- fmt_current_time("%Y-%m-%dT%H:%M:%OSZ", TRUE) 77 | 78 | function(level, ...) { 79 | fields <- list(...) 80 | if (is.null(names(fields))) { 81 | fields <- list(msg = paste0(fields, collapse = "")) 82 | } 83 | extra <- list(level = level, ts = timestamp()) 84 | encode_logfmt(c(extra, fields)) 85 | } 86 | } 87 | 88 | #' @details `json_log_layout` requires the `jsonlite` package. 89 | #' 90 | #' @rdname layouts 91 | #' @aliases json_log_layout 92 | #' @export 93 | json_log_layout <- function() { 94 | rlang::check_installed("jsonlite", "to use this JSON layout.") 95 | timestamp <- fmt_current_time("%Y-%m-%dT%H:%M:%OSZ", TRUE) 96 | 97 | function(level, ...) { 98 | fields <- list(...) 99 | if (is.null(names(fields))) { 100 | fields <- list(message = paste0(fields, collapse = "")) 101 | } 102 | fields$level <- as.character(level) 103 | fields$time <- timestamp() 104 | sprintf("%s\n", jsonlite::toJSON(fields, auto_unbox = TRUE)) 105 | } 106 | } 107 | 108 | # Given a strftime format string, return a fast function that outputs the 109 | # current time in that format. This is about 1000x faster than using 110 | # format(Sys.now()). 111 | fmt_current_time <- function(format, use_utc = FALSE) { 112 | if (!grepl("%OS", format, fixed = TRUE)) { 113 | return(compiler::cmpfun(function() { 114 | .Call(R_fmt_current_time, format, use_utc, FALSE, NULL, PACKAGE = "log4r") 115 | })) 116 | } 117 | # If we need fractional seconds, break formatting into three pieces: (1) the 118 | # bit before %OS; (2) %S plus fractional seconds; and (3) the bit after %OS, 119 | # if any. 120 | split <- strsplit(format, "%OS[0-9]?")[[1]] 121 | prefix <- paste0(split[1], "%S") 122 | suffix <- NULL 123 | if (length(split) > 1) { 124 | suffix <- split[2] 125 | } 126 | compiler::cmpfun(function() { 127 | .Call(R_fmt_current_time, prefix, use_utc, TRUE, suffix, PACKAGE = "log4r") 128 | }) 129 | } 130 | 131 | check_time_format <- function(x, arg = rlang::caller_arg(x), 132 | call = rlang::caller_env()) { 133 | if (is.character(x)) { 134 | tryCatch({ 135 | fmt_current_time(x)() 136 | return(invisible(NULL)) 137 | }, error = function(e) {}) 138 | } 139 | cli::cli_abort( 140 | c( 141 | "{.arg {arg}} must be a valid timestamp format string.", 142 | "i" = "See {.help strptime} for details on available formats." 143 | ), 144 | call = call 145 | ) 146 | } 147 | 148 | encode_logfmt <- function(fields) { 149 | .Call(R_encode_logfmt, fields, PACKAGE = "log4r") 150 | } 151 | -------------------------------------------------------------------------------- /R/level.R: -------------------------------------------------------------------------------- 1 | #' Set the logging threshold level for a logger dynamically 2 | #' 3 | #' It can sometimes be useful to change the logging threshold level at runtime. 4 | #' The [`level()`] accessor allows doing so. 5 | #' 6 | #' @param x An object of class `"logger"`. 7 | #' @param value One of `"DEBUG"`, `"INFO"`, `"WARN"`, `"ERROR"`, or `"FATAL"`. 8 | #' 9 | #' @examples 10 | #' lgr <- logger() 11 | #' level(lgr) # Prints "INFO". 12 | #' info(lgr, "This message is shown.") 13 | #' level(lgr) <- "FATAL" 14 | #' info(lgr, "This message is now suppressed.") 15 | #' @export 16 | level <- function(x) { 17 | UseMethod("level", x) 18 | } 19 | 20 | #' @rdname level 21 | #' @export 22 | `level<-` <- function(x, value) { 23 | UseMethod("level<-", x) 24 | } 25 | 26 | #' @rdname level 27 | #' @export 28 | level.logger <- function(x) x$threshold 29 | 30 | #' @rdname level 31 | #' @export 32 | `level<-.logger` <- function(x, value) { 33 | x$threshold <- as_level(value) 34 | x 35 | } 36 | 37 | # Converts strings like "INFO" or "ERROR" to the internal level representation. 38 | # For historical reasons we also accept integers. 39 | as_level <- function(i) { 40 | if (inherits(i, "loglevel")) { 41 | return(i) 42 | } 43 | 44 | idx <- NULL 45 | if (length(i) == 1 && is.numeric(i)) { 46 | idx <- max(min(i, length(LEVELS)), 1) 47 | } else if (length(i) == 1 && is.character(i)) { 48 | idx <- which(i == levels(LEVELS)) 49 | } 50 | 51 | if (length(idx) == 0) { 52 | arg <- rlang::caller_arg(i) 53 | levels <- cli::cli_vec(LEVEL_NAMES, style = list("vec-last" = ", or ")) 54 | cli::cli_abort( 55 | "{.arg {arg}} must be one of {.str {levels}}.", 56 | call = rlang::caller_env() 57 | ) 58 | } 59 | 60 | structure(LEVELS[idx], class = "loglevel") 61 | } 62 | 63 | # Internal constants. 64 | LEVEL_NAMES <- c("DEBUG", "INFO", "WARN", "ERROR", "FATAL") 65 | LEVELS <- factor(LEVEL_NAMES, levels = LEVEL_NAMES, ordered = TRUE) 66 | DEBUG <- as_level("DEBUG") 67 | INFO <- as_level("INFO") 68 | WARN <- as_level("WARN") 69 | ERROR <- as_level("ERROR") 70 | FATAL <- as_level("FATAL") 71 | 72 | #' @rdname level 73 | #' @export 74 | available.loglevels <- function() lapply(stats::setNames(nm = LEVEL_NAMES), as_level) 75 | -------------------------------------------------------------------------------- /R/log4r-package.R: -------------------------------------------------------------------------------- 1 | #' @keywords internal 2 | #' @useDynLib log4r, .registration = TRUE 3 | "_PACKAGE" 4 | -------------------------------------------------------------------------------- /R/logfuncs.R: -------------------------------------------------------------------------------- 1 | #' Write logs at a given level 2 | #' 3 | #' @param logger An object of class `"logger"`. 4 | #' @param level The desired severity, one of `"DEBUG"`, `"INFO"`, `"WARN"`, 5 | #' `"ERROR"`, or `"FATAL"`. Messages with a lower severity than the logger 6 | #' threshold will be discarded. 7 | #' @param ... One or more items to log. 8 | #' @examples 9 | #' logger <- logger() 10 | #' 11 | #' log_at(logger, "WARN", "First warning from our code") 12 | #' log_debug(logger, "Debugging our code") 13 | #' log_info(logger, "Information about our code") 14 | #' log_warn(logger, "Another warning from our code") 15 | #' log_error(logger, "An error from our code") 16 | #' log_fatal(logger, "I'm outta here") 17 | #' @export 18 | log_at <- function(logger, level, ...) { 19 | level <- as_level(level) 20 | if (logger$threshold > level) return(invisible(NULL)) 21 | for (appender in logger$appenders) { 22 | appender(LEVEL_NAMES[level], ...) 23 | } 24 | } 25 | 26 | #' @rdname log_at 27 | #' @export 28 | log_debug <- function(logger, ...) { 29 | if (logger$threshold > DEBUG) return(invisible(NULL)) 30 | for (appender in logger$appenders) { 31 | appender("DEBUG", ...) 32 | } 33 | } 34 | 35 | #' @rdname log_at 36 | #' @export 37 | log_info <- function(logger, ...) { 38 | if (logger$threshold > INFO) return(invisible(NULL)) 39 | for (appender in logger$appenders) { 40 | appender("INFO", ...) 41 | } 42 | } 43 | 44 | #' @rdname log_at 45 | #' @export 46 | log_warn <- function(logger, ...) { 47 | if (logger$threshold > WARN) return(invisible(NULL)) 48 | for (appender in logger$appenders) { 49 | appender("WARN", ...) 50 | } 51 | } 52 | 53 | #' @rdname log_at 54 | #' @export 55 | log_error <- function(logger, ...) { 56 | if (logger$threshold > ERROR) return(invisible(NULL)) 57 | for (appender in logger$appenders) { 58 | appender("ERROR", ...) 59 | } 60 | } 61 | 62 | #' @rdname log_at 63 | #' @export 64 | log_fatal <- function(logger, ...) { 65 | # NOTE: It should not be possible to have a higher threshold, so don't check. 66 | for (appender in logger$appenders) { 67 | appender("FATAL", ...) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /R/logger.R: -------------------------------------------------------------------------------- 1 | #' Create a logger 2 | #' 3 | #' This is the main interface for configuring logging behaviour. We adopt the 4 | #' well-known [log4j](https://logging.apache.org/log4j/) etymology: 5 | #' **[Appenders][appenders]** are destinations (e.g. the console or a file) 6 | #' where logs are written, and the **[Layout][layouts]** is the format of 7 | #' these logs. 8 | #' 9 | #' @param threshold The logging threshold, one of `"DEBUG"`, `"INFO"`, `"WARN"`, 10 | #' `"ERROR"`, or `"FATAL"`. Logs with a lower severity than the threshold 11 | #' will be discarded. 12 | #' @param appenders The logging appenders; both single appenders and a `list()` 13 | #' of them are supported. See **[Appenders][appenders]**. 14 | #' 15 | #' @return An object of class `"logger"`. 16 | #' 17 | #' @examples 18 | #' # By default, logs are written to the console at the "INFO" threshold. 19 | #' logger <- logger() 20 | #' 21 | #' log_info(logger, "Located nearest gas station.") 22 | #' log_warn(logger, "Ez-Gas sensor network is not available.") 23 | #' log_debug(logger, "Debug messages are suppressed by default.") 24 | #' @seealso 25 | #' 26 | #' **[Appenders][appenders]** and **[Layouts][layouts]** for information on 27 | #' controlling the behaviour of the logger object. 28 | #' 29 | #' @export 30 | logger <- function(threshold = "INFO", appenders = console_appender()) { 31 | threshold <- as_level(threshold) 32 | if (!is.list(appenders)) { 33 | appenders <- list(appenders) 34 | } 35 | if (!all(vapply(appenders, is.function, logical(1)))) { 36 | cli::cli_abort("{.arg appenders} must be a function or list of functions.") 37 | } 38 | appenders <- lapply(appenders, compiler::cmpfun) 39 | structure( 40 | list(threshold = threshold, appenders = appenders), 41 | class = "logger" 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /README.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: github_document 3 | --- 4 | 5 | 6 | 7 | ```{r setup, include = FALSE, warning = FALSE} 8 | knitr::opts_chunk$set( 9 | collapse = TRUE, 10 | comment = "#>", 11 | fig.path = "man/figures/README-", 12 | out.width = "100%" 13 | ) 14 | 15 | library(log4r) 16 | ``` 17 | 18 | # log4r 19 | 20 | 21 | [![CRAN status](https://www.r-pkg.org/badges/version/log4r)](https://cran.r-project.org/package=log4r) 22 | [![R-CMD-check](https://github.com/johnmyleswhite/log4r/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/johnmyleswhite/log4r/actions/workflows/R-CMD-check.yaml) 23 | 24 | 25 | **log4r** is a fast, lightweight, object-oriented approach to logging in R based 26 | on the widely-emulated [Apache Log4j](https://logging.apache.org/log4j/) project. 27 | 28 | **log4r** differs from other R logging packages in its focus on performance and 29 | simplicity. As such, it has fewer features -- although it is still quite 30 | extensible, as seen below -- but is much faster. See 31 | `vignette("performance", package = "log4r")` for details. 32 | 33 | Unlike other R logging packages, **log4r** also has first-class support for 34 | structured logging. See `vignette("structured-logging", package = "log4r")` for 35 | details. 36 | 37 | ## Installation 38 | 39 | The package is available from CRAN: 40 | 41 | ```r 42 | install.packages("log4r") 43 | ``` 44 | 45 | If you want to use the development version, you can install the package from 46 | GitHub as follows: 47 | 48 | ```r 49 | # install.packages("remotes") 50 | remotes::install_github("johnmyleswhite/log4r") 51 | ``` 52 | 53 | ## Usage 54 | 55 | Logging is configured by passing around `logger` objects created by `logger()`. 56 | By default, this will log to the console and suppress messages below the 57 | `"INFO"` level: 58 | 59 | ```{r basic-example} 60 | logger <- logger() 61 | 62 | log_info(logger, "Located nearest gas station.") 63 | log_warn(logger, "Ez-Gas sensor network is not available.") 64 | log_debug(logger, "Debug messages are suppressed by default.") 65 | ``` 66 | 67 | Logging destinations are controlled by **Appenders**, a few of which are 68 | provided by the package. For instance, if we want to debug-level messages to a 69 | file: 70 | 71 | ```{r file-example, echo = 1:7} 72 | log_file <- tempfile() 73 | logger <- logger("DEBUG", appenders = file_appender(log_file)) 74 | 75 | log_info(logger, "Messages are now written to the file instead.") 76 | log_debug(logger, "Debug messages are now visible.") 77 | 78 | readLines(log_file) 79 | unlink(log_file) 80 | ``` 81 | 82 | The `appenders` parameter takes a list, so you can log to multiple destinations 83 | transparently. 84 | 85 | For local development or simple batch R scripts run manually, writing log 86 | messages to a file for later inspection is convenient. However, for deployed R 87 | applications or automated scripts it is more likely you will need to send logs 88 | to a central location; see 89 | `vignette("logging-beyond-local-files", package = "log4r")`. 90 | 91 | To control the format of the messages you can change the **Layout** used by 92 | each appender. Layouts are functions; you can write your own quite easily: 93 | 94 | ```{r layout-example} 95 | my_layout <- function(level, ...) { 96 | paste0(format(Sys.time()), " [", level, "] ", ..., collapse = "") 97 | } 98 | 99 | logger <- logger(appenders = console_appender(my_layout)) 100 | log_info(logger, "Messages should now look a little different.") 101 | ``` 102 | 103 | With an appropriate layout, you can also use *structured logging*, enriching log 104 | messages with contextual fields: 105 | 106 | ```{r sl-example} 107 | logger <- logger(appenders = console_appender(logfmt_log_layout())) 108 | log_info( 109 | logger, message = "processed entries", file = "catpics_01.csv", 110 | entries = 4124, elapsed = 2.311 111 | ) 112 | ``` 113 | 114 | ## Older APIs 115 | 116 | The 0.2 API is still supported, but will issue deprecation warnings when used: 117 | 118 | ```{r old-api, echo = 1:12} 119 | logger <- create.logger() 120 | 121 | logfile(logger) <- log_file 122 | level(logger) <- "INFO" 123 | 124 | debug(logger, 'A Debugging Message') 125 | info(logger, 'An Info Message') 126 | warn(logger, 'A Warning Message') 127 | error(logger, 'An Error Message') 128 | fatal(logger, 'A Fatal Error Message') 129 | 130 | readLines(log_file) 131 | unlink(log_file) 132 | ``` 133 | 134 | ## License 135 | 136 | The package is available under the terms of the Artistic License 2.0. 137 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # log4r 5 | 6 | 7 | 8 | [![CRAN 9 | status](https://www.r-pkg.org/badges/version/log4r)](https://cran.r-project.org/package=log4r) 10 | [![R-CMD-check](https://github.com/johnmyleswhite/log4r/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/johnmyleswhite/log4r/actions/workflows/R-CMD-check.yaml) 11 | 12 | 13 | **log4r** is a fast, lightweight, object-oriented approach to logging in 14 | R based on the widely-emulated [Apache 15 | Log4j](https://logging.apache.org/log4j/) project. 16 | 17 | **log4r** differs from other R logging packages in its focus on 18 | performance and simplicity. As such, it has fewer features – although it 19 | is still quite extensible, as seen below – but is much faster. See 20 | `vignette("performance", package = "log4r")` for details. 21 | 22 | Unlike other R logging packages, **log4r** also has first-class support 23 | for structured logging. See 24 | `vignette("structured-logging", package = "log4r")` for details. 25 | 26 | ## Installation 27 | 28 | The package is available from CRAN: 29 | 30 | ``` r 31 | install.packages("log4r") 32 | ``` 33 | 34 | If you want to use the development version, you can install the package 35 | from GitHub as follows: 36 | 37 | ``` r 38 | # install.packages("remotes") 39 | remotes::install_github("johnmyleswhite/log4r") 40 | ``` 41 | 42 | ## Usage 43 | 44 | Logging is configured by passing around `logger` objects created by 45 | `logger()`. By default, this will log to the console and suppress 46 | messages below the `"INFO"` level: 47 | 48 | ``` r 49 | logger <- logger() 50 | 51 | log_info(logger, "Located nearest gas station.") 52 | #> INFO [2019-09-04 16:31:04] Located nearest gas station. 53 | log_warn(logger, "Ez-Gas sensor network is not available.") 54 | #> WARN [2019-09-04 16:31:04] Ez-Gas sensor network is not available. 55 | log_debug(logger, "Debug messages are suppressed by default.") 56 | ``` 57 | 58 | Logging destinations are controlled by **Appenders**, a few of which are 59 | provided by the package. For instance, if we want to debug-level 60 | messages to a file: 61 | 62 | ``` r 63 | log_file <- tempfile() 64 | logger <- logger("DEBUG", appenders = file_appender(log_file)) 65 | 66 | log_info(logger, "Messages are now written to the file instead.") 67 | log_debug(logger, "Debug messages are now visible.") 68 | 69 | readLines(log_file) 70 | #> [1] "INFO [2019-09-04 16:31:04] Messages are now written to the file instead." 71 | #> [2] "DEBUG [2019-09-04 16:31:04] Debug messages are now visible." 72 | ``` 73 | 74 | The `appenders` parameter takes a list, so you can log to multiple 75 | destinations transparently. 76 | 77 | For local development or simple batch R scripts run manually, writing 78 | log messages to a file for later inspection is convenient. However, for 79 | deployed R applications or automated scripts it is more likely you will 80 | need to send logs to a central location; see 81 | `vignette("logging-beyond-local-files", package = "log4r")`. 82 | 83 | To control the format of the messages you can change the **Layout** used 84 | by each appender. Layouts are functions; you can write your own quite 85 | easily: 86 | 87 | ``` r 88 | my_layout <- function(level, ...) { 89 | paste0(format(Sys.time()), " [", level, "] ", ..., collapse = "") 90 | } 91 | 92 | logger <- logger(appenders = console_appender(my_layout)) 93 | log_info(logger, "Messages should now look a little different.") 94 | #> 2019-09-04 16:31:04 [INFO] Messages should now look a little different. 95 | ``` 96 | 97 | With an appropriate layout, you can also use *structured logging*, 98 | enriching log messages with contextual fields: 99 | 100 | ``` r 101 | logger <- logger(appenders = console_appender(logfmt_log_layout())) 102 | log_info( 103 | logger, message = "processed entries", file = "catpics_01.csv", 104 | entries = 4124, elapsed = 2.311 105 | ) 106 | #> level=INFO ts=2021-10-22T20:19:21Z message="processed entries" file=catpics_01.csv entries=4124 elapsed=2.311 107 | ``` 108 | 109 | ## Older APIs 110 | 111 | The 0.2 API is still supported, but will issue deprecation warnings when 112 | used: 113 | 114 | ``` r 115 | logger <- create.logger() 116 | 117 | logfile(logger) <- log_file 118 | level(logger) <- "INFO" 119 | 120 | debug(logger, 'A Debugging Message') 121 | info(logger, 'An Info Message') 122 | warn(logger, 'A Warning Message') 123 | error(logger, 'An Error Message') 124 | fatal(logger, 'A Fatal Error Message') 125 | 126 | readLines(log_file) 127 | #> [1] "INFO [2019-09-04 16:31:05] An Info Message" 128 | #> [2] "WARN [2019-09-04 16:31:05] A Warning Message" 129 | #> [3] "ERROR [2019-09-04 16:31:05] An Error Message" 130 | #> [4] "FATAL [2019-09-04 16:31:05] A Fatal Error Message" 131 | ``` 132 | 133 | ## License 134 | 135 | The package is available under the terms of the Artistic License 2.0. 136 | -------------------------------------------------------------------------------- /_pkgdown.yml: -------------------------------------------------------------------------------- 1 | url: https://log4r.r-lib.org 2 | template: 3 | package: tidytemplate 4 | bootstrap: 5 5 | includes: 6 | in_header: | 7 | 8 | development: 9 | mode: auto 10 | reference: 11 | - title: Basics 12 | contents: 13 | - logger 14 | - log_at 15 | - title: Appenders 16 | contents: 17 | - appenders 18 | - syslog_appender 19 | - http_appender 20 | - tcp_appender 21 | - title: Layouts 22 | contents: layouts 23 | - title: Advanced 24 | contents: level 25 | -------------------------------------------------------------------------------- /cran-comments.md: -------------------------------------------------------------------------------- 1 | ## Release Summary 2 | 3 | This is a bugfix release fixing flaky unit tests on CRAN. 4 | 5 | ## R CMD check Results 6 | 7 | 0 errors | 0 warnings | 0 notes 8 | -------------------------------------------------------------------------------- /log4r.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: No 4 | SaveWorkspace: No 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: knitr 13 | LaTeX: pdfLaTeX 14 | 15 | AutoAppendNewline: Yes 16 | StripTrailingWhitespace: Yes 17 | 18 | BuildType: Package 19 | PackageUseDevtools: Yes 20 | PackageInstallArgs: --no-multiarch --with-keep.source 21 | PackageRoxygenize: rd,collate,namespace 22 | -------------------------------------------------------------------------------- /man/appenders.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/appenders.R 3 | \name{appenders} 4 | \alias{appenders} 5 | \alias{console_appender} 6 | \alias{file_appender} 7 | \title{Send logs to their final destination with Appenders} 8 | \usage{ 9 | console_appender(layout = default_log_layout()) 10 | 11 | file_appender(file, append = TRUE, layout = default_log_layout()) 12 | } 13 | \arguments{ 14 | \item{layout}{A layout function taking a \code{level} parameter and additional 15 | arguments corresponding to the message. See \code{\link[=layouts]{layouts()}}.} 16 | 17 | \item{file}{The file to write messages to.} 18 | 19 | \item{append}{When \code{TRUE}, the file is not truncated when opening for 20 | the first time.} 21 | } 22 | \description{ 23 | In \href{https://logging.apache.org/log4j/}{log4j} etymology, \strong{Appenders} are 24 | destinations where logs are written. Appenders have no control over 25 | formatting; this is controlled by the \strong{\link[=layouts]{Layout}}. 26 | 27 | The most basic appenders write logs to the console or to a file; these are 28 | described below. 29 | 30 | For implementing your own appenders, see Details. 31 | } 32 | \details{ 33 | Appenders are implemented as functions with the interface \verb{function(level, ...)}. These functions are expected to write their arguments to a destination 34 | and return \code{invisible(NULL)}. 35 | } 36 | \examples{ 37 | # The behaviour of an appender can be seen by using them directly; the 38 | # following snippet will write the message to the console. 39 | appender <- console_appender() 40 | appender("INFO", "Input has length ", 0, ".") 41 | } 42 | \seealso{ 43 | \code{\link[=tcp_appender]{tcp_appender()}}, \code{\link[=http_appender]{http_appender()}}, \code{\link[=syslog_appender]{syslog_appender()}} 44 | } 45 | -------------------------------------------------------------------------------- /man/figures/lifecycle-deprecated.svg: -------------------------------------------------------------------------------- 1 | 2 | lifecycle: deprecated 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | lifecycle 18 | 19 | deprecated 20 | 21 | 22 | -------------------------------------------------------------------------------- /man/figures/lifecycle-experimental.svg: -------------------------------------------------------------------------------- 1 | 2 | lifecycle: experimental 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | lifecycle 18 | 19 | experimental 20 | 21 | 22 | -------------------------------------------------------------------------------- /man/figures/lifecycle-stable.svg: -------------------------------------------------------------------------------- 1 | 2 | lifecycle: stable 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | lifecycle 21 | 22 | 25 | 26 | stable 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /man/figures/lifecycle-superseded.svg: -------------------------------------------------------------------------------- 1 | 2 | lifecycle: superseded 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | lifecycle 18 | 19 | superseded 20 | 21 | 22 | -------------------------------------------------------------------------------- /man/http_appender.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/appenders.R 3 | \name{http_appender} 4 | \alias{http_appender} 5 | \title{Send logs over HTTP} 6 | \usage{ 7 | http_appender(url, method = "POST", layout = default_log_layout(), ...) 8 | } 9 | \arguments{ 10 | \item{url}{The URL to submit messages to.} 11 | 12 | \item{method}{The HTTP method to use, usually \code{"POST"} or \code{"GET"}.} 13 | 14 | \item{layout}{A layout function taking a \code{level} parameter and 15 | additional arguments corresponding to the message.} 16 | 17 | \item{...}{Further arguments passed on to \code{\link[httr:POST]{httr::POST()}}.} 18 | } 19 | \description{ 20 | Send logs in the body of HTTP requests. Responses with status code 400 21 | or above will trigger errors. 22 | 23 | Requires the \code{httr} package. 24 | } 25 | \examples{ 26 | \dontrun{ 27 | # POST messages to localhost. 28 | appender <- http_appender("localhost") 29 | appender("INFO", "Message.") 30 | 31 | # POST JSON-encoded messages. 32 | appender <- http_appender( 33 | "localhost", method = "POST", layout = default_log_layout(), 34 | httr::content_type_json() 35 | ) 36 | appender("INFO", "Message.") 37 | } 38 | } 39 | \seealso{ 40 | \link{appenders} for more information on Appenders. 41 | } 42 | -------------------------------------------------------------------------------- /man/layouts.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/layouts.R 3 | \name{layouts} 4 | \alias{layouts} 5 | \alias{default_log_layout} 6 | \alias{simple_log_layout} 7 | \alias{bare_log_layout} 8 | \alias{logfmt_log_layout} 9 | \alias{json_log_layout} 10 | \title{Format logs with Layouts} 11 | \usage{ 12 | default_log_layout(time_format = "\%Y-\%m-\%d \%H:\%M:\%S") 13 | 14 | simple_log_layout() 15 | 16 | bare_log_layout() 17 | 18 | logfmt_log_layout() 19 | 20 | json_log_layout() 21 | } 22 | \arguments{ 23 | \item{time_format}{A valid format string for timestamps. See 24 | \code{\link[base:strptime]{base::strptime()}}.} 25 | } 26 | \description{ 27 | In \href{https://logging.apache.org/log4j/}{log4j} etymology, \strong{Layouts} are how 28 | \strong{\link[=appenders]{Appenders}} control the format of messages. Most users will 29 | use one of the general-purpose layouts provided by the package: 30 | \itemize{ 31 | \item \code{\link[=default_log_layout]{default_log_layout()}} formats messages much like the original log4j 32 | library. \code{\link[=simple_log_layout]{simple_log_layout()}} does the same, but omits the timestamp. 33 | \item \code{\link[=bare_log_layout]{bare_log_layout()}} emits only the log message, with no level or timestamp 34 | fields. 35 | \item \code{\link[=logfmt_log_layout]{logfmt_log_layout()}} and \code{\link[=json_log_layout]{json_log_layout()}} format structured logs in the 36 | two most popular machine-readable formats. 37 | } 38 | 39 | For implementing your own layouts, see Details. 40 | } 41 | \details{ 42 | Layouts return a function with the signature \verb{function(level, ...)} that 43 | itself returns a single newline-terminated string. Anything that meets this 44 | interface can be passed as a layout to one of the existing \link{appenders}. 45 | 46 | \code{json_log_layout} requires the \code{jsonlite} package. 47 | } 48 | \examples{ 49 | # The behaviour of a layout can be seen by using them directly: 50 | simple <- simple_log_layout() 51 | simple("INFO", "Input has length ", 0, ".") 52 | 53 | with_timestamp <- default_log_layout() 54 | with_timestamp("INFO", "Input has length ", 0, ".") 55 | 56 | logfmt <- logfmt_log_layout() 57 | logfmt("INFO", msg = "got input", length = 24) 58 | } 59 | -------------------------------------------------------------------------------- /man/level.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/level.R 3 | \name{level} 4 | \alias{level} 5 | \alias{level<-} 6 | \alias{level.logger} 7 | \alias{level<-.logger} 8 | \alias{available.loglevels} 9 | \title{Set the logging threshold level for a logger dynamically} 10 | \usage{ 11 | level(x) 12 | 13 | level(x) <- value 14 | 15 | \method{level}{logger}(x) 16 | 17 | \method{level}{logger}(x) <- value 18 | 19 | available.loglevels() 20 | } 21 | \arguments{ 22 | \item{x}{An object of class \code{"logger"}.} 23 | 24 | \item{value}{One of \code{"DEBUG"}, \code{"INFO"}, \code{"WARN"}, \code{"ERROR"}, or \code{"FATAL"}.} 25 | } 26 | \description{ 27 | It can sometimes be useful to change the logging threshold level at runtime. 28 | The \code{\link[=level]{level()}} accessor allows doing so. 29 | } 30 | \examples{ 31 | lgr <- logger() 32 | level(lgr) # Prints "INFO". 33 | info(lgr, "This message is shown.") 34 | level(lgr) <- "FATAL" 35 | info(lgr, "This message is now suppressed.") 36 | } 37 | -------------------------------------------------------------------------------- /man/log4r-deprecated.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/deprecated.R 3 | \name{create.logger} 4 | \alias{create.logger} 5 | \alias{logfile} 6 | \alias{logfile<-} 7 | \alias{logfile.logger} 8 | \alias{logfile<-.logger} 9 | \alias{logformat} 10 | \alias{logformat<-} 11 | \alias{is.loglevel} 12 | \alias{loglevel} 13 | \alias{as.loglevel} 14 | \alias{print.loglevel} 15 | \alias{as.numeric.loglevel} 16 | \alias{as.character.loglevel} 17 | \alias{verbosity} 18 | \alias{levellog} 19 | \alias{debug} 20 | \alias{info} 21 | \alias{warn} 22 | \alias{error} 23 | \alias{fatal} 24 | \title{Deprecated logger functions} 25 | \usage{ 26 | create.logger(logfile = "logfile.log", level = "FATAL", logformat = NULL) 27 | 28 | logfile(x) 29 | 30 | logfile(x) <- value 31 | 32 | \method{logfile}{logger}(x) 33 | 34 | \method{logfile}{logger}(x) <- value 35 | 36 | logformat(x) 37 | 38 | logformat(x) <- value 39 | 40 | is.loglevel(x, ...) 41 | 42 | loglevel(i) 43 | 44 | as.loglevel(i) 45 | 46 | \method{print}{loglevel}(x, ...) 47 | 48 | \method{as.numeric}{loglevel}(x, ...) 49 | 50 | \method{as.character}{loglevel}(x, ...) 51 | 52 | verbosity(v) 53 | 54 | levellog(logger, level, ...) 55 | 56 | debug(logger, ...) 57 | 58 | info(logger, ...) 59 | 60 | warn(logger, ...) 61 | 62 | error(logger, ...) 63 | 64 | fatal(logger, ...) 65 | } 66 | \description{ 67 | \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} 68 | \itemize{ 69 | \item \code{\link[=create.logger]{create.logger()}} and \code{\link[=logfile]{logfile()}} are deprecated in favour of \code{\link[=logger]{logger()}}. 70 | They issue deprecation warnings when used. 71 | \item \code{\link[=debug]{debug()}}, \code{\link[=info]{info()}}, \code{\link[=warn]{warn()}}, \code{\link[=error]{error()}}, and \code{\link[=fatal]{fatal()}} are deprecated in 72 | favour of \code{\link[=log_debug]{log_debug()}}, \code{\link[=log_info]{log_info()}}, \code{\link[=log_warn]{log_warn()}}, \code{\link[=log_error]{log_error()}}, and 73 | \code{\link[=log_fatal]{log_fatal()}}, respectively. For performance reasons they do not yet issue 74 | deprecation warnings when used. 75 | \item \code{\link[=levellog]{levellog()}} is deprecated in favour of \code{\link[=log_at]{log_at()}}. It issues a deprecation 76 | warning when used. 77 | \item \code{\link[=logformat]{logformat()}} is incompatible with \strong{\link[=layouts]{Layouts}} and has been 78 | nonfunctional for many years. It issues a deprecation error when used. 79 | \item \code{\link[=is.loglevel]{is.loglevel()}}, \code{\link[=as.loglevel]{as.loglevel()}}, its alias \code{\link[=loglevel]{loglevel()}}, and S3 generics 80 | for the \code{"loglevel"} class are now considered an implementation detail and 81 | are no longer part of the public API. They issue deprecation warnings when 82 | used. 83 | \item \code{\link[=verbosity]{verbosity()}} is similar, in that there is no longer a stable mapping 84 | between priority integers and levels. It issues a deprecation warning when 85 | used. 86 | } 87 | } 88 | \keyword{internal} 89 | -------------------------------------------------------------------------------- /man/log4r-package.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/log4r-package.R 3 | \docType{package} 4 | \name{log4r-package} 5 | \alias{log4r} 6 | \alias{log4r-package} 7 | \title{log4r: A Fast and Lightweight Logging System for R, Based on 'log4j'} 8 | \description{ 9 | The log4r package is meant to provide a fast, lightweight, object-oriented approach to logging in R based on the widely-emulated 'log4j' system and etymology. 10 | } 11 | \seealso{ 12 | Useful links: 13 | \itemize{ 14 | \item \url{https://github.com/johnmyleswhite/log4r} 15 | \item Report bugs at \url{https://github.com/johnmyleswhite/log4r/issues} 16 | } 17 | 18 | } 19 | \author{ 20 | \strong{Maintainer}: Aaron Jacobs \email{atheriel@gmail.com} 21 | 22 | Authors: 23 | \itemize{ 24 | \item John Myles White [copyright holder] 25 | } 26 | 27 | Other contributors: 28 | \itemize{ 29 | \item Kenton White [contributor] 30 | \item Kirill Müller \email{krlmlr+r@mailbox.org} [contributor] 31 | } 32 | 33 | } 34 | \keyword{internal} 35 | -------------------------------------------------------------------------------- /man/log_at.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/logfuncs.R 3 | \name{log_at} 4 | \alias{log_at} 5 | \alias{log_debug} 6 | \alias{log_info} 7 | \alias{log_warn} 8 | \alias{log_error} 9 | \alias{log_fatal} 10 | \title{Write logs at a given level} 11 | \usage{ 12 | log_at(logger, level, ...) 13 | 14 | log_debug(logger, ...) 15 | 16 | log_info(logger, ...) 17 | 18 | log_warn(logger, ...) 19 | 20 | log_error(logger, ...) 21 | 22 | log_fatal(logger, ...) 23 | } 24 | \arguments{ 25 | \item{logger}{An object of class \code{"logger"}.} 26 | 27 | \item{level}{The desired severity, one of \code{"DEBUG"}, \code{"INFO"}, \code{"WARN"}, 28 | \code{"ERROR"}, or \code{"FATAL"}. Messages with a lower severity than the logger 29 | threshold will be discarded.} 30 | 31 | \item{...}{One or more items to log.} 32 | } 33 | \description{ 34 | Write logs at a given level 35 | } 36 | \examples{ 37 | logger <- logger() 38 | 39 | log_at(logger, "WARN", "First warning from our code") 40 | log_debug(logger, "Debugging our code") 41 | log_info(logger, "Information about our code") 42 | log_warn(logger, "Another warning from our code") 43 | log_error(logger, "An error from our code") 44 | log_fatal(logger, "I'm outta here") 45 | } 46 | -------------------------------------------------------------------------------- /man/logger.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/logger.R 3 | \name{logger} 4 | \alias{logger} 5 | \title{Create a logger} 6 | \usage{ 7 | logger(threshold = "INFO", appenders = console_appender()) 8 | } 9 | \arguments{ 10 | \item{threshold}{The logging threshold, one of \code{"DEBUG"}, \code{"INFO"}, \code{"WARN"}, 11 | \code{"ERROR"}, or \code{"FATAL"}. Logs with a lower severity than the threshold 12 | will be discarded.} 13 | 14 | \item{appenders}{The logging appenders; both single appenders and a \code{list()} 15 | of them are supported. See \strong{\link[=appenders]{Appenders}}.} 16 | } 17 | \value{ 18 | An object of class \code{"logger"}. 19 | } 20 | \description{ 21 | This is the main interface for configuring logging behaviour. We adopt the 22 | well-known \href{https://logging.apache.org/log4j/}{log4j} etymology: 23 | \strong{\link[=appenders]{Appenders}} are destinations (e.g. the console or a file) 24 | where logs are written, and the \strong{\link[=layouts]{Layout}} is the format of 25 | these logs. 26 | } 27 | \examples{ 28 | # By default, logs are written to the console at the "INFO" threshold. 29 | logger <- logger() 30 | 31 | log_info(logger, "Located nearest gas station.") 32 | log_warn(logger, "Ez-Gas sensor network is not available.") 33 | log_debug(logger, "Debug messages are suppressed by default.") 34 | } 35 | \seealso{ 36 | \strong{\link[=appenders]{Appenders}} and \strong{\link[=layouts]{Layouts}} for information on 37 | controlling the behaviour of the logger object. 38 | } 39 | -------------------------------------------------------------------------------- /man/syslog_appender.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/appenders.R 3 | \name{syslog_appender} 4 | \alias{syslog_appender} 5 | \title{Send logs to the local syslog} 6 | \usage{ 7 | syslog_appender(identifier, layout = bare_log_layout(), ...) 8 | } 9 | \arguments{ 10 | \item{identifier}{A string identifying the application.} 11 | 12 | \item{layout}{A layout function taking a \code{level} parameter and 13 | additional arguments corresponding to the message.} 14 | 15 | \item{...}{Further arguments passed on to \code{\link[rsyslog:syslog]{rsyslog::open_syslog()}}.} 16 | } 17 | \description{ 18 | Send messages to the local syslog. Requires the \code{rsyslog} package. 19 | } 20 | \seealso{ 21 | \link{appenders} for more information on Appenders. 22 | } 23 | -------------------------------------------------------------------------------- /man/tcp_appender.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/appenders.R 3 | \name{tcp_appender} 4 | \alias{tcp_appender} 5 | \title{Send logs over TCP} 6 | \usage{ 7 | tcp_appender( 8 | host, 9 | port, 10 | layout = default_log_layout(), 11 | timeout = getOption("timeout") 12 | ) 13 | } 14 | \arguments{ 15 | \item{host}{Hostname for the socket connection.} 16 | 17 | \item{port}{Port number for the socket connection.} 18 | 19 | \item{layout}{A layout function taking a \code{level} parameter and 20 | additional arguments corresponding to the message.} 21 | 22 | \item{timeout}{Timeout for the connection.} 23 | } 24 | \description{ 25 | Append messages to arbitrary TCP destinations. 26 | } 27 | \seealso{ 28 | \link{appenders} for more information on Appenders, and 29 | \code{\link[base:connections]{base::socketConnection()}} for the underlying connection object 30 | used by \code{\link[=tcp_appender]{tcp_appender()}}. 31 | } 32 | -------------------------------------------------------------------------------- /src/buf.h: -------------------------------------------------------------------------------- 1 | /* buf.h --- growable memory buffers for C99 2 | * This is free and unencumbered software released into the public domain. 3 | * 4 | * buf_size(v) : return the number of elements in the buffer (size_t) 5 | * buf_capacity(v) : return the total capacity of the buffer (size_t) 6 | * buf_free(v) : destroy and free the buffer 7 | * buf_push(v, e) : append an element E to the end 8 | * buf_pop(v) : remove and return an element E from the end 9 | * buf_grow(v, n) : increase buffer capactity by (ptrdiff_t) N elements 10 | * buf_trunc(v, n) : set buffer capactity to exactly (ptrdiff_t) N elements 11 | * buf_clear(v, n) : set buffer size to 0 (for push/pop) 12 | * 13 | * Note: buf_push(), buf_grow(), buf_trunc(), and buf_free() may change 14 | * the buffer pointer, and any previously-taken pointers should be 15 | * considered invalidated. 16 | * 17 | * Example usage: 18 | * 19 | * float *values = 0; 20 | * for (size_t i = 0; i < 25; i++) 21 | * buf_push(values, rand() / (float)RAND_MAX); 22 | * for (size_t i = 0; i < buf_size(values); i++) 23 | * printf("values[%zu] = %f\n", i, values[i]); 24 | * buf_free(values); 25 | */ 26 | #include 27 | #include 28 | 29 | #ifndef BUF_INIT_CAPACITY 30 | # define BUF_INIT_CAPACITY 8 31 | #endif 32 | 33 | #ifndef BUF_ABORT 34 | # define BUF_ABORT abort() 35 | #endif 36 | 37 | struct buf { 38 | size_t capacity; 39 | size_t size; 40 | char buffer[]; 41 | }; 42 | 43 | #define buf_ptr(v) \ 44 | ((struct buf *)((char *)(v) - offsetof(struct buf, buffer))) 45 | 46 | #define buf_free(v) \ 47 | do { \ 48 | if (v) { \ 49 | free(buf_ptr((v))); \ 50 | (v) = 0; \ 51 | } \ 52 | } while (0) 53 | 54 | #define buf_size(v) \ 55 | ((v) ? buf_ptr((v))->size : 0) 56 | 57 | #define buf_capacity(v) \ 58 | ((v) ? buf_ptr((v))->capacity : 0) 59 | 60 | #define buf_push(v, e) \ 61 | do { \ 62 | if (buf_capacity((v)) == buf_size((v))) { \ 63 | (v) = buf_grow1(v, sizeof(*(v)), \ 64 | !buf_capacity((v)) ? \ 65 | BUF_INIT_CAPACITY : \ 66 | buf_capacity((v))); \ 67 | } \ 68 | (v)[buf_ptr((v))->size++] = (e); \ 69 | } while (0) 70 | 71 | #define buf_pop(v) \ 72 | ((v)[--buf_ptr(v)->size]) 73 | 74 | #define buf_grow(v, n) \ 75 | ((v) = buf_grow1((v), sizeof(*(v)), n)) 76 | 77 | #define buf_trunc(v, n) \ 78 | ((v) = buf_grow1((v), sizeof(*(v)), n - buf_capacity(v))) 79 | 80 | #define buf_clear(v) \ 81 | ((v) ? (buf_ptr((v))->size = 0) : 0) 82 | 83 | 84 | static void * 85 | buf_grow1(void *v, size_t esize, ptrdiff_t n) 86 | { 87 | struct buf *p; 88 | size_t max = (size_t)-1 - sizeof(struct buf); 89 | if (v) { 90 | p = buf_ptr(v); 91 | if (n > 0 && p->capacity + n > max / esize) 92 | goto fail; /* overflow */ 93 | p = realloc(p, sizeof(struct buf) + esize * (p->capacity + n)); 94 | if (!p) 95 | goto fail; 96 | p->capacity += n; 97 | if (p->size > p->capacity) 98 | p->size = p->capacity; 99 | } else { 100 | if ((size_t)n > max / esize) 101 | goto fail; /* overflow */ 102 | p = malloc(sizeof(struct buf) + esize * n); 103 | if (!p) 104 | goto fail; 105 | p->capacity = n; 106 | p->size = 0; 107 | } 108 | return p->buffer; 109 | fail: 110 | BUF_ABORT; 111 | return 0; 112 | } 113 | -------------------------------------------------------------------------------- /src/log4r.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #define STRICT_R_HEADERS 5 | #include 6 | #include // Included by default in R (>= 3.4). 7 | 8 | SEXP R_fmt_current_time(SEXP fmt, SEXP utc, SEXP use_fractional, 9 | SEXP suffix_fmt) 10 | { 11 | const char *fmt_str = CHAR(Rf_asChar(fmt)); 12 | int use_utc = Rf_asLogical(utc); 13 | int append_us = Rf_asLogical(use_fractional); 14 | const char *suffix_fmt_str = NULL; 15 | if (suffix_fmt != R_NilValue) { 16 | suffix_fmt_str = CHAR(Rf_asChar(suffix_fmt)); 17 | } 18 | 19 | #if (__STDC_VERSION__ >= 201112L) 20 | struct timespec ts; 21 | timespec_get(&ts, TIME_UTC); 22 | char buffer[64]; 23 | struct tm* tm_info; 24 | 25 | tm_info = use_utc ? gmtime(&ts.tv_sec) : localtime(&ts.tv_sec); 26 | 27 | int written = strftime(buffer, 64, fmt_str, tm_info); 28 | if (!written) { 29 | Rf_error("Failed to format current time."); 30 | return R_NilValue; 31 | } 32 | 33 | if (!append_us) { 34 | return Rf_ScalarString(Rf_mkCharLen(buffer, written)); 35 | } 36 | 37 | int added = snprintf(buffer + written, 64 - written, ".%06ld", 38 | ts.tv_nsec / 1000); 39 | if (!added) { 40 | Rf_error("Failed to format current time."); 41 | return R_NilValue; 42 | } 43 | written += added; 44 | 45 | if (!suffix_fmt_str) { 46 | return Rf_ScalarString(Rf_mkCharLen(buffer, written)); 47 | } 48 | 49 | added = strftime(buffer + written, 64 - written, suffix_fmt_str, tm_info); 50 | if (!added) { 51 | Rf_error("Failed to format current time."); 52 | return R_NilValue; 53 | } 54 | written += added; 55 | #else 56 | time_t timer; 57 | char buffer[26]; 58 | struct tm* tm_info; 59 | 60 | time(&timer); 61 | tm_info = use_utc ? gmtime(&timer) : localtime(&timer); 62 | 63 | int written = strftime(buffer, 26, fmt_str, tm_info); 64 | if (!written) { 65 | Rf_error("Failed to format current time."); 66 | return R_NilValue; 67 | } 68 | 69 | if (!suffix_fmt_str) { 70 | return Rf_ScalarString(Rf_mkCharLen(buffer, written)); 71 | } 72 | 73 | int added = strftime(buffer + written, 64 - written, suffix_fmt_str, tm_info); 74 | if (!added) { 75 | Rf_error("Failed to format current time."); 76 | return R_NilValue; 77 | } 78 | written += added; 79 | #endif 80 | 81 | return Rf_ScalarString(Rf_mkCharLen(buffer, written)); 82 | } 83 | 84 | SEXP R_encode_logfmt(SEXP list); /* See logfmt.c. */ 85 | 86 | static const R_CallMethodDef log4r_entries[] = { 87 | {"R_fmt_current_time", (DL_FUNC) &R_fmt_current_time, 4}, 88 | {"R_encode_logfmt", (DL_FUNC) &R_encode_logfmt, 1}, 89 | {NULL, NULL, 0} 90 | }; 91 | 92 | void R_init_log4r(DllInfo *info) { 93 | R_registerRoutines(info, NULL, log4r_entries, NULL, NULL); 94 | R_useDynamicSymbols(info, FALSE); 95 | } 96 | -------------------------------------------------------------------------------- /src/logfmt.c: -------------------------------------------------------------------------------- 1 | #define STRICT_R_HEADERS 2 | #include 3 | 4 | #define BUF_ABORT 5 | #include "buf.h" 6 | 7 | #define LOGFMT_SIG_DIGITS 4 8 | 9 | /* Hex code "lookup table". */ 10 | static const char *hex = "0123456789abcdef"; 11 | 12 | char* buf_push_all(char *dest, const char *src, size_t n) 13 | { 14 | /* It's possible we could be more clever and use memcpy here somehow, but it 15 | would probably not be worth it. */ 16 | for (size_t i = 0; i < n; i++) { 17 | buf_push(dest, src[i]); 18 | } 19 | return dest; 20 | } 21 | 22 | int logfmt_needs_escape(char ch) 23 | { 24 | return ch == '"' || ch == '=' || ch == '\\' || ch <= 0x20; 25 | } 26 | 27 | char* buf_push_escaped(char *dest, const char *src, size_t n) 28 | { 29 | char ch; 30 | buf_push(dest, '"'); 31 | for (size_t i = 0; i < n; i++) { 32 | ch = src[i]; 33 | if (ch > 0x1f && ch != '"' && ch != '\\') { 34 | buf_push(dest, ch); 35 | continue; 36 | } 37 | buf_push(dest, '\\'); 38 | switch(ch) { 39 | case '\\': 40 | /* fallthrough */ 41 | case '"': 42 | buf_push(dest, ch); 43 | break; 44 | case '\n': 45 | buf_push(dest, 'n'); 46 | break; 47 | case '\t': 48 | buf_push(dest, 't'); 49 | break; 50 | case '\r': 51 | buf_push(dest, 'r'); 52 | break; 53 | default: 54 | /* Encode embedded control characters in the same manner as JSON, by 55 | converting e.g. '\b' to \u0008. This uses the classic bitshift/and 56 | approach to convert a character to hex. */ 57 | dest = buf_push_all(dest, "u00", 3); 58 | buf_push(dest, hex[(ch >> 4) & 0x0F]); 59 | buf_push(dest, hex[ch & 0x0F]); 60 | break; 61 | } 62 | } 63 | buf_push(dest, '"'); 64 | return dest; 65 | } 66 | 67 | SEXP R_encode_logfmt(SEXP list) 68 | { 69 | R_xlen_t len = Rf_length(list); 70 | SEXP keys = Rf_getAttrib(list, R_NamesSymbol); 71 | if (len == 0 || keys == R_NilValue) { 72 | return R_BlankScalarString; 73 | } 74 | PROTECT(keys); 75 | char *buffer = NULL; 76 | buf_grow(buffer, 256); 77 | 78 | SEXP elt; 79 | const char *str; 80 | R_xlen_t i, j, elt_len = 0; 81 | int empty_key = 1; 82 | for (i = 0; i < len; i++) { 83 | elt = STRING_ELT(keys, i); 84 | str = CHAR(elt); 85 | elt_len = Rf_length(elt); 86 | /* Skip fields with zero-length names rather than produce invalid output. */ 87 | if (elt_len == 0 || elt == NA_STRING) { 88 | continue; 89 | } 90 | if (!empty_key) { 91 | buf_push(buffer, ' '); 92 | } 93 | empty_key = 1; 94 | for (j = 0; j < elt_len; j++) { 95 | /* Drop characters that would need an escape. This matches the behaviour 96 | of popular Go implementations, and makes sense from a user perspective 97 | -- i.e. few programs respond to errors in their log writing code 98 | effectively, so it is better to be slightly lossy. */ 99 | if (logfmt_needs_escape(str[j])) { 100 | continue; 101 | } 102 | buf_push(buffer, str[j]); 103 | empty_key = 0; 104 | } 105 | /* Again, skip fields with zero-length names (after dropping escape 106 | characters) rather than produce invalid output. */ 107 | if (empty_key) { 108 | continue; 109 | } 110 | buf_push(buffer, '='); 111 | elt = VECTOR_ELT(list, i); 112 | if (Rf_length(elt) == 0) { 113 | buffer = buf_push_all(buffer, "null", 4); 114 | continue; 115 | } 116 | switch(TYPEOF(elt)) { 117 | case LGLSXP: { 118 | int v = LOGICAL_ELT(elt, 0); 119 | if (v == NA_LOGICAL) { 120 | buffer = buf_push_all(buffer, "null", 4); 121 | } else if (v) { 122 | buffer = buf_push_all(buffer, "true", 4); 123 | } else { 124 | buffer = buf_push_all(buffer, "false", 5); 125 | } 126 | break; 127 | } 128 | case INTSXP: { 129 | int v = INTEGER_ELT(elt, 0); 130 | if (v == NA_INTEGER) { 131 | buffer = buf_push_all(buffer, "null", 4); 132 | break; 133 | } 134 | char vbuff[32]; 135 | size_t written = snprintf(vbuff, 32, "%d", v); 136 | buffer = buf_push_all(buffer, vbuff, written); 137 | break; 138 | } 139 | case REALSXP: { 140 | double v = REAL_ELT(elt, 0); 141 | if (!R_finite(v)) { 142 | /* TODO: Write out Inf, -Inf, and NaN? */ 143 | buffer = buf_push_all(buffer, "null", 4); 144 | break; 145 | } 146 | char vbuff[32]; 147 | size_t written = snprintf(vbuff, 32, "%.*g", LOGFMT_SIG_DIGITS, v); 148 | buffer = buf_push_all(buffer, vbuff, written); 149 | break; 150 | } 151 | case CPLXSXP: 152 | elt = Rf_coerceVector(elt, STRSXP); 153 | /* fallthrough */ 154 | case STRSXP: { 155 | elt = STRING_ELT(elt, 0); 156 | if (elt == NA_STRING) { 157 | buffer = buf_push_all(buffer, "null", 4); 158 | break; 159 | } 160 | str = CHAR(elt); 161 | elt_len = Rf_length(elt); 162 | /* We need to scan through once to determine if the string needs to be 163 | escaped before we can actually write it out. */ 164 | for (j = 0; j < elt_len; j++) { 165 | if (logfmt_needs_escape(str[j])) { 166 | buffer = buf_push_escaped(buffer, str, elt_len); 167 | goto wrote_string; 168 | } 169 | } 170 | /* The string can be safely written without escaping. */ 171 | for (j = 0; j < elt_len; j++) { 172 | buf_push(buffer, str[j]); 173 | } 174 | wrote_string: 175 | break; 176 | } 177 | default: 178 | buffer = buf_push_all(buffer, "", 9); 179 | break; 180 | } 181 | } 182 | buf_push(buffer, '\n'); 183 | SEXP out = Rf_ScalarString(Rf_mkCharLen(buffer, buf_size(buffer))); 184 | buf_free(buffer); 185 | UNPROTECT(1); 186 | return out; 187 | } 188 | -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | # This file is part of the standard setup for testthat. 2 | # It is recommended that you do not modify it. 3 | # 4 | # Where should you do additional test configuration? 5 | # Learn more about the roles of various files in: 6 | # * https://r-pkgs.org/tests.html 7 | # * https://testthat.r-lib.org/reference/test_package.html#special-files 8 | 9 | library(testthat) 10 | library(log4r) 11 | 12 | test_check("log4r") 13 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/level.md: -------------------------------------------------------------------------------- 1 | # The logger threshold works as expected 2 | 3 | Code 4 | level(lgr) 5 | Output 6 | FATAL 7 | 8 | # Levels can be listed 9 | 10 | Code 11 | available.loglevels() 12 | Output 13 | $DEBUG 14 | DEBUG 15 | 16 | $INFO 17 | INFO 18 | 19 | $WARN 20 | WARN 21 | 22 | $ERROR 23 | ERROR 24 | 25 | $FATAL 26 | FATAL 27 | 28 | 29 | -------------------------------------------------------------------------------- /tests/testthat/helpers-logging.R: -------------------------------------------------------------------------------- 1 | expect_file_contains <- function(outfile, regex) { 2 | expect_true(file.exists(outfile)) 3 | 4 | if (file.exists(outfile)) { 5 | content <- readLines(outfile) 6 | 7 | expect_true(any(grepl(regex, content))) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/testthat/test-acceptance.R: -------------------------------------------------------------------------------- 1 | test_that('Creation', { 2 | rlang::local_options(lifecycle_verbosity = "quiet") 3 | 4 | logger <- create.logger() 5 | expect_s3_class(logger, "logger") 6 | 7 | logfile(logger) <- file.path('base.log') 8 | expect_equal(logfile(logger), file.path('base.log')) 9 | }) 10 | 11 | test_that('Logger levels', { 12 | rlang::local_options(lifecycle_verbosity = "quiet") 13 | 14 | logger <- create.logger() 15 | logfile(logger) <- file.path('base.log') 16 | 17 | level(logger) <- log4r:::DEBUG 18 | expect_true(level(logger) == 1) 19 | 20 | level(logger) <- log4r:::INFO 21 | expect_true(level(logger) == 2) 22 | 23 | level(logger) <- log4r:::WARN 24 | expect_true(level(logger) == 3) 25 | 26 | level(logger) <- log4r:::ERROR 27 | expect_true(level(logger) == 4) 28 | 29 | level(logger) <- log4r:::FATAL 30 | expect_true(level(logger) == 5) 31 | 32 | unlink(logfile(logger)) 33 | }) 34 | 35 | test_that('Creation of log file on first log entry', { 36 | rlang::local_options(lifecycle_verbosity = "quiet") 37 | 38 | logger <- create.logger() 39 | logfile(logger) <- file.path('base.log') 40 | level(logger) <- log4r:::DEBUG 41 | 42 | debug(logger, 'A Debugging Message') 43 | expect_true(file.exists(logfile(logger))) 44 | 45 | unlink(logfile(logger)) 46 | 47 | info(logger, 'An Info Message') 48 | expect_true(file.exists(logfile(logger))) 49 | 50 | unlink(logfile(logger)) 51 | 52 | warn(logger, 'A Warning Message') 53 | expect_true(file.exists(logfile(logger))) 54 | 55 | unlink(logfile(logger)) 56 | 57 | error(logger, 'An Error Message') 58 | expect_true(file.exists(logfile(logger))) 59 | 60 | unlink(logfile(logger)) 61 | 62 | fatal(logger, 'A Fatal Error Message') 63 | expect_true(file.exists(logfile(logger))) 64 | 65 | unlink(logfile(logger)) 66 | }) 67 | 68 | test_that('No creation of log file with insufficient level', { 69 | rlang::local_options(lifecycle_verbosity = "quiet") 70 | 71 | logger <- create.logger() 72 | logfile(logger) <- file.path('base.log') 73 | 74 | level(logger) <- "INFO" 75 | debug(logger, 'A Debugging Message') 76 | expect_false(file.exists(logfile(logger))) 77 | 78 | level(logger) <- "WARN" 79 | info(logger, 'An Info Message') 80 | expect_false(file.exists(logfile(logger))) 81 | 82 | level(logger) <- "ERROR" 83 | warn(logger, 'A Warning Message') 84 | expect_false(file.exists(logfile(logger))) 85 | 86 | level(logger) <- "FATAL" 87 | error(logger, 'An Error Message') 88 | expect_false(file.exists(logfile(logger))) 89 | 90 | fatal(logger, 'A Fatal Error Message') 91 | expect_true(file.exists(logfile(logger))) 92 | 93 | unlink(logfile(logger)) 94 | }) 95 | -------------------------------------------------------------------------------- /tests/testthat/test-appenders.R: -------------------------------------------------------------------------------- 1 | test_that("The console appender works correctly", { 2 | appender <- console_appender(simple_log_layout()) 3 | expect_output(appender("Message"), "Message") 4 | }) 5 | 6 | test_that("The file appender works correctly", { 7 | outfile <- tempfile("log") 8 | appender <- file_appender(outfile, layout = simple_log_layout()) 9 | 10 | expect_silent(appender("Message")) 11 | expect_file_contains(outfile, regex = "Message") 12 | }) 13 | 14 | test_that("The HTTP appender works correctly", { 15 | skip_if_not_installed("httr") 16 | # Don't send actual HTTP requests on CRAN. 17 | skip_on_cran() 18 | 19 | appender <- http_appender( 20 | "http://httpbin.org/post", layout = simple_log_layout() 21 | ) 22 | expect_silent(appender("INFO", "Message")) 23 | 24 | appender <- expect_silent(http_appender( 25 | "http://httpbin.org/post", 26 | method = "POST", 27 | layout = bare_log_layout(), 28 | httr::content_type_json() 29 | )) 30 | expect_silent(appender("INFO", '{"message":"Message"}')) 31 | }) 32 | 33 | test_that("The HTTP appender accepts only valid verbs", { 34 | skip_if_not_installed("httr") 35 | 36 | expect_error( 37 | http_appender("http://httpbin.org/post", method = "INVALID"), 38 | regex = "not a supported HTTP method" 39 | ) 40 | }) 41 | 42 | test_that("Layout arguments are checked", { 43 | expect_error(console_appender("notalayout")) 44 | expect_error(file_appender(tempfile("log"), layout = "notalayout")) 45 | expect_error(tcp_appender(layout = "notalayout")) 46 | 47 | skip_if_not_installed("httr") 48 | expect_error(http_appender(layout = "notalayout")) 49 | 50 | skip_if_not_installed("rsyslog") 51 | expect_error(syslog_appender(layout = "notalayout")) 52 | }) 53 | 54 | test_that("The syslog appender works correctly", { 55 | skip_if_not_installed("rsyslog") 56 | 57 | appender <- syslog_appender("myapp") 58 | 59 | # Don't send actual syslog messages on CRAN. 60 | skip_on_cran() 61 | expect_silent(appender("INFO", "Message")) 62 | }) 63 | 64 | test_that("Messages from the syslog appender end up in the system log", { 65 | skip_if_not_installed("rsyslog") 66 | skip_on_cran() 67 | 68 | if (nchar(Sys.which("journalctl")) == 0) { 69 | skip("No 'journalctl' available to check syslog messages.") 70 | } 71 | 72 | journal <- system2( 73 | "journalctl", c("-rq", "-t", "myapp"), stdout = TRUE, stderr = FALSE 74 | ) 75 | 76 | if (!is.null(attr(journal, "status"))) { 77 | skip("'journalctl' command failed, likely due to a permissions issue.") 78 | } 79 | 80 | expect_true(any( 81 | grepl(paste0("myapp[", Sys.getpid(), "]"), journal, fixed = TRUE) 82 | )) 83 | }) 84 | 85 | test_that("Lazy evaluation does not lead to surprising results", { 86 | x <- "foo.txt" 87 | y <- TRUE 88 | appender <- file_appender(file = x, append = y) 89 | x <- "bar.txt" 90 | y <- FALSE 91 | 92 | # Check that mutating the parent environment does not modify them in the 93 | # frame in which the appender evaluates. 94 | expect_equal(environment(appender)$file, "foo.txt") 95 | expect_equal(environment(appender)$append, TRUE) 96 | }) 97 | -------------------------------------------------------------------------------- /tests/testthat/test-layouts.R: -------------------------------------------------------------------------------- 1 | test_that("Basic layouts work correctly", { 2 | layout <- simple_log_layout() 3 | expect_match(layout("INFO", "Message"), "Message") 4 | expect_match( 5 | layout("INFO", "Message ", "in a", " bottle."), 6 | "Message in a bottle." 7 | ) 8 | 9 | layout <- default_log_layout() 10 | expect_match(layout("INFO", "Message"), "Message") 11 | expect_match( 12 | layout("INFO", "Message ", "in a", " bottle."), 13 | "Message in a bottle." 14 | ) 15 | 16 | layout <- bare_log_layout() 17 | expect_match(layout("INFO", "Message"), "Message") 18 | expect_match( 19 | layout("INFO", "Message ", "in a", " bottle."), 20 | "Message in a bottle." 21 | ) 22 | }) 23 | 24 | test_that("logfmt layouts work correctly", { 25 | layout <- logfmt_log_layout() 26 | expect_match(layout("INFO", "Message"), 'msg=Message') 27 | expect_match( 28 | layout("INFO", a = "a", b = 1.2, c = 1L, d = TRUE, e = NULL, f = 1+3i), 29 | 'a=a b=1.2 c=1 d=true e=null f=1\\+3i' 30 | ) 31 | # Test escaping of keys and values. 32 | expect_match(layout("INFO", spaces = "with spaces"), 'spaces="with spaces"') 33 | expect_match(layout("INFO", "with spaces" = "value"), 'withspaces=value') 34 | # Test dropped keys. 35 | expect_false(grepl('value', layout("INFO", a = "a", "value"))) 36 | expect_false(grepl('value', layout("INFO", " " = "value"))) 37 | # Test precision. 38 | expect_match(layout("INFO", a = 1.234567, b = NULL), 'a=1.235 b=null') 39 | # Test dropped field values. 40 | expect_match(layout("INFO", a = 1:3, b = NULL), 'a=1 b=null') 41 | expect_match( 42 | layout("INFO", a = data.frame(a = 1:3), b = NULL), 43 | 'a= b=null' 44 | ) 45 | # Test long keys and values. 46 | long <- paste(sample(c(letters, "="), 1019, TRUE), collapse = "") 47 | expect_match(layout("INFO", key = long), 'key=') 48 | expect_match(layout("INFO", long = "value"), '=value') 49 | }) 50 | 51 | test_that("JSON layouts work correctly", { 52 | skip_if_not_installed("jsonlite") 53 | 54 | layout <- json_log_layout() 55 | expect_match(layout("INFO", "Message"), "\"message\":\"Message\"") 56 | expect_match(layout("INFO", field = "value"), "\"field\":\"value\"") 57 | }) 58 | 59 | test_that("Wonky times formats are caught early", { 60 | expect_error( 61 | default_log_layout(strrep("%Y", 30)), 62 | regexp = "must be a valid timestamp format string" 63 | ) 64 | }) 65 | -------------------------------------------------------------------------------- /tests/testthat/test-level.R: -------------------------------------------------------------------------------- 1 | test_that("The logger threshold works as expected", { 2 | expect_error(logger(threshold = "UNKNOWN"), "must be one of") 3 | expect_error(logger(threshold = NULL), "must be one of") 4 | lgr <- logger() 5 | level(lgr) <- "FATAL" 6 | expect_snapshot(level(lgr)) 7 | }) 8 | 9 | test_that("Levels can be listed", { 10 | expect_snapshot(available.loglevels()) 11 | }) 12 | 13 | test_that("The loglevel() constructor works as expected", { 14 | rlang::local_options(lifecycle_verbosity = "quiet") 15 | 16 | expect_equal(loglevel(-19), DEBUG) 17 | expect_equal(loglevel(-1), DEBUG) 18 | expect_equal(loglevel(1), DEBUG) 19 | expect_equal(loglevel(2), INFO) 20 | expect_equal(loglevel(3), WARN) 21 | expect_equal(loglevel(4), ERROR) 22 | expect_equal(loglevel(5), FATAL) 23 | expect_equal(loglevel(6), FATAL) 24 | expect_equal(loglevel(60), FATAL) 25 | expect_equal(loglevel("DEBUG"), DEBUG) 26 | expect_equal(loglevel("INFO"), INFO) 27 | expect_equal(loglevel("WARN"), WARN) 28 | expect_equal(loglevel("ERROR"), ERROR) 29 | expect_equal(loglevel("FATAL"), FATAL) 30 | 31 | expect_error(loglevel("UNLOG"), "must be one of") 32 | expect_error(loglevel(1:3), "must be one of") 33 | expect_error(loglevel(FALSE), "must be one of") 34 | }) 35 | 36 | test_that("Coercion works as expected", { 37 | rlang::local_options(lifecycle_verbosity = "quiet") 38 | 39 | expect_equal(as.numeric(loglevel("DEBUG")), 1) 40 | expect_equal(as.character(loglevel("WARN")), "WARN") 41 | expect_equal(as.loglevel(loglevel("INFO")), loglevel("INFO")) 42 | }) 43 | 44 | test_that("The verbosity() constructor creates equivalent log levels", { 45 | rlang::local_options(lifecycle_verbosity = "quiet") 46 | 47 | expect_equal(verbosity(-19), FATAL) 48 | expect_equal(verbosity(-1), FATAL) 49 | expect_equal(verbosity(1), FATAL) 50 | expect_equal(verbosity(2), ERROR) 51 | expect_equal(verbosity(3), WARN) 52 | expect_equal(verbosity(4), INFO) 53 | expect_equal(verbosity(5), DEBUG) 54 | expect_equal(verbosity(6), DEBUG) 55 | expect_equal(verbosity(60), DEBUG) 56 | }) 57 | -------------------------------------------------------------------------------- /vignettes/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | *.R 3 | -------------------------------------------------------------------------------- /vignettes/logging-beyond-local-files.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Logging Beyond Local Files" 3 | date: "Updated as of `r as.Date(Sys.time())`" 4 | output: rmarkdown::html_vignette 5 | vignette: > 6 | %\VignetteIndexEntry{Logging Beyond Local Files} 7 | %\VignetteEngine{knitr::rmarkdown} 8 | %\VignetteEncoding{UTF-8} 9 | --- 10 | 11 | ```{r, include = FALSE} 12 | knitr::opts_chunk$set( 13 | collapse = TRUE, 14 | comment = "#>" 15 | ) 16 | library(log4r) 17 | ``` 18 | 19 | For local development or simple batch R scripts run manually, writing log 20 | messages to a file for later inspection (with `file_appender`) is quite 21 | convenient. However, for deployed R applications (like Shiny apps and Plumber 22 | APIs) or automated scripts it is more likely that all an organization's logs 23 | will be aggregated in one central place (perhaps with a commercial tool or 24 | service[^1]) for searching and monitoring. It can be annoying or impossible to 25 | upload log files in these cases. 26 | 27 | If your organization's platform supports reading log messages from regular 28 | program output,[^2] you can just use the default setup, which uses the 29 | `console_appender()`. Otherwise, **log4r** includes three additional appenders 30 | to facilitate shipping logs off to an aggregator: 31 | 32 | * `syslog_appender`: For writing messages to the system log on Linux, macOS, and 33 | other Unix-like operating systems. 34 | 35 | * `http_appender`: For sending log messages as HTTP requests. 36 | 37 | * `tcp_appender`: For writing log messages to TCP connections. 38 | 39 | ## Writing to the System Log 40 | 41 | The Unix "System log" (syslog) dates to the mid-1980s, and is still widely used. 42 | Almost all log aggregation services support ingesting a server's syslog 43 | messages, so often the easiest way to get your logs to these services is to make 44 | your R talk to the local syslog. 45 | 46 | To use the `syslog_appender`, all you need is an identifier for your R app or 47 | script: 48 | 49 | ```{r rsyslog, eval=requireNamespace("rsyslog", quietly = TRUE)} 50 | logger <- logger(appenders = syslog_appender("my-R-script")) 51 | ``` 52 | 53 | Requires the [**rsyslog**](https://cran.r-project.org/package=rsyslog) package. 54 | 55 | ## Sending Logs over HTTP 56 | 57 | If you're not already forwarding syslog messages (or need to send logs from 58 | Windows), the next most-common approach is to send them over HTTP. Log 59 | aggregation services usually provide an HTTP API endpoint to facilitate this: 60 | 61 | ```{r http-1} 62 | logger <- logger(appenders = http_appender("http://logging.example.local")) 63 | ``` 64 | 65 | Some services use `GET` or `PUT` requests instead of the more intuitive `POST`, 66 | which you can opt into as follows: 67 | 68 | ```{r http-2} 69 | logger <- logger( 70 | appenders = http_appender("http://logging.example.local", method = "GET") 71 | ) 72 | ``` 73 | 74 | Finally, if you need complete control over the HTTP request (for example, to 75 | send a specific header or use authentication), you can pass additional 76 | parameters to the underlying **httr** verb function: 77 | 78 | ```{r http-3} 79 | logger <- logger( 80 | appenders = http_appender( 81 | "http://logging.example.local", 82 | method = "GET", 83 | layout = default_log_layout(), 84 | httr::add_headers(`X-Custom-Header` = 1), 85 | httr::user_agent("my-r-script/1.0.0") 86 | ) 87 | ) 88 | ``` 89 | 90 | 91 | Requires the [**httr**](https://cran.r-project.org/package=httr) package. 92 | 93 | ## Writing Directly to TCP Connections 94 | 95 | For some workloads, the send-and-receive structure of HTTP requests may be 96 | undesirable, so many log aggregators also accept messages directly at a TCP 97 | port: 98 | 99 | ```{r tcp, eval = FALSE} 100 | logger <- logger( 101 | appenders = tcp_appender("tcp://logging.example.local", port = 9551) 102 | ) 103 | ``` 104 | 105 | [^1]: Such as [Splunk](https://www.splunk.com/) or 106 | [Loggly](https://www.loggly.com/solution/log-management/). There are also 107 | many open-source options, such as the [ELK 108 | Stack](https://www.elastic.co/elastic-stack/) or 109 | [Graylog](https://graylog.org/). 110 | 111 | [^2]: For example, when running as a Docker container in a cluster with all logs 112 | forwarded automatically, or when scripts are wrapped up as SystemD unit files. 113 | -------------------------------------------------------------------------------- /vignettes/performance.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Logging Performance" 3 | date: "Updated as of `r as.Date(Sys.time())`" 4 | output: rmarkdown::html_vignette 5 | vignette: > 6 | %\VignetteIndexEntry{Logging Performance} 7 | %\VignetteEngine{knitr::rmarkdown} 8 | %\VignetteEncoding{UTF-8} 9 | --- 10 | 11 | ```{r, include = FALSE} 12 | knitr::opts_chunk$set( 13 | collapse = TRUE, 14 | comment = "#>" 15 | ) 16 | ``` 17 | 18 | The following vignette presents benchmarks for **log4r** against all 19 | general-purpose logging packages available on CRAN: 20 | 21 | - [**futile.logger**](https://cran.r-project.org/package=futile.logger) and 22 | [**logging**](https://cran.r-project.org/package=logging), which are 23 | well-established packages; and 24 | - [**logger**](https://cran.r-project.org/package=logger), 25 | [**lgr**](https://cran.r-project.org/package=lgr), 26 | [**loggit**](https://cran.r-project.org/package=loggit), and 27 | [**rlog**](https://cran.r-project.org/package=rlog), all of which are 28 | relative newcomers. 29 | 30 | Each logging package features slightly different capabilities, but these 31 | benchmarks are focused on the two situations common to using all of them: 32 | 33 | 1. Logging simple messages to the console; and 34 | 2. Deciding not to log a message because it is below the threshold. 35 | 36 | The first of these is likely the most common kind of logging done by end users, 37 | although some may chose to log to files, over HTTP, or to the [system log](https://cran.r-project.org/package=rsyslog) 38 | (among others). Yet a benchmark of these other scenarios would largely show 39 | the relative expense of these operations, instead of the overhead of the 40 | logic performed by the logging packages themselves. 41 | 42 | The second measures the performance impact of leaving logging messages in 43 | running code, even if they are below the current threshold of visibility. This 44 | is another measure of overhead for each logging package. 45 | 46 | ## Using `cat()` 47 | 48 | As a reference point, we can measure how long it takes R itself to write a 49 | simple message to the console: 50 | 51 | ```{r cat-setup} 52 | cat_debug <- function() { 53 | cat() # Print nothing. 54 | } 55 | 56 | cat_info <- function() cat( 57 | "INFO [", format(Sys.time(), "%Y-%m-%d %H:%M:%S", usetz = FALSE), 58 | "] Info message.", sep = "" 59 | ) 60 | ``` 61 | 62 | ## The **log4r** Package 63 | 64 | The following is a typical **log4r** setup: 65 | 66 | ```{r log4r-setup, results = "hide"} 67 | log4r_logger <- log4r::logger(threshold = "INFO") 68 | 69 | log4r_info <- function() { 70 | log4r::log_info(log4r_logger, "Info message.") 71 | } 72 | 73 | log4r_debug <- function() { 74 | log4r::log_debug(log4r_logger, "Debug message.") 75 | } 76 | ``` 77 | 78 | ## The **futile.logger** Package 79 | 80 | The following is a typical **futile.logger** setup: 81 | 82 | ```{r fl-setup, results = "hide"} 83 | requireNamespace("futile.logger") 84 | 85 | futile.logger::flog.logger() 86 | 87 | fl_info <- function() { 88 | futile.logger::flog.info("Info message.") 89 | } 90 | 91 | fl_debug <- function() { 92 | futile.logger::flog.debug("Debug message.") 93 | } 94 | ``` 95 | 96 | ## The **logging** Package 97 | 98 | The following is what I believe to be a typical **logging** setup: 99 | 100 | ```{r logging-setup, results = "hide"} 101 | requireNamespace("logging") 102 | 103 | logging::basicConfig() 104 | 105 | logging_info <- function() { 106 | logging::loginfo("Info message.") 107 | } 108 | 109 | logging_debug <- function() { 110 | logging::logdebug("Debug message.") 111 | } 112 | ``` 113 | 114 | ## The **logger** Package 115 | 116 | The following is what I believe to be a typical **logger** setup: 117 | 118 | ```{r logger-setup, results = "hide"} 119 | requireNamespace("logger") 120 | 121 | # Match the behaviour of other logging packages and write to the console. 122 | logger::log_appender(logger::appender_stdout) 123 | 124 | logger_info <- function() { 125 | logger::log_info("Info message.") 126 | } 127 | 128 | logger_debug <- function() { 129 | logger::log_debug("Debug message.") 130 | } 131 | ``` 132 | 133 | ## The **lgr** Package 134 | 135 | The following is what I believe to be a typical **lgr** setup: 136 | 137 | ```{r lgr-setup, results = "hide"} 138 | requireNamespace("lgr") 139 | 140 | lgr_logger <- lgr::get_logger("perf-test") 141 | lgr_logger$set_appenders(list(cons = lgr::AppenderConsole$new())) 142 | lgr_logger$set_propagate(FALSE) 143 | 144 | lgr_info <- function() { 145 | lgr_logger$info("Info message.") 146 | } 147 | 148 | lgr_debug <- function() { 149 | lgr_logger$debug("Debug message.") 150 | } 151 | ``` 152 | 153 | ## The **loggit** Package 154 | 155 | The following is what I believe to be a typical **loggit** setup. Since we only 156 | want to log to the console, set the output file to `/dev/null`. In addition, 157 | **loggit** does not have a notion of thresholds, so there is no "do nothing" 158 | operation to test. 159 | 160 | ```{r loggit-setup, results = "hide"} 161 | requireNamespace("loggit") 162 | 163 | if (.Platform$OS.type == "unix") { 164 | loggit::set_logfile("/dev/null") 165 | } else { 166 | loggit::set_logfile("nul") 167 | } 168 | 169 | loggit_info <- function() { 170 | loggit::loggit("INFO", "Info message.") 171 | } 172 | ``` 173 | 174 | ## The **rlog** Package 175 | 176 | The **rlog** package currently has no configuration options other than the 177 | threshold, which is controlled via an environment variable and defaults to 178 | hiding debug-level messages: 179 | 180 | ```{r rlog-setup, results = "hide"} 181 | requireNamespace("rlog") 182 | 183 | rlog_info <- function() { 184 | rlog::log_info("Info message.") 185 | } 186 | 187 | rlog_debug <- function() { 188 | rlog::log_debug("Debug message.") 189 | } 190 | ``` 191 | 192 | ## Test All Loggers 193 | 194 | Debug messages should print nothing. 195 | 196 | ```{r test-loggers-debug} 197 | log4r_debug() 198 | cat_debug() 199 | logging_debug() 200 | fl_debug() 201 | logger_debug() 202 | lgr_debug() 203 | rlog_debug() 204 | ``` 205 | 206 | Info messages should print to the console. Small differences in output format 207 | are to be expected. 208 | 209 | ```{r test-loggers, collapse=TRUE} 210 | log4r_info() 211 | cat_info() 212 | logging_info() 213 | fl_info() 214 | logger_info() 215 | lgr_info() 216 | loggit_info() 217 | rlog_info() 218 | ``` 219 | 220 | ## Benchmarks 221 | 222 | The following benchmarks all loggers defined above: 223 | 224 | ```{r benchmark, results = "hide", echo = TRUE} 225 | info_bench <- microbenchmark::microbenchmark( 226 | cat = cat_info(), 227 | log4r = log4r_info(), 228 | futile.logger = fl_info(), 229 | logging = logging_info(), 230 | logger = logger_info(), 231 | lgr = lgr_info(), 232 | loggit = loggit_info(), 233 | rlog = rlog_info(), 234 | times = 500, 235 | control = list(warmups = 50) 236 | ) 237 | 238 | debug_bench <- microbenchmark::microbenchmark( 239 | cat = cat_debug(), 240 | log4r = log4r_debug(), 241 | futile.logger = fl_debug(), 242 | logging = logging_debug(), 243 | logger = logger_debug(), 244 | lgr = lgr_debug(), 245 | rlog = rlog_debug(), 246 | times = 500, 247 | control = list(warmups = 50) 248 | ) 249 | ``` 250 | 251 | ### How long does it take to print messages? 252 | 253 | ```{r print-benchmark-1} 254 | print(info_bench, order = "median") 255 | ``` 256 | 257 | ### How long does it take to decide to do nothing? 258 | 259 | ```{r print-benchmark-2} 260 | print(debug_bench, order = "median") 261 | ``` 262 | -------------------------------------------------------------------------------- /vignettes/structured-logging.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Structured Logging" 3 | date: "Updated as of `r as.Date(Sys.time())`" 4 | output: rmarkdown::html_vignette 5 | vignette: > 6 | %\VignetteIndexEntry{Structured Logging} 7 | %\VignetteEngine{knitr::rmarkdown} 8 | %\VignetteEncoding{UTF-8} 9 | --- 10 | 11 | ```{r, include = FALSE} 12 | knitr::opts_chunk$set( 13 | collapse = TRUE, 14 | comment = "#>" 15 | ) 16 | library(log4r) 17 | 18 | # Dummy Plumber request & response. 19 | req <- list( 20 | REQUEST_METHOD = "POST", PATH_INFO = "/upload", 21 | QUERY_STRING = "", HTTP_USER_AGENT = "curl/7.58.0", 22 | REMOTE_ADDR = "124.133.52.161" 23 | ) 24 | res <- list(status = 401) 25 | 26 | # Dummy CSV-parsing stuff: 27 | filename <- "catpics_01.csv" 28 | entries <- data.frame(x = 1:4124) 29 | start <- Sys.time() 30 | ``` 31 | 32 | Traditionally, messages emitted from R packages or scripts are unstructured messages, like this one from the **shiny** package: 33 | 34 | ``` text 35 | Listening on http://localhost:8080 36 | ``` 37 | 38 | A richer, more structured representation of this log message might be: 39 | 40 | ``` text 41 | level=INFO ts=2021-10-21T20:21:01Z message="starting Shiny" host=localhost port=8080 shiny_version=1.6.0 appdir=projects/mycoolapp 42 | ``` 43 | 44 | This second message uses *structured logging*, attaching relevant metadata to a log message as standalone fields. 45 | 46 | Structured logs have two advantages: 47 | 48 | * They tend to have more standard, predictable content, which can make reading 49 | them easier (especially if you did not write the message yourself); and 50 | 51 | * They are much, much easier for log management and aggregation systems to 52 | query -- fields can be used to aggregate logs into metrics like "how 53 | many times has X happened" and "how long does task Y take on average", and 54 | individual fields can be used to answer questions like "what happened to user 55 | Z over their last few logins". 56 | 57 | **log4r** includes support for two of the most popular structured logging 58 | formats: 59 | 60 | * `json_log_layout`: Emit log messages as JSON, likely the most widely-used. 61 | 62 | * `logfmt_log_layout`: Emit log messages using the more human-friendly [logfmt](https://brandur.org/logfmt). 63 | 64 | To use these formats, you can pass additional arguments to the existing logging 65 | functions `log_info()`, `log_warn()` and `log_error()`. 66 | 67 | ## JSON Logs 68 | 69 | The most popular format for structured logging is probably JSON, which you can 70 | configure as follows: 71 | 72 | ```{r json} 73 | logger <- logger(appenders = console_appender(json_log_layout())) 74 | ``` 75 | 76 | As an example, suppose you are logging unauthorised requests to a [Plumber](https://www.rplumber.io/) API. You might 77 | have a log message with fields like the following: 78 | 79 | ```{r json-example} 80 | # Here "req" and "res" are slightly fake request & response objects. 81 | log_info( 82 | logger, message = "authentication failed", 83 | method = req$REQUEST_METHOD, 84 | path = req$PATH_INFO, 85 | params = sub("^\\?", "", req$QUERY_STRING), 86 | user_agent = req$HTTP_USER_AGENT, 87 | remote_addr = req$REMOTE_ADDR, 88 | status = res$status 89 | ) 90 | ``` 91 | 92 | ## logfmt Logs 93 | 94 | An alternative to JSON is the popular, more human-friendly logfmt style, which 95 | you can configure as follows: 96 | 97 | ```{r logfmt} 98 | logger <- logger(appenders = console_appender(logfmt_log_layout())) 99 | ``` 100 | 101 | As an example, you might have the following in a script that processes many CSV 102 | files: 103 | 104 | ```{r logfmt-example} 105 | log_info( 106 | logger, message = "processed entries", file = filename, 107 | entries = nrow(entries), 108 | elapsed = unclass(difftime(Sys.time(), start, units = "secs")) 109 | ) 110 | ``` 111 | --------------------------------------------------------------------------------