Search code examples
rdockerplumber

How to deploy a Plumber docker that writes a log file?


Im trying to deploy a R Plumber test API as a docker container as my main goal is to successfully create a log file everytime the API processes a request. I have tested it locally and it works.

Here are my test files:

plumber.R

# plumber.R
# A simple API to illustrate logging with Plumber

library(plumber)

#* @apiTitle Logging Example

#* @apiDescription Simple example API for implementing logging with Plumber

#* Echo back the input
#* @param msg The message to echo
#* @get /echo
function(msg = "") {
  list(msg = paste0("The message is: '", msg, "'"))
}

entrypoint.R

library(plumber)

# Config
#config <- config::get()

# logging
library(logger)
# Ensure glue is a specific dependency so it's avaible for logger
library(glue)

# Specify how logs are written 
log_dir <- "/app/logs"
if (!fs::dir_exists(log_dir)) fs::dir_create(log_dir)
log_appender(appender_tee(tempfile("plumber_", log_dir, ".log")))

convert_empty <- function(string) {
  if (string == "") {
    "-"
  } else {
    string
  }
}

pr <- plumb("plumber.R")

pr$registerHooks(
  list(
    preroute = function() {
      # Start timer for log info
      tictoc::tic()
    },
    postroute = function(req, res) {
      end <- tictoc::toc(quiet = TRUE)
      # Log details about the request and the response
      # TODO: Sanitize log details - perhaps in convert_empty
      log_info('{convert_empty(req$REMOTE_ADDR)} "{convert_empty(req$HTTP_USER_AGENT)}" {convert_empty(req$HTTP_HOST)} {convert_empty(req$REQUEST_METHOD)} {convert_empty(req$PATH_INFO)} {convert_empty(res$status)} {round(end$toc - end$tic, digits = getOption("digits", 5))}')
    }
  )
)

pr

Dockerfile

# Base image https://hub.docker.com/u/rocker/
FROM rstudio/plumber

# Install R libraries
RUN R -e "install.packages(c('odbc','glue','parsedate','tibble','dplyr','httr','RCurl','jsonlite','rjson','stringr','telegram','R.utils','logger','tictoc'))"

# Run plumber package
CMD ["/app/plumber.R"]

My expected result is to be able to store the log in a shared folder between the container and the host. But so far I havent been able to have the plumber app even to even create a log file with logs inside /app/logs.

docker build -t logger2 .

docker run -d -p 18001:8000 --rm \
-v /home/user/R/projects/logger2/:/app \
-v /tmp/log1:/app/logs \
logger2

Solution

  • The problem here is the entrypoint. Your entrypoint.R script is being ignored.

    Here's the default entrypoint that the rplumber image comes with:

    ENTRYPOINT ["R", "-e", "pr <- plumber::plumb(rev(commandArgs())[1]); args <- list(host = '0.0.0.0', port = 8000); if (packageVersion('plumber') >= '1.0.0') { pr$setDocs(TRUE) } else { args$swagger <- TRUE }; do.call(pr$run, args)"]
    

    Here's what that does:

    • Load the plumber library.
    • From the CMD line in your dockerfile, get filename of the file to load.
    • Plumb that file.
    • Turn on docs.
    • Listen on all interfaces on port 8000.

    In this case, it loads the /app/plumber.R file, and ignores the /app/entrypoint.R file.

    How can you fix that? Define your own entrypoint in the Dockerfile.

    FROM rstudio/plumber
    
    # Install R libraries
    RUN R -e "install.packages(c('odbc','glue','parsedate','tibble','dplyr','httr','RCurl','jsonlite','rjson','stringr','telegram','R.utils','logger','tictoc'))"
    
    ENTRYPOINT ["Rscript", "/app/entrypoint.R"]
    

    Here I still ran into a few problems:

    1. No fs package. I added RUN R -e "install.packages(c('fs'))" to install it.
    2. Got the error Error in plumb("plumber.R") : File does not exist: plumber.R. This is because the server is running in the wrong directory. I fixed this by adding WORKDIR /app to your Dockerfile.
    3. Had to change last line of entrypoint.R from pr to pr$run(host='0.0.0.0', port=8000) to make it actually run.

    Full Dockerfile:

    FROM rstudio/plumber
    
    # Install R libraries
    RUN R -e "install.packages(c('odbc','glue','parsedate','tibble','dplyr','httr','RCurl','jsonlite','rjson','stringr','telegram','R.utils','logger','tictoc'))"
    RUN R -e "install.packages(c('fs'))"
    
    WORKDIR /app
    ENTRYPOINT ["Rscript", "/app/entrypoint.R"]
    

    Full entrypoint.R file:

    library(plumber)
    
    # Config
    #config <- config::get()
    
    # logging
    library(logger)
    # Ensure glue is a specific dependency so it's avaible for logger
    library(glue)
    
    # Specify how logs are written 
    log_dir <- "/app/logs"
    if (!fs::dir_exists(log_dir)) fs::dir_create(log_dir)
    log_appender(appender_tee(tempfile("plumber_", log_dir, ".log")))
    
    convert_empty <- function(string) {
      if (string == "") {
        "-"
      } else {
        string
      }
    }
    
    pr <- plumb("plumber.R")
    
    pr$registerHooks(
      list(
        preroute = function() {
          # Start timer for log info
          tictoc::tic()
        },
        postroute = function(req, res) {
          end <- tictoc::toc(quiet = TRUE)
          # Log details about the request and the response
          # TODO: Sanitize log details - perhaps in convert_empty
          log_info('{convert_empty(req$REMOTE_ADDR)} "{convert_empty(req$HTTP_USER_AGENT)}" {convert_empty(req$HTTP_HOST)} {convert_empty(req$REQUEST_METHOD)} {convert_empty(req$PATH_INFO)} {convert_empty(res$status)} {round(end$toc - end$tic, digits = getOption("digits", 5))}')
        }
      )
    )
    
    pr$run(host='0.0.0.0', port=8000)