2

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
Andres Mora
  • 1,040
  • 8
  • 16

1 Answers1

2

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)
Nick ODell
  • 15,465
  • 3
  • 32
  • 66