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 in common/log/log.go (lines 19-27)
  • AccessMessage – Connection metadata including source IP, destination, status (Accepted/Rejected), email, and detour, defined in common/log/access.go (lines 23-31)
  • DNSLog – DNS query details including domain, answer IPs, and resolution latency, defined in common/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:

  1. General Logger – Created by log.NewLogger(WriterCreator) in common/log/logger.go (lines 35-44), buffers messages and writes through concrete stdout or file Writers
  2. Severity Logger – Wraps a general logger and filters GeneralMessage instances based on configured severity thresholds, implemented in common/log/logger.go (lines 45-69)
  3. Log Instance – The concrete Xray logger in app/log/log.go (lines 16-42) implements log.Handler and 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:

  1. Emission – Any component calls log.Record(msg) or uses helper functions from common/errors
  2. Global Handler – log.Record forwards to the globally registered logHandler, which is the app/log.Instance created at startup (see the New function in app/log/log.go)
  3. Type-Based Routing – Instance.Handle examines the concrete Message type:
    • *log.AccessMessage routes to the access logger (file or console as configured)
    • *log.DNSLog routes to the access logger only when EnableDnsLog is true in the configuration
    • *log.GeneralMessage filters by ErrorLogLevel and routes to the error logger
  4. Output – Sub-loggers execute write operations through handlers created by app/log/log_creator.go via the handlerCreatorMap

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 Message interface with a String() method
  • Structured data lives in concrete types: AccessMessage (connection metadata), DNSLog (resolution details), and GeneralMessage (diagnostics)
  • The Log Instance in app/log/log.go routes messages to access or error sub-loggers based on type and configuration
  • Configure via JSON/TOML/YAML using fields: access, error, loglevel, dnsLog, and maskAddress, which map to protobuf definitions in app/log/config.pb.go
  • Programmatic emission uses clog.Record(msg) with structured message types defined in common/log/access.go and common/log/dns.go
  • Custom output formats require implementing the Handler interface and registering via handlerCreatorMap in app/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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →