Logback Pattern Layout: Configure Custom Log Message Formats

Published:

Ever spent 20 minutes scrolling through generic log files looking for one specific error? Default log formats are useless when you’re debugging under pressure. PatternLayout in Logback lets you control exactly what shows up in each log line, from timestamps and thread names to custom context fields and stack traces. This guide walks through the conversion specifiers, format modifiers, and XML configuration you need to build log formats that actually help you ship faster and debug smarter.

Practical Pattern Syntax and Common Specifiers

W5JrK01eQK-_BYFj0s0k_w

PatternLayout is Logback’s go-to tool for formatting log messages. It uses conversion specifiers that start with the percent sign to control how each piece of your log shows up in the output.

Pattern: %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n

Output: 14:23:47.891 [main] INFO  c.e.application.UserService - User login successful

Conversion specifiers kick off with % and grab different elements from your logging event. Could be the timestamp, thread name, message content, whatever you need. You configure these patterns in logback.xml within encoder or layout elements, stringing together multiple specifiers to build your complete log format.

Conversion Specifier Purpose Example Output
%d Timestamp with default format 2024-01-15 14:23:47,891
%d{HH:mm:ss.SSS} Timestamp with custom pattern 14:23:47.891
%thread Thread name main
%level Log level INFO
%-5level Log level padded to 5 characters INFO
%logger Full logger name com.example.application.UserService
%logger{36} Logger name truncated to 35 chars c.e.application.UserService
%msg Log message User login successful
%n Platform-specific line separator (newline)
%class Class name where log was called UserService
%method Method name where log was called authenticateUser
%line Line number where log was called 47

You can stack specifiers together to create complete log formats, and each one accepts additional formatting tweaks through modifiers. The %d specifier takes SimpleDateFormat patterns inside curly braces to dial in timestamp precision. %logger uses a length parameter to shorten package names from the left. So “com.example.service.UserService” becomes “c.e.s.UserService” when you set it to a shorter length. Format modifiers like the minus sign in %-5level control alignment and padding, keeping your log levels lined up vertically in console output.

Configuring Pattern Layout in XML Files

iF70vA_SQXWzMOOdTHdfMg

Your logback.xml configuration file goes in the src/main/resources directory. PatternLayout slots into the XML structure under the appender definition, nested inside either an encoder or layout element. The encoder wraps the pattern and converts formatted text into bytes before writing to wherever it’s going.

Logback searches for configuration files in a specific order. It checks logback-test.xml first, then logback.xml. If neither exists, you get console logging at DEBUG level with a basic pattern.

<configuration>
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="CONSOLE" />
    </root>
</configuration>

FileAppender configuration uses identical pattern syntax. Main difference is you add file path and append settings. Same pattern element sits inside the encoder, and the pattern conversion specifiers work exactly the same way across all appender types. Console, files, rolling files, doesn’t matter. The pattern formatting stays consistent.

<configuration>
    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
        <file>application.log</file>
        <append>true</append>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="DEBUG">
        <appender-ref ref="FILE" />
    </root>
</configuration>

Comprehensive Conversion Specifier Reference

VGD6hQchQCOk21hpI30CWg

Logback gives you conversion specifiers for timestamp formatting, logger and class information extraction, exception handling, and contextual data display.

Date and Timestamp Conversion Specifiers

The %d conversion specifier outputs timestamps and accepts SimpleDateFormat patterns inside curly braces to control the exact format. Without any pattern specified, %d uses the default format “yyyy-MM-dd HH:mm:ss,SSS” which includes date, time, and milliseconds.

  • %d{ISO8601} outputs 2024-01-15T14:23:47,891+0000
  • %d{HH:mm:ss.SSS} outputs 14:23:47.891
  • %d{yyyy-MM-dd HH:mm:ss} outputs 2024-01-15 14:23:47
  • %d{dd MMM yyyy HH:mm:ss} outputs 15 Jan 2024 14:23:47
  • %d{HH:mm:ss,SSS} outputs 14:23:47,891
  • %d{yyyy-MM-dd} outputs 2024-01-15
  • %d{HH:mm} outputs 14:23
  • %d{yyyyMMdd’T’HHmmss} outputs 20240115T142347

The %relative conversion word shows milliseconds elapsed since application start. Pretty handy for tracking performance and timing between log events. You can specify timezone using %d{HH:mm:ss.SSS, UTC} or any valid timezone identifier.

Logger and Class Information Specifiers

The %logger conversion word outputs the logger name, which typically matches the fully qualified class name where the logger was instantiated. For a logger created in “com.example.service.UserService”, %logger outputs the complete package and class name.

Precision specifiers control logger name length using %logger{length} syntax. The number indicates how many characters to preserve from the right side of the name. The truncation algorithm shortens package names from left to right. So %logger{36} applied to “com.example.application.service.UserService” might produce “c.e.a.service.UserService”, shortening early package segments while keeping the class name intact.

The %class, %method, and %line specifiers provide detailed caller information, extracting the exact class, method, and line number where the logging statement executed. These are incredibly useful during debugging but they’re expensive. Logback has to walk the stack trace to extract caller data. Avoid using these in production high-throughput systems unless the debugging value justifies the overhead.

Exception and Stack Trace Formatting

Exception logging needs special handling to capture stack traces and error details. That’s what the %ex conversion word does. When a Throwable is passed to a logging method, %ex formats the complete exception message and stack trace.

  • %ex gives you full exception stack trace
  • %exception is identical to %ex
  • %throwable works the same as %ex and %exception
  • %xEx gives you enhanced exception format with packaging data
  • %rootException shows only the root cause exception
  • %nopex suppresses exception printing

Stack trace depth control is available using %ex{short}, %ex{full}, or %ex{n} where n is the number of stack trace lines to include. The syntax %ex{5} outputs only the first 5 lines of the stack trace, cutting down log verbosity while still capturing the immediate error context.

The %xEx enhanced exception format adds packaging data that shows which JAR files contain the classes in the stack trace. This helps identify version conflicts and dependency issues. For example, %xEx might output “at com.example.UserService.login(UserService.java:47) [application-1.0.jar:1.0]” showing both the source location and the JAR file.

Format Modifiers and Pattern Layout Customization

wjr-884PQC6DCI06GGd5fQ

Format modifiers are optional parameters that control output formatting, particularly width, padding, and alignment of each field. These modifiers sit between the percent sign and the conversion word, giving you precise control over how log lines align.

Minimum width specifications use a number immediately after the percent sign. The pattern %20logger means the logger name will occupy at least 20 characters, padding with spaces if the actual name is shorter. This creates columns in log output. If the logger name “UserService” appears with %20logger, it gets left-padded with spaces to reach 20 characters total width.

Maximum width and truncation use curly braces with precision specifiers. The pattern %logger{35} limits the logger name to 35 characters, truncating from the left side if the full name exceeds that length. This works by shortening package names while preserving the class name. You can combine minimum and maximum width like %20.35logger, which enforces both a minimum field width of 20 characters and a maximum of 35 characters.

Left versus right justification gets controlled using the minus sign modifier. By default, fields are right-justified with padding added to the left. The pattern %-5level adds the minus sign to force left justification, so “INFO” becomes “INFO ” instead of ” INFO” when padded to 5 characters. This is why %-5level is the standard pattern for log levels. It keeps them aligned and left-justified for easier visual scanning.

Thread Context and MDC Integration with Pattern Layout

hIHYiZ5FSv-By39BgfmH0A

The %thread conversion specifier displays the name of the thread that generated the log event. In single-threaded applications this might always show “main”, but in multi-threaded or async systems it reveals which thread handled each request, like “http-nio-8080-exec-5” or “kafka-consumer-thread-1”.

Pattern Syntax Output Behavior Use Case
%mdc All MDC key-value pairs Debug all context at once
%mdc{requestId} Specific MDC value for “requestId” Track single request through system
%mdc{userId:-anonymous} MDC value with default if missing Show userId or “anonymous” when not set
%X{correlationId} Equivalent to %mdc{correlationId} Shorter syntax for MDC access
%X All MDC values (same as %mdc) Quick display of full context map

MDC (Mapped Diagnostic Context) becomes critical in microservices and distributed systems where a single user request might flow through multiple services, threads, and async processes. By storing a correlation ID in MDC at the entry point and including %mdc{correlationId} in your pattern, every log statement automatically shows which original request it belongs to.

This transforms scattered logs into traceable request flows without manually passing IDs through every method call. The pattern %d{HH:mm:ss.SSS} [%thread] [%mdc{requestId}] %-5level %logger{36} – %msg%n produces output like “14:23:47.891 [http-exec-5] [req-abc-123] INFO c.e.UserService – Processing login”, making request tracking trivial.

The %X specifier is just a shorter alias for %mdc, so %X{userId} and %mdc{userId} produce identical output.

Colored Console Output with Pattern Layout

i1j1VCglRb2eJ8ZcdkhGNg

Color support in PatternLayout works only with ConsoleAppender and needs a terminal that supports ANSI escape codes. Most modern terminals and IDEs handle ANSI codes correctly, displaying different log levels in distinct colors that make visual scanning much faster during development.

The %highlight conversion word automatically assigns colors based on log level. Wrapping your pattern in %highlight() applies these defaults: ERROR logs appear in red, WARN in yellow, INFO in blue, DEBUG in cyan, and TRACE in gray. The syntax looks like %highlight(%msg) where the message content gets colored according to its severity level.

Custom colorization uses the %color() specifier with explicit color names. You can specify colors like %red(), %green(), %yellow(), %blue(), %magenta(), %cyan(), or %white() to wrap any part of your pattern. For example, %green(%d{HH:mm:ss.SSS}) makes the timestamp green regardless of log level. Color names can also be combined with ANSI codes directly, though named colors are more readable and portable across different terminals.

<configuration>
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%green(%d{HH:mm:ss.SSS}) [%blue(%thread)] %highlight(%-5level) %cyan(%logger{36}) - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="DEBUG">
        <appender-ref ref="CONSOLE" />
    </root>
</configuration>

Practical Pattern Layout Examples for Common Use Cases

T19fhpkVRYqNEuTgaceR0g

Different logging scenarios need different patterns, balancing verbosity against readability and performance.

Use Case Pattern Example Key Features
Basic Development %d{HH:mm:ss.SSS} %-5level %logger{36} – %msg%n Simple timestamp, level, logger, message
Production %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} – %msg%n Full date, thread name for multi-threaded debugging
Microservices %d{HH:mm:ss.SSS} [%thread] [%mdc{requestId}] %-5level %logger – %msg%n Request tracking with MDC correlation ID
Debugging %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} %class.%method:%line – %msg%n Caller information with class, method, line number
Minimal/Performance %d{HH:mm:ss} %-5level %msg%n Fast pattern with only essentials
Structured %d{ISO8601}|%level|%thread|%logger|%msg%n Pipe-delimited for parsing scripts
Web Application %d{HH:mm:ss.SSS} [%mdc{sessionId}] [%mdc{userId}] %-5level %logger – %msg%n Session and user tracking via MDC

Choosing the right pattern depends on whether you’re developing locally or running in production, and whether logs are read by humans or parsed by machines. Development patterns can be more verbose with color coding and detailed context. Production patterns balance detail against performance, avoiding expensive caller data extraction like %class, %method, and %line unless debugging a specific issue.

Simpler patterns with fewer conversion specifiers perform better under high logging volume because there’s less string formatting and data extraction happening on every log statement. When configuring production systems, think about how logs will be consumed. If they’re going to a centralized logging system for parsing, structured formats with delimiters might matter more than human readability. For more details on how patterns integrate with different output destinations, see our guide on logback appender configuration.

Advanced Pattern Layout Features and Custom Conversion

ycNt7rF8QV6UbH4Wev9Apw

