# Hysteria Core Logging Options: EventLogger and TrafficLogger Explained

> Explore Hysteria core logging options EventLogger and TrafficLogger to monitor server activity. Implement Go interfaces for connection events and traffic statistics.

- Repository: [Aperture Internet Laboratory/hysteria](https://github.com/apernet/hysteria)
- Tags: deep-dive
- Published: 2026-05-13

---

**Hysteria core provides two independent logging hooks—**`EventLogger`** for connection events and **`TrafficLogger`** for per-stream traffic statistics—that you implement via Go interfaces in [`core/server/config.go`](https://github.com/apernet/hysteria/blob/main/core/server/config.go) to monitor and control server activity.**

The apernet/hysteria repository offers a high-performance proxy core that supports custom instrumentation through optional logging interfaces. When integrating the Hysteria core library into your own server implementation, you can tap into two distinct logging options to capture authentication events, proxy requests, and real-time traffic metrics without affecting the data flow.

## Core Logging Interfaces in Hysteria

Hysteria core defines two complementary interfaces in [`core/server/config.go`](https://github.com/apernet/hysteria/blob/main/core/server/config.go) that serve different observability needs. Both are optional; the server validates their presence with `if config.EventLogger != nil` and `if config.TrafficLogger != nil` checks before invoking any methods, as seen in [`core/server/server.go`](https://github.com/apernet/hysteria/blob/main/core/server/server.go) (lines 113-120 and 242-254).

### EventLogger: Connection-Level Events

The **EventLogger** interface (lines 218-226 in [`core/server/config.go`](https://github.com/apernet/hysteria/blob/main/core/server/config.go)) records high-level connection lifecycle events and protocol-specific requests. It is invoked once per event and does not participate in data forwarding decisions. This interface captures:

- Client authentication and disconnection
- TCP proxy requests and their failures
- UDP session creation and errors

The server also contains a thin UDP event wrapper in [`core/server/server.go`](https://github.com/apernet/hysteria/blob/main/core/server/server.go) (lines 401-415) that forwards UDP-specific calls to your `EventLogger` implementation, mapping `udpEventLoggerImpl.New` to `EventLogger.UDPRequest` and `Close` to `EventLogger.UDPError`.

### TrafficLogger: Per-Stream Statistics and Control

The **TrafficLogger** interface (lines 242-251 in [`core/server/config.go`](https://github.com/apernet/hysteria/blob/main/core/server/config.go)) tracks granular traffic data and can enforce disconnection policies. Unlike `EventLogger`, this interface can reject clients by returning `false` from its primary method. Key capabilities include:

- Byte counters for every read and write operation
- Online/offline state change notifications
- Stream lifecycle tracing for latency measurement

## Interface Methods Reference

### EventLogger Method Signatures

Implement these methods to capture connection events:

- **`Connect(addr, id, tx)`**: Called after successful client authentication. Receives the remote address, client identifier, and initial transmitted bytes.
- **`Disconnect(addr, id, err)`**: Called when the connection terminates, either normally or with an error.
- **`TCPRequest(addr, id, reqAddr)`**: Called when a client initiates a TCP proxy request to a target address.
- **`TCPError(addr, id, reqAddr, err)`**: Called if TCP request handling fails.
- **`UDPRequest(addr, id, sessionID, reqAddr)`**: Called when a new UDP session is created.
- **`UDPError(addr, id, sessionID, err)`**: Called when a UDP session ends with an error.

### TrafficLogger Method Signatures

Implement these methods to monitor and control traffic:

- **`LogTraffic(id, tx, rx) bool`**: Called after each read or write on a stream or UDP packet. The `tx` and `rx` parameters represent bytes sent to and received from the remote target. Return `false` to force the server to disconnect the client immediately.
- **`LogOnlineState(id, online)`**: Called when a client's presence state changes, useful for building real-time dashboards.
- **`TraceStream(stream, stats)`**: Called at the start of a TCP stream to attach custom tracing instrumentation.
- **`UntraceStream(stream)`**: Called when a stream ends to clean up tracing resources.

## Implementation Requirements

**Thread safety is mandatory.** Both loggers are invoked from multiple goroutines handling concurrent connections, so your implementations must synchronize access to shared state using mutexes or other concurrency primitives.

**Optional attachment.** You may provide one, both, or neither logger. The core server skips logging calls when the respective interface field is `nil`.

## Code Examples

### Minimal EventLogger Implementation

```go
package main

import (
	"fmt"
	"net"

	"hysteria/core/server"
)

type simpleEventLogger struct{}

func (l *simpleEventLogger) Connect(addr net.Addr, id string, tx uint64) {
	fmt.Printf("[CONNECT] %s (id=%s, tx=%d)\n", addr, id, tx)
}

func (l *simpleEventLogger) Disconnect(addr net.Addr, id string, err error) {
	fmt.Printf("[DISCONNECT] %s (id=%s, err=%v)\n", addr, id, err)
}

func (l *simpleEventLogger) TCPRequest(addr net.Addr, id, reqAddr string) {
	fmt.Printf("[TCP REQ] %s → %s (id=%s)\n", addr, reqAddr, id)
}

func (l *simpleEventLogger) TCPError(addr net.Addr, id, reqAddr string, err error) {
	fmt.Printf("[TCP ERR] %s → %s (id=%s, err=%v)\n", addr, reqAddr, id, err)
}

func (l *simpleEventLogger) UDPRequest(addr net.Addr, id string, sessionID uint32, reqAddr string) {
	fmt.Printf("[UDP REQ] %s (session=%d) → %s (id=%s)\n", addr, sessionID, reqAddr, id)
}

func (l *simpleEventLogger) UDPError(addr net.Addr, id string, sessionID uint32, err error) {
	fmt.Printf("[UDP ERR] %s (session=%d) (id=%s, err=%v)\n", addr, sessionID, id, err)
}

```

### Minimal TrafficLogger Implementation

```go
import (
	"fmt"
	"hysteria/core/internal/protocol"
	"hysteria/core/server"
)

type simpleTrafficLogger struct{}

func (l *simpleTrafficLogger) LogTraffic(id string, tx, rx uint64) bool {
	fmt.Printf("[TRAFFIC] %s tx=%d rx=%d\n", id, tx, rx)
	return true // Allow connection to continue
}

func (l *simpleTrafficLogger) LogOnlineState(id string, online bool) {
	state := "offline"
	if online {
		state = "online"
	}
	fmt.Printf("[STATE] %s is %s\n", id, state)
}

func (l *simpleTrafficLogger) TraceStream(stream protocol.HyStream, stats *protocol.StreamStats) {
	fmt.Printf("[STREAM START] ID=%d\n", stream.StreamID())
}

func (l *simpleTrafficLogger) UntraceStream(stream protocol.HyStream) {
	fmt.Printf("[STREAM END] ID=%d\n", stream.StreamID())
}

```

### Wiring Loggers into Server Configuration

```go
cfg := &server.Config{
	// ... other required fields: TLSConfig, QUICConfig, Conn, Authenticator
	EventLogger:   &simpleEventLogger{},
	TrafficLogger: &simpleTrafficLogger{},
}

srv, err := server.New(cfg)
if err != nil {
	// handle error
}
srv.Serve()

```

### Enforcing Bandwidth Limits with LogTraffic

Use the return value of `LogTraffic` to implement hard limits:

```go
const maxTx = 10 << 20 // 10 MiB per connection

func (l *simpleTrafficLogger) LogTraffic(id string, tx, rx uint64) bool {
	if tx > maxTx {
		fmt.Printf("[LIMIT] %s exceeded tx limit (%d > %d)\n", id, tx, maxTx)
		return false // Disconnect the client
	}
	return true
}

```

## Key Source Files and Architecture

Understanding the logging architecture requires familiarity with these specific files in the apernet/hysteria repository:

- **[`core/server/config.go`](https://github.com/apernet/hysteria/blob/main/core/server/config.go)** – Contains the `Config` struct and the `EventLogger` and `TrafficLogger` interface definitions.
- **[`core/server/server.go`](https://github.com/apernet/hysteria/blob/main/core/server/server.go)** – Implements the server logic that checks for logger presence and invokes methods (lines 113-120, 242-254). Also contains the UDP event logger wrapper (lines 401-415).
- **[`core/server/copy.go`](https://github.com/apernet/hysteria/blob/main/core/server/copy.go)** – Houses the `copyTwoWayEx` helper that forwards traffic statistics to the `TrafficLogger`.
- **[`core/server/udp.go`](https://github.com/apernet/hysteria/blob/main/core/server/udp.go)** – Manages UDP sessions and utilizes the `udpEventLoggerImpl` to map UDP events to `EventLogger` calls.
- **[`core/internal/integration_tests/trafficlogger_test.go`](https://github.com/apernet/hysteria/blob/main/core/internal/integration_tests/trafficlogger_test.go)** – Contains unit tests demonstrating expected `TrafficLogger` call patterns and the disconnect behavior when `LogTraffic` returns `false`.

## Summary

- **EventLogger** captures high-level connection events (authentication, TCP/UDP requests, errors) without affecting traffic flow.
- **TrafficLogger** provides per-stream byte counters and online state tracking, with the ability to forcibly disconnect clients by returning `false` from `LogTraffic`.
- Both interfaces are defined in [`core/server/config.go`](https://github.com/apernet/hysteria/blob/main/core/server/config.go) and are optional; the server checks for `nil` before invoking them.
- Implementations must be thread-safe due to concurrent goroutine access.
- The UDP event wrapper in [`core/server/server.go`](https://github.com/apernet/hysteria/blob/main/core/server/server.go) bridges UDP session management to the `EventLogger` interface.

## Frequently Asked Questions

### What is the difference between EventLogger and TrafficLogger in Hysteria?

**EventLogger** records discrete connection events like authentication and proxy requests, while **TrafficLogger** tracks continuous byte-level statistics and can control whether a connection remains active. EventLogger is purely observational, whereas TrafficLogger can deny service by returning `false` from `LogTraffic`.

### How do I make my Hysteria logger thread-safe?

Since Hysteria invokes logger methods from multiple concurrent goroutines, protect shared state with a `sync.Mutex` or use lock-free patterns like atomic counters for statistics. Each method call happens independently, so your implementation must handle concurrent access to any internal maps or counters.

### Can I reject client connections using the TrafficLogger?

Yes. Implement `LogTraffic(id, tx, rx) bool` to return `false` when a client exceeds bandwidth quotas or violates policy. The server checks this return value in [`core/server/copy.go`](https://github.com/apernet/hysteria/blob/main/core/server/copy.go) and will immediately terminate the client connection when it receives `false`.

### Where are the logging interfaces defined in the Hysteria source code?

The `EventLogger` interface is defined at lines 218-226 and `TrafficLogger` at lines 242-251 in **[`core/server/config.go`](https://github.com/apernet/hysteria/blob/main/core/server/config.go)**. The server implementation that invokes these interfaces is located in **[`core/server/server.go`](https://github.com/apernet/hysteria/blob/main/core/server/server.go)**, with specific invocation logic at lines 113-120 for setup and lines 242-254 for traffic logging.