How the Xray-core Logging System Works: Configuring Structured Logging
Xray-core employs a lightweight Message → Handler pipeline where all log entries implement the Message interface, with structured data carried in specific concrete types like AccessMessage and DNSLog, configurable via JSON/TOML/YAML sections that map to protobuf-based runtime settings.
The XTLS/Xray-core repository implements a sophisticated yet extensible logging framework designed to handle high-throughput proxy traffic with structured context. Understanding how this Xray-core logging system operates enables you to capture detailed access records, debug DNS resolution, and eliminate noise through severity-based filtering. This guide examines the internal architecture and provides concrete configuration examples for implementing structured logging in production environments.
Core Components of the Xray-core Logging System
The logging framework centers on three abstractions defined in common/log/log.go: the Message interface, the Handler interface, and concrete logger implementations that manage buffering and output.
Message Interface and Structured Types
Every log entry in Xray-core implements the Message interface, which requires a String() method for text rendering. The system provides three primary concrete types that carry structured fields beyond simple text:
GeneralMessage– Standard diagnostic logs with severity levels, defined incommon/log/log.go(lines 19-27)AccessMessage– Connection metadata including source IP, destination, status (Accepted/Rejected), email, and detour, defined incommon/log/access.go(lines 23-31)DNSLog– DNS query details including domain, answer IPs, and resolution latency, defined incommon/log/dns.go(lines 9-16)
These types encapsulate rich context that remains available programmatically even when rendered as human-readable strings via their String() implementations.
Handler and Logger Architecture
The Handler interface in common/log/logger.go (lines 14-22) defines a single method Handle(msg Message) that consumes log entries. The architecture stacks multiple layers to provide buffering, filtering, and routing:
- General Logger – Created by
log.NewLogger(WriterCreator)incommon/log/logger.go(lines 35-44), buffers messages and writes through concrete stdout or file Writers - Severity Logger – Wraps a general logger and filters
GeneralMessageinstances based on configured severity thresholds, implemented incommon/log/logger.go(lines 45-69) - Log Instance – The concrete Xray logger in
app/log/log.go(lines 16-42) implementslog.Handlerand routes incoming messages to access or error sub-loggers based on runtime configuration
Message Flow Through the Logging Pipeline
When application code emits a log entry, the message traverses a specific pipeline from emission to persistence:
- Emission – Any component calls
log.Record(msg)or uses helper functions fromcommon/errors - Global Handler –
log.Recordforwards to the globally registeredlogHandler, which is theapp/log.Instancecreated at startup (see theNewfunction inapp/log/log.go) - Type-Based Routing –
Instance.Handleexamines the concrete Message type:*log.AccessMessageroutes to the access logger (file or console as configured)*log.DNSLogroutes to the access logger only whenEnableDnsLogis true in the configuration*log.GeneralMessagefilters byErrorLogLeveland routes to the error logger
- Output – Sub-loggers execute write operations through handlers created by
app/log/log_creator.govia thehandlerCreatorMap
This design separates the emission of structured data from its eventual formatting, enabling you to swap output formats without modifying the instrumentation code.
Configuring Structured Logging in Xray-core
Configuration flows from JSON/TOML/YAML files through protocol buffer definitions to runtime logger initialization. The Log Instance reads the log section and constructs an app/log.Config protobuf message to build the runtime logger accordingly.
JSON Configuration Fields
The logging configuration accepts the following fields mapped in infra/conf/log.go and defined in app/log/config.pb.go (lines 25-85):
| Field | Description | Protobuf Mapping |
|---|---|---|
access |
Path to access log file; use "none" to disable |
AccessLogPath / AccessLogType |
error |
Path to error log file; use "none" to disable |
ErrorLogPath / ErrorLogType |
loglevel |
Minimum severity for general logs: debug, info, warning, error |
ErrorLogLevel |
dnsLog |
Boolean to enable DNS query logging to the access log | EnableDnsLog |
maskAddress |
IP masking mode: full, half, quarter, or custom CIDR |
MaskAddress |
Minimal configuration example:
{
"log": {
"access": "access.log",
"error": "error.log",
"loglevel": "warning",
"dnsLog": true,
"maskAddress": "full"
}
}
Command Line Overrides
For runtime adjustments without modifying configuration files, use CLI flags defined in main/commands/all/api/logger_restart.go:
# Start with custom config
xray -c my-config.json
# Override specific fields
xray -c '' -log.level warning -log.dnsLog true
These flags re-initialize the logger while Xray runs, enabling dynamic log level adjustments without service restarts.
Programmatic Usage
Within custom modules or plugins, import the logging package and construct structured messages:
import (
clog "github.com/xtls/xray-core/common/log"
"net"
"time"
)
// Emit structured access log
accessMsg := &clog.AccessMessage{
From: clientIP,
To: targetIP,
Status: clog.AccessAccepted,
Reason: "policy ok",
Email: "user@example.com",
}
clog.Record(accessMsg)
// Emit structured DNS log
dnsMsg := &clog.DNSLog{
Domain: "example.com",
Answer: []net.IP{net.ParseIP("93.184.216.34")},
Duration: time.Millisecond * 42,
}
clog.Record(dnsMsg)
Because the system operates on the Message interface, you can implement custom Handler types (such as JSON exporters or remote aggregators) by registering new HandlerCreator functions in app/log/log_creator.go without modifying existing emission code.
Summary
- Xray-core implements a Message → Handler pipeline where all log entries implement the
Messageinterface with aString()method - Structured data lives in concrete types:
AccessMessage(connection metadata),DNSLog(resolution details), andGeneralMessage(diagnostics) - The Log Instance in
app/log/log.goroutes messages to access or error sub-loggers based on type and configuration - Configure via JSON/TOML/YAML using fields:
access,error,loglevel,dnsLog, andmaskAddress, which map to protobuf definitions inapp/log/config.pb.go - Programmatic emission uses
clog.Record(msg)with structured message types defined incommon/log/access.goandcommon/log/dns.go - Custom output formats require implementing the Handler interface and registering via
handlerCreatorMapinapp/log/log_creator.go
Frequently Asked Questions
What log levels are supported in Xray-core?
Xray-core supports four severity levels for general logging: debug, info, warning, and error. These map to protobuf constants in app/log/config.pb.go (Severity_Debug through Severity_Error). The loglevel configuration field sets the minimum threshold; messages below this level are filtered by the severityLogger wrapper in common/log/logger.go and never reach the output Writer.
How do I disable access logs while keeping error logs?
Set the access field to "none" in your configuration while specifying a path for error:
{
"log": {
"access": "none",
"error": "/var/log/xray/error.log",
"loglevel": "warning"
}
}
This maps to AccessLogType = None in the protobuf configuration, causing the app/log/log.go router to discard AccessMessage and DNSLog instances rather than forwarding them to a Writer, while GeneralMessage instances continue to flow to the error logger.
Can I output logs in JSON format?
Xray-core does not include a built-in JSON formatter in the current source tree. However, because the logging system operates on the Message interface, you can implement structured JSON output by creating a custom Handler that implements Handle(msg Message) and serializes the structured fields of AccessMessage (source, destination, status) or DNSLog (domain, answers) rather than calling msg.String(). Register your handler via the handlerCreatorMap in app/log/log_creator.go.
How does IP masking work in Xray-core logs?
The maskAddress field supports four modes defined in the configuration: full (masks entire IP), half (masks last two octets of IPv4 or last 8 bytes of IPv6), quarter (masks last octet of IPv4 or last 4 bytes of IPv6), or custom CIDR notation like "192.0.2.0/24". This setting is processed according to app/log/config.pb.go and applied when stringifying AccessMessage instances, allowing privacy-compliant logging in production environments while retaining enough information for debugging.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →