1.4. Migrating from SAS to R: A Skill Conversion Guide
1.4.10. Log Messages: SAS Log vs R Console
This guide compares SAS log and R console approaches for handling diagnostic messages, warnings, and errors, highlighting key similarities and differences between the two languages.
1. Basic Log Messaging
| Feature | SAS | R |
|---|---|---|
| Informational messages | PUT statement, %PUT macro statement |
message() function |
| Warning messages | Automatic warnings, %PUT WARNING: |
warning() function |
| Error messages | Automatic errors, %PUT ERROR: |
stop() function |
| Execution behavior | Errors may halt step but not entire program | stop() halts execution unless in try() or tryCatch() |
| Log destination | SAS log window/file | R console/sink file |
SAS Example
/* SAS informational message */
data _null_;
put "Processing data step at " datetime.;
run;
/* SAS warning via %PUT */
%put WARNING: Missing observations detected;
/* SAS error via %PUT */
%put ERROR: Data does not meet validation criteria;
/* SAS error from data step */
data invalid;
set sashelp.class;
if age < 0 then abort;
run;
Explanation (SAS):
- The SAS log automatically collects all output messages
PUTstatement writes text to the log from DATA steps%PUTwrites messages from macro codeWARNING:andERROR:prefixes create highlighted warning/error messagesabortstatement halts execution of the current DATA step- SAS continues processing subsequent code even after most errors
- Messages appear in the log window or log file in batch mode
R Example
# R informational message
message("Processing data at ", format(Sys.time(), "%Y-%m-%d %H:%M:%S"))
# R warning message
if (any(is.na(data$age))) {
warning("Missing values detected in age variable")
}
# R error message
if (nrow(data) == 0) {
stop("Dataset contains no observations")
}
# R error with condition
validate_age <- function(age) {
if (any(age < 0, na.rm = TRUE)) {
stop("Negative age values detected")
}
}
Explanation (R):
- R writes messages directly to the console (no separate log)
message()creates informational messageswarning()creates warning messages (code continues)stop()creates error messages that halt executiontry()andtryCatch()can catch errors for graceful handlingsuppressMessages(),suppressWarnings()can hide output- Messages can be redirected using
sink()or logging packages
Example Output in SAS Log:
NOTE: DATA statement used (Total process time):
real time 0.00 seconds
cpu time 0.00 seconds
Processing data step at 01JAN2023:14:30:45.000
WARNING: Missing observations detected
ERROR: Data does not meet validation criteria
ERROR: The data set WORK.INVALID may be incomplete. When this step was stopped there were 0 observations and 5 variables.
Example Output in R Console:
Processing data at 2023-01-01 14:30:45
Warning: Missing values detected in age variable
Error: Dataset contains no observations
2. Log Message Types and Features
| Message Type | SAS | R |
|---|---|---|
| NOTE | Informational, automatically generated | Use message() |
| WARNING | Potential issues, execution continues | Use warning() |
| ERROR | Critical issues, step may halt | Use stop() |
| Custom severity | %PUT %SYSFUNC(CATS(%NRSTR(WAR), NING)): |
Use custom functions |
| Conditional messages | %IF condition %THEN %PUT |
if (condition) message() |
| Message suppression | options nonotes nosource; |
suppressMessages(), suppressWarnings() |
SAS Example
/* Create sample data for age check */
data patients;
input PatID $ Age Treatment $;
datalines;
P001 45 A
P002 -2 B
P003 . A
P004 62 B
;
run;
/* Validation with custom messages */
data validated;
set patients;
/* Count issues for final report */
if _N_ = 1 then do;
call symputx('negative_count', 0);
call symputx('missing_count', 0);
end;
/* Check age values */
if Age < 0 then do;
put "WARNING: Patient " PatID " has invalid negative age " Age;
call symputx('negative_count', symget('negative_count') + 1);
end;
if Age = . then do;
put "NOTE: Patient " PatID " has missing age";
call symputx('missing_count', symget('missing_count') + 1);
end;
run;
/* Final validation summary */
%put NOTE: Data validation complete;
%put %sysfunc(ifc(&negative_count > 0,
%str(WARNING: &negative_count patients with negative ages),
%str(NOTE: No patients with negative ages)));
%put %sysfunc(ifc(&missing_count > 0,
%str(NOTE: &missing_count patients with missing ages),
%str(NOTE: No patients with missing ages)));
Explanation (SAS):
PUTstatements generate custom log messages during data step execution- Messages can include variable values like
PatIDandAge call symputx()creates macro variables to track counts across observations%PUTwith conditional logic generates summary messages- Different message types (NOTE/WARNING) help highlight issues by severity
- Messages appear chronologically as the code executes
R Example
# Show input data
patients <- data.frame(
PatID = c("P001", "P002", "P003", "P004"),
Age = c(45, -2, NA, 62),
Treatment = c("A", "B", "A", "B")
)
print(patients)
# No package needed for base functions
# Function to validate age values
validate_age_data <- function(data) {
# Initialize counters
negative_count <- 0
missing_count <- 0
# Check each observation
for (i in 1:nrow(data)) {
# Check negative age
if (!is.na(data$Age[i]) && data$Age[i] < 0) {
warning(sprintf("Patient %s has invalid negative age %d",
data$PatID[i], data$Age[i]))
negative_count <- negative_count + 1
}
# Check missing age
if (is.na(data$Age[i])) {
message(sprintf("Patient %s has missing age", data$PatID[i]))
missing_count <- missing_count + 1
}
}
# Final validation summary
message("Data validation complete")
if (negative_count > 0) {
warning(sprintf("%d patients with negative ages", negative_count))
} else {
message("No patients with negative ages")
}
if (missing_count > 0) {
message(sprintf("%d patients with missing ages", missing_count))
} else {
message("No patients with missing ages")
}
return(data)
}
# Run validation
validated <- validate_age_data(patients)
Explanation (R):
- R combines validation logic and messaging in a function
sprintf()formats strings with variable valuesmessage()creates informational notes (similar to SAS NOTEs)warning()creates warning messages for potential issues- Messages can be generated conditionally inside loops
- R returns results at the end with a summary report
Input Data:
| PatID | Age | Treatment |
|---|---|---|
| P001 | 45 | A |
| P002 | -2 | B |
| P003 | . | A |
| P004 | 62 | B |
Expected SAS Log Output:
WARNING: Patient P002 has invalid negative age -2
NOTE: Patient P003 has missing age
NOTE: Data validation complete
WARNING: 1 patients with negative ages
NOTE: 1 patients with missing ages
Expected R Console Output:
Patient P003 has missing age
Data validation complete
1 patients with missing ages
Warning messages:
1: In validate_age_data(patients) :
Patient P002 has invalid negative age -2
2: In validate_age_data(patients) : 1 patients with negative ages
3. Error Handling Approaches
| Capability | SAS | R |
|---|---|---|
| Continue after errors | Default behavior | Use try() or tryCatch() |
| Stop on first error | options errorabend; |
Default behavior with stop() |
| Capture error info | %SYSERR, &SYSERRORTEXT |
Error object in tryCatch() |
| Custom error actions | %MACRO with error handling |
Custom functions with tryCatch() |
| Return error codes | %LET rc = &SYSERR |
Functions return error status |
SAS Example
/* Basic error handling with %SYSERR */
%macro import_check(file=);
/* Try to import the file */
proc import datafile="&file"
out=work.imported
dbms=xlsx replace;
run;
/* Check if import succeeded */
%if &syserr > 0 %then %do;
%put ERROR: Import failed with error code &syserr;
%put ERROR: &syserrortext;
%put ERROR: Skipping subsequent processing;
%return;
%end;
%put NOTE: Import successful, proceeding with analysis;
proc means data=work.imported;
run;
%mend;
/* Test with nonexistent file */
%import_check(file=nonexistent.xlsx);
Explanation (SAS):
&SYSERRautomatic macro variable contains the error status code&SYSERRORTEXTcontains textual description of the error%IF-%THENlogic allows conditional execution based on error status%RETURNexits the macro when an error is detected- SAS will execute the entire macro unless it's explicitly terminated
- Error details are recorded in the SAS log
R Example
# Explicitly load readxl for Excel import
library(readxl)
# Basic error handling with tryCatch
import_check <- function(file) {
# Try to import the file
result <- tryCatch(
{
# Attempt the import
imported <- readxl::read_excel(file)
message("Import successful, proceeding with analysis")
# Perform additional analysis
summary_stats <- summary(imported)
return(list(data = imported, summary = summary_stats, status = "success"))
},
error = function(e) {
# Handle the error
message("ERROR: Import failed")
message(paste("ERROR:", e$message))
message("ERROR: Skipping subsequent processing")
return(list(data = NULL, summary = NULL, status = "failed",
error = e$message))
},
warning = function(w) {
# Handle warnings
message("WARNING: Import completed with warnings")
message(paste("WARNING:", w$message))
# Still try to proceed
imported <- readxl::read_excel(file)
summary_stats <- summary(imported)
return(list(data = imported, summary = summary_stats,
status = "warning", warning = w$message))
}
)
return(result)
}
# Test with nonexistent file
result <- import_check("nonexistent.xlsx")
if (result$status != "success") {
message("Processing halted due to import issues")
} else {
message("Continue with further processing...")
}
Explanation (R):
tryCatch()wraps code that might produce errors or warnings- Separate handler functions for error and warning conditions
- Error handler prevents the program from stopping and captures details
- Return value includes error status and information
- Calling code checks the status to determine next steps
- Structured error handling similar to try-catch in other languages
Expected SAS Log Output:
ERROR: Import failed with error code 4
ERROR: File NONEXISTENT.XLSX does not exist.
ERROR: Skipping subsequent processing
Expected R Console Output:
> # Test with nonexistent file
> result <- import_check("nonexistent.xlsx")
ERROR: Import failed
ERROR: `path` does not exist: ‘nonexistent.xlsx’
ERROR: Skipping subsequent processing
> if (result$status != "success") {
+ message("Processing halted due to import issues")
+ } else {
+ message("Continue with further processing...")
+ }
Processing halted due to import issues
4. Advanced Logging Techniques
| Capability | SAS | R |
|---|---|---|
| Log destination | PROC PRINTTO LOG="file.log" |
sink("file.log") |
| Timestamp log entries | %PUT %SYSFUNC(DATETIME(), DATETIME.) |
message(paste(Sys.time(), "message")) |
| Log levels | Custom implementation | logger or futile.logger packages |
| Custom message formats | Macro implementation | Custom functions |
| Log file rotation | OS-level or custom macros | rotatelog or log4r packages |
SAS Example
/* Create a custom logging macro */
%macro log(level, message);
%let timestamp = %sysfunc(datetime(), datetime20.);
%let level = %upcase(&level);
/* Standardized format for all log messages */
%put &level [×tamp]: &message;
/* Write to external log file as well */
%if %sysfunc(fileexist(mylog.txt)) %then %do;
filename logfile "mylog.txt" mod;
%end;
%else %do;
filename logfile "mylog.txt";
%end;
data _null_;
file logfile;
put "&level [×tamp]: &message";
run;
filename logfile clear;
%mend;
/* Using the custom log macro */
%log(INFO, Starting data processing);
data processed;
set sashelp.class;
if age < 12 then do;
%log(WARNING, Found subjects under 12);
end;
if age > 16 then delete;
run;
%log(INFO, Processing complete);
Explanation (SAS):
- Custom
%logmacro provides standardized logging format - Timestamp added to each message for tracking
- Messages written to both SAS log and external file
- Consistent format makes logs easier to parse and analyze
- Log levels (INFO, WARNING) help categorize messages
- External log file persists after the SAS session ends
R Example
# No package needed for base logging
# Show input data
data <- data.frame(age = c(10, 15, 17, 8, 20))
print(data)
# Create a basic custom logging system
create_logger <- function(log_file = NULL) {
# Initialize log file if provided
if (!is.null(log_file)) {
# Create or truncate the log file
cat("", file = log_file)
}
# Return logger functions
list(
info = function(msg) {
timestamp <- format(Sys.time(), "%Y-%m-%d %H:%M:%S")
formatted <- sprintf("INFO [%s]: %s", timestamp, msg)
message(formatted)
if (!is.null(log_file)) {
cat(formatted, "\n", file = log_file, append = TRUE)
}
},
warning = function(msg) {
timestamp <- format(Sys.time(), "%Y-%m-%d %H:%M:%S")
formatted <- sprintf("WARNING [%s]: %s", timestamp, msg)
warning(formatted)
if (!is.null(log_file)) {
cat(formatted, "\n", file = log_file, append = TRUE)
}
},
error = function(msg) {
timestamp <- format(Sys.time(), "%Y-%m-%d %H:%M:%S")
formatted <- sprintf("ERROR [%s]: %s", timestamp, msg)
if (!is.null(log_file)) {
cat(formatted, "\n", file = log_file, append = TRUE)
}
stop(formatted, call. = FALSE)
}
)
}
# Create and use a logger
log <- create_logger("mylog.txt")
log$info("Starting data processing")
# Process data with logging
process_data <- function(data, log) {
log$info(paste("Processing", nrow(data), "records"))
young_subjects <- sum(data$age < 12)
if (young_subjects > 0) {
log$warning(paste("Found", young_subjects, "subjects under 12"))
}
processed <- data[data$age <= 16, ]
log$info(paste("Retained", nrow(processed), "records after filtering"))
return(processed)
}
# Run with logging
processed <- process_data(data, log)
log$info("Processing complete")
Explanation (R):
- Creates a custom logging system using a closure
- Logger provides consistent formatting with timestamps
- Different logging levels (info, warning, error)
- Writes to both console and external log file
- Returns functions that are used throughout the code
- Similar to professional logging packages but simplified
Expected Output in mylog.txt:
> processed <- process_data(data, log)
INFO [2025-07-26 04:49:07]: Processing 5 records
INFO [2025-07-26 04:49:07]: Retained records after filtering
Warning message:
In log$warning(paste("Found", young_subjects, "subjects under 12")) :
WARNING [2025-07-26 04:49:07]: Found 2 subjects under 12
>
> log$info("Processing complete")
INFO [2025-07-26 04:49:07]: Processing complete
5. Beyond Basics: Structured Logging with Packages
| Capability | SAS | R |
|---|---|---|
| Logging libraries | Enterprise Guide logging facility | logger, futile.logger, log4r |
| Log levels | Custom implementation | DEBUG, INFO, WARN, ERROR |
| Logging configurations | Custom implementation | Config files, runtime settings |
| Contextual information | _ALL_ automatic vars |
Environment details, tracebacks |
| Conditional logging | %IF conditions |
Logger thresholds |
SAS Macro-based Logging Framework Example
/* SAS Advanced Logging Framework */
%macro init_logger(log_level=INFO, log_file=);
%global log_level log_file log_level_num;
%let log_level = %upcase(&log_level);
%let log_file = &log_file;
/* Set numeric log level for threshold comparison */
%if &log_level = DEBUG %then %let log_level_num = 1;
%else %if &log_level = INFO %then %let log_level_num = 2;
%else %if &log_level = WARNING %then %let log_level_num = 3;
%else %if &log_level = ERROR %then %let log_level_num = 4;
%else %if &log_level = FATAL %then %let log_level_num = 5;
%else %let log_level_num = 2; /* Default to INFO */
/* Initialize log file if provided */
%if %length(&log_file) > 0 %then %do;
data _null_;
file "&log_file";
put "# Log initialized at %sysfunc(datetime(), datetime20.) with level &log_level";
run;
%end;
%put NOTE: Logger initialized with level &log_level;
%mend;
/* Logger function with threshold level */
%macro log(level, message);
%local level_num timestamp;
%let level = %upcase(&level);
/* Set level number for comparison */
%if &level = DEBUG %then %let level_num = 1;
%else %if &level = INFO %then %let level_num = 2;
%else %if &level = WARNING %then %let level_num = 3;
%else %if &level = ERROR %then %let level_num = 4;
%else %if &level = FATAL %then %let level_num = 5;
%else %let level_num = 2; /* Default to INFO */
/* Only log if at or above threshold */
%if &level_num >= &log_level_num %then %do;
%let timestamp = %sysfunc(datetime(), datetime20.);
/* Log to SAS log */
%if &level = WARNING %then %put WARNING [×tamp]: &message;
%else %if &level = ERROR %then %put ERROR [×tamp]: &message;
%else %if &level = FATAL %then %put ERROR [×tamp]: (FATAL) &message;
%else %put NOTE: &level [×tamp]: &message;
/* Log to file if provided */
%if %length(&log_file) > 0 %then %do;
data _null_;
file "&log_file" mod;
put "&level [×tamp]: &message";
run;
%end;
%end;
%mend;
Explanation (SAS Advanced Logging):
- Creates a robust logging framework with threshold levels
- Only logs messages at or above the configured threshold
- Consistent formatting across all message types
- Configurable output to SAS log and/or external file
- Log levels follow industry standards (DEBUG, INFO, WARNING, etc.)
- Macro variables provide global configuration
R Package-based Logging Example
# Install the 'logger' package if not already installed
# install.packages("logger")
library(logger)
# Configure logger threshold (INFO and above)
log_threshold(INFO)
# Use a simple layout without caller info
log_layout(layout_glue_generator(
format = "{level} [{time}] {msg}"
))
# Log to both console and file
log_appender(appender_tee("application.log"))
# Basic log messages
log_info("Starting data import process")
# Function to import data safely
import_data <- function(file_path) {
log_info("Attempting to import file: {file_path}")
if (!file.exists(file_path)) {
log_error("File not found: {file_path}")
return(NULL)
}
tryCatch({
data <- read.csv(file_path)
log_info("Successfully imported {nrow(data)} rows from {file_path}")
return(data)
}, error = function(e) {
log_error("Import failed: {conditionMessage(e)}")
return(NULL)
}, warning = function(w) {
log_warn("Import warning: {conditionMessage(w)}")
# Try importing anyway
data <- read.csv(file_path)
return(data)
})
}
# Simulate a module execution
log_info("Starting module: data_processing")
result <- import_data("mydata.csv")
if (is.null(result)) {
log_error("Processing failed due to import errors")
} else {
log_info("Processing completed successfully")
}
Explanation (R Advanced Logging):
- Uses the
loggerpackage for professional logging capabilities - Configures log threshold level, layout, and multiple appenders
- Includes structured contextual information with messages
- Consistent formatting across all code
- Log layout is highly customizable
- Multiple appenders allow output to console, files, databases, etc.
- Can be configured in a central location and used throughout the application
Expected Output in application.log:
> # Simulate a module execution
> log_info("Starting module: data_processing")
INFO [2025-07-26 04:55:29.635103] Starting module: data_processing
> result <- import_data("mydata.csv")
INFO [2025-07-26 04:55:29.637604] Attempting to import file: mydata.csv
ERROR [2025-07-26 04:55:29.640514] File not found: mydata.csv
>
> if (is.null(result)) {
+ log_error("Processing failed due to import errors")
+ } else {
+ log_info("Processing completed successfully")
+ }
ERROR [2025-07-26 04:55:29.645951] Processing failed due to import errors
6. Profiling and Debugging
| Capability | SAS | R |
|---|---|---|
| Timing code | options fullstimer; |
system.time(), tictoc package |
| Memory usage | System options | pryr::mem_used(), gc() |
| Execution tracing | options mlogic mprint symbolgen; |
trace(), debug() |
| Interactive debugging | Enterprise Guide debugging | browser(), RStudio debugging |
| Performance logging | Performance stats in log | profvis package |
SAS Example
/* Enable detailed timing */
options fullstimer;
/* Enable macro debugging */
options mlogic mprint symbolgen;
/* Performance tracking */
%macro perf_log(step);
%put NOTE: Starting step "&step" at %sysfunc(datetime(), datetime.);
%global _timer_&step;
%let _timer_&step = %sysfunc(datetime());
%mend;
%macro perf_end(step);
%local end_time duration;
%let end_time = %sysfunc(datetime());
%let duration = %sysevalf(&end_time - &&_timer_&step);
%put NOTE: Completed step "&step" in &duration seconds;
%mend;
/* Use performance tracking */
%perf_log(data_import);
data work.imported;
set sashelp.class;
run;
%perf_end(data_import);
%perf_log(data_processing);
proc sort data=work.imported;
by age;
run;
%perf_end(data_processing);
/* Reset options */
options nomlogic nomprint nosymbolgen;
Explanation (SAS Performance Tracking):
fullstimeroption provides detailed execution time information- Macro debugging options help trace execution flow
- Custom macros track timing for specific code sections
- SAS log includes automatic performance statistics
- Provides visibility into resource usage and bottlenecks
R Example
# Basic timing with system.time
system.time({
# Code to time
Sys.sleep(1)
})
# More flexible timing with tictoc
library(tictoc)
library(pryr)
# Performance tracking function
perf_track <- function(description, expr) {
message(paste("Starting", description, "at", format(Sys.time())))
tic(description)
result <- eval(expr)
toc(log = TRUE)
return(result)
}
# Memory usage before operation
mem_start <- mem_used()
message(paste("Memory before:", format(mem_start, units = "auto")))
# Track performance of data operations
imported <- perf_track("data_import", {
# Simulating data import
Sys.sleep(1)
data <- datasets::iris
data
})
processed <- perf_track("data_processing", {
# Simulating data processing
Sys.sleep(0.5)
processed <- imported[order(imported$Sepal.Length), ]
processed
})
# Memory usage after operations
mem_end <- mem_used()
message(paste("Memory after:", format(mem_end, units = "auto")))
message(paste("Memory change:", format(mem_end - mem_start, units = "auto")))
# Get timing log
timing_log <- tic.log(format = TRUE)
print(timing_log)
# Reset timer log
tic.clearlog()
# Advanced profiling with profvis
library(profvis)
profvis({
# Code to profile
data <- datasets::iris
result <- lapply(1:10, function(i) {
Sys.sleep(0.1)
mean(data$Sepal.Length)
})
})
Explanation (R Performance Tracking):
system.time()provides basic execution timingtictocpackage offers flexible timing with nested operationsmem_used()tracks memory consumption- Custom functions combine timing and messaging
profvisprovides interactive visualization of performance- Helps identify bottlenecks and resource-intensive operations
- RStudio integration for debugging and profiling
Expected R Console Output:
Starting data_import at 2025-01-01 17:30:45
data_import: 1.01 sec elapsed
Starting data_processing at 2025-01-01 17:30:46
data_processing: 0.51 sec elapsed
Memory before: 50.5 MB
Memory after: 51.2 MB
Memory change: 700 KB
[1] "data_import: 1.01 sec elapsed"
[2] "data_processing: 0.51 sec elapsed"
7. Best Practices for Logging
Be Consistent
- Use standard log levels (DEBUG, INFO, WARNING, ERROR)
- Format messages consistently for easier reading and parsing
- Include timestamps for troubleshooting timing issues
Log Appropriate Information
- Include context data like record counts and parameters
- Avoid logging sensitive information (PII, passwords)
- Balance verbosity against performance impacts
Handle Errors Gracefully
- Log error details for easier troubleshooting
- Include error locations (file, line number, function)
- Consider user-friendly error messages vs. technical details
Configure Log Levels
- Use DEBUG during development
- Use INFO or WARNING in production
- Allow runtime configuration when possible
Log Management
- Implement log rotation for long-running applications
- Consider log file size limits
- Archive logs for compliance where needed
Structured Logging
- Use structured formats (e.g., JSON) for machine-readable logs
- Include contextual information (user, session, process)
- Enable easy filtering and search
Performance Considerations
- Use conditional logging for high-volume messages
- Buffer log messages when appropriate
- Consider asynchronous logging for performance-critical code
**Resource download links**
1.4.9.-Log-Messages-SAS-Log-vs-R-Console.zip