PatternLayout is extensible through custom conversion words, letting you create specialized formatting that goes beyond Logback’s built-in specifiers. This becomes useful when you need domain-specific formatting, like masking sensitive data or extracting custom fields from log events.

Creating custom conversion words requires extending the ClassicConverter base class and implementing the convert() method. Your custom converter receives the logging event and returns a string that gets inserted at that position in the pattern. After implementing the converter class, register it in logback.xml using the element with a conversion word name and the fully qualified class name. Then use your custom word in patterns just like built-in specifiers.

Variable substitution in patterns uses ${propertyname} syntax to inject dynamic values into the pattern at configuration time, not at logging time. This lets you define a property once and reference it multiple times throughout your configuration. For example, defining lets you use ${LOG_PATTERN} in multiple appenders without repeating the pattern string.

Property sources include properties defined directly in logback.xml using tags, system properties set via -D flags when starting the JVM, and environment variables. System properties take precedence over properties defined in the configuration file. You can access environment variables using ${ENVVARNAME} syntax. This is particularly useful for containerized environments where configuration comes from environment variables rather than XML files, like using ${LOGLEVEL:-INFO} to read the LOGLEVEL environment variable with a fallback to INFO if it’s not set.

Pattern Layout Performance Considerations

ETLdke5mRRuWG5uliNcM3Q

Pattern complexity directly impacts logging throughput, especially in applications generating high volumes of log statements. Each conversion specifier adds processing overhead, and some specifiers cost way more than others.

  • Caller data (%class, %method, %line) is extremely expensive, requires stack trace walking
  • Exception formatting (%ex, %throwable) has moderate cost when exceptions are present
  • MDC access (%mdc) is low cost, simple map lookup
  • Date formatting (%d with complex patterns) is low to moderate depending on pattern complexity
  • Simple text and basic specifiers (%level, %msg, %n) have minimal cost

Optimization techniques include using asynchronous appenders to offload formatting to a separate thread, keeping patterns simple in high-throughput code paths, and avoiding caller data specifiers in production unless actively debugging. The %class, %method, and %line specifiers are particularly expensive because Logback must examine the entire call stack to determine where the log statement originated.

In performance-critical applications, a pattern like %d{HH:mm:ss.SSS} %-5level %msg%n sacrifices some debugging context for speed.

Measuring logging overhead involves running the application with logging disabled, then comparing throughput with logging enabled using different pattern complexities. The difference shows you the actual cost of your logging configuration. If logging reduces throughput by more than a few percent, consider simplifying patterns or moving to async appenders.

Integrating Pattern Layout with Groovy Configuration

qy6zNBrtRmuG9yKZ9g1uyw

Logback supports Groovy-based configuration through logback.groovy files as an alternative to XML, offering more programmatic flexibility and less verbose syntax. The Groovy DSL (Domain Specific Language) provides the same configuration capabilities with a more developer-friendly structure.

import ch.qos.logback.classic.encoder.PatternLayoutEncoder
import ch.qos.logback.core.ConsoleAppender

appender("CONSOLE", ConsoleAppender) {
    encoder(PatternLayoutEncoder) {
        pattern = "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
    }
}

root(INFO, ["CONSOLE"])

The XML versus Groovy decision comes down to team preference and configuration complexity. Groovy configuration shines when you need programmatic logic, like conditionally setting patterns based on environment variables or building configurations dynamically. XML works better for simple, static configurations and in teams unfamiliar with Groovy.

Pattern syntax remains identical between both formats. The %d, %level, and %msg specifiers work exactly the same way. Logback checks for logback.groovy before looking for logback.xml during application startup, so having both files means Groovy takes precedence.

Pattern Layout Special Characters and Escaping

The %n conversion word outputs the platform-specific line separator, automatically using \n on Unix/Linux/Mac systems and \r\n on Windows. This makes patterns portable across different operating systems without manual line ending adjustments.

Tab characters in patterns can be inserted using \t in the pattern string or by using the literal tab character. For example, %d{HH:mm:ss.SSS}\t%-5level\t%logger outputs tab-delimited fields useful for importing into spreadsheet tools.

