├── .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 | [](https://cran.r-project.org/package=log4r)
22 | [](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 | [](https://cran.r-project.org/package=log4r)
10 | [](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 |
22 |
--------------------------------------------------------------------------------
/man/figures/lifecycle-experimental.svg:
--------------------------------------------------------------------------------
1 |
22 |
--------------------------------------------------------------------------------
/man/figures/lifecycle-stable.svg:
--------------------------------------------------------------------------------
1 |
30 |
--------------------------------------------------------------------------------
/man/figures/lifecycle-superseded.svg:
--------------------------------------------------------------------------------
1 |
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 |
--------------------------------------------------------------------------------