Escaping literal percent signs requires using %% because a single % triggers conversion specifier parsing. To output “Progress: 50%” in a log message with pattern “%msg”, the logging code would include the percent sign in the message itself. But if you need a literal % in the pattern definition itself, use %%. Other special characters like curly braces, parentheses, and minus signs have special meaning in certain contexts but can usually appear literally outside conversion specifiers without escaping.

Pattern Layout in Spring Boot Applications

Spring Boot auto-configures Logback as the default logging framework and provides sensible pattern defaults. The default console pattern is “%clr(%d{${LOGDATEFORMATPATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOGLEVELPATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(—){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOGEXCEPTIONCONVERSION_WORD:-%wEx}” which includes colored output, timestamps, process ID, and formatted logger names.

Overriding patterns in application.properties is straightforward using the logging.pattern.console and logging.pattern.file properties. Setting logging.pattern.console=%d{HH:mm:ss.SSS} [%thread] %-5level %logger – %msg%n in application.properties replaces the default console pattern across your entire application without touching XML files. The same works for file output with logging.pattern.file when using Spring Boot’s logging.file.name or logging.file.path properties.

For more advanced customization, use logback-spring.xml instead of logback.xml. The -spring suffix enables Spring-specific features like the element that applies configuration conditionally based on active Spring profiles. You can define different patterns for different environments. Verbose patterns with caller data for development profiles and minimal patterns for production.

<springProfile name="dev">
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} %class.%method:%line - %msg%n</pattern>
        </encoder>
    </appender>
</springProfile>

<springProfile name="prod">
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %logger - %msg%n</pattern>
        </encoder>
    </appender>
</springProfile>

The relationship between Spring Boot, SLF4J, and Logback is transparent. Spring Boot includes both SLF4J API and Logback Classic as transitive dependencies, so no explicit configuration is needed in your pom.xml or build.gradle. For more details on customizing Spring Boot’s logging behavior beyond pattern configuration, check out our detailed guide on spring boot logging configuration.

Structured Logging and JSON Pattern Alternatives

PatternLayout produces human-readable text output where each log line is a formatted string. Structured logging using JSON produces machine-readable output where each log event becomes a JSON object with separate fields for timestamp, level, logger, message, and additional context. The difference matters when logs feed into centralized aggregation systems that parse and index log data.

The logstash-logback-encoder is the standard library for JSON format logging in Logback. Instead of using PatternLayout, you configure LoggingEventCompositeJsonEncoder with providers that extract specific fields. Each provider handles one aspect of the log event. Timestamp, logger name, level, message, MDC values, stack traces. The configuration looks completely different from PatternLayout because you’re defining a JSON structure instead of a text pattern.

<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
    <providers>
        <timestamp/>
        <loggerName/>
        <logLevel/>
        <message/>
        <mdc/>
        <stackTrace/>
    </providers>
</encoder>

JSON encoding is preferred over PatternLayout when logs go to systems like Elasticsearch, Splunk, AWS CloudWatch, or any log aggregator that indexes and searches log data. These systems parse JSON natively and let you query specific fields, like finding all ERROR level logs from a particular service where requestId matches a pattern.

PatternLayout text requires custom parsing rules to extract fields, while JSON provides structured data immediately. The tradeoff is readability. JSON logs are harder to scan visually during local development, which is why many teams use PatternLayout for development environments and JSON for production systems feeding centralized logging infrastructure.

Final Words

PatternLayout conversion specifiers give you precise control over how Logback formats your log output. The basic syntax starts with the % prefix, and you can combine multiple specifiers to build patterns that match your exact needs.

Whether you’re formatting timestamps with %d{HH:mm:ss.SSS}, aligning log levels with %-5level, or adding thread context with %mdc{requestId}, each logback pattern layout choice affects readability and performance.

Start with a simple pattern during development, then adjust based on what you actually need in production. Most apps do fine with timestamp, level, logger name, and message. Add caller data or detailed exception formatting only when the debugging value justifies the performance cost.

Pick patterns that help you ship faster and debug quicker.

FAQ

What is PatternLayout in Logback and how does it work?

PatternLayout in Logback is the primary formatting mechanism that controls how log messages appear in your output by using conversion specifiers prefixed with a percentage sign to define the structure and content of each log line.

How do I write a basic Logback pattern with timestamp, level, and message?

A basic Logback pattern like %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n includes timestamp with milliseconds, thread name in brackets, left-aligned log level, truncated logger name, and the message followed by a newline.

What are the most commonly used Logback conversion specifiers?

The most commonly used Logback conversion specifiers are %d for timestamp, %level for log level, %logger for logger name, %thread for thread name, %msg for the actual message, and %n for platform-specific newline.

Where do I configure PatternLayout in my Logback XML file?

PatternLayout is configured in your logback.xml file located in src/main/resources, nested within the <encoder> or <layout> element inside an appender configuration like ConsoleAppender or FileAppender.

How do I customize date and timestamp formats in Logback patterns?

You customize date and timestamp formats in Logback patterns using %d{pattern} syntax where pattern follows SimpleDateFormat conventions, such as %d{yyyy-MM-dd HH:mm:ss.SSS} for full date with milliseconds or %d{ISO8601} for standard format.

What does the %logger{length} syntax control in Logback patterns?

The %logger{length} syntax in Logback patterns controls logger name truncation by limiting the output to the specified number of characters, truncating from the left side of the fully qualified class name to keep the rightmost portion.

How do format modifiers like %-5level work in Logback patterns?

Format modifiers like %-5level in Logback patterns control padding and alignment, where the minus sign means left-justify and the number specifies minimum width, so %-5level pads log levels to 5 characters aligned left for clean column formatting.

What is the performance cost of using caller information specifiers?

The performance cost of using caller information specifiers like %class, %method, and %line in Logback patterns is significant because Logback must generate a stack trace snapshot for every log statement to extract this location data.

How do I include MDC context values in my log pattern?

You include MDC context values in your log pattern using %mdc{key} or the shorter alias %X{key} syntax, where key is the MDC key name, allowing you to output contextual information like correlation IDs or user sessions.

Can I add color to console logs using PatternLayout?

Yes, you can add color to console logs using PatternLayout by wrapping conversion specifiers with %highlight() for automatic level-based coloring or %color() for custom color specifications, though this only works with ConsoleAppender in ANSI-supporting terminals.

How do I handle exception stack traces in Logback patterns?

You handle exception stack traces in Logback patterns using %ex or %exception conversion words, with options like %ex{short} for abbreviated output, %ex{full} for complete traces, or %ex{5} to limit depth to 5 stack frames.

What pattern should I use for production versus development environments?

For production environments, use simpler patterns like %d{ISO8601} %-5level [%thread] %logger{36} - %msg%n for performance, while development benefits from more verbose patterns including caller data, MDC values, and colored output for easier debugging.

How do I escape special characters and output literal percent signs in patterns?

You escape literal percent signs in patterns by using double percent signs %%, while special characters like newlines should use %n instead of \n for platform portability, and tabs can be represented with %t.

Does Spring Boot change how I configure Logback patterns?

Spring Boot provides default Logback patterns that you can override using logging.pattern.console and logging.pattern.file properties in application.properties, or through a logback-spring.xml file for more advanced customization with Spring profile support.

When should I use JSON logging instead of PatternLayout?

You should use JSON logging instead of PatternLayout when shipping logs to centralized aggregation systems like ELK stack or Splunk, where structured machine-readable formats enable better parsing, searching, and analysis than text-based pattern output.

curtisharmon
Curtis has spent over two decades guiding hunters and anglers through the backcountry of Montana and Wyoming. His expertise in elk hunting and fly fishing has made him a sought-after voice in the outdoor community. Curtis combines traditional woodsmanship with modern techniques to help readers succeed in the field.

Related articles

Recent articles