Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ $ bazel-bin/hello-world-client

- [OAuth2-based Authentication](example-oauth)

- [Log4j 2 logging context](example-log4j2)

- [Pre-serialized messages](src/main/java/io/grpc/examples/preserialized)

## Unit test examples
Expand Down
67 changes: 67 additions & 0 deletions examples/example-log4j2/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
gRPC Log4j 2 Example
==============================================

This example illustrates how to set the Log4j 2 `ThreadContext` from a server interceptor so that
it is picked up by the logger in your server.

The server interceptor puts a `requestId` (a randomly generated UUID) and the `clientName` taken
from a request header into the `ThreadContext`. The `%X` conversion pattern in
[log4j2.xml](src/main/resources/log4j2.xml) then appends both values to every log statement made
while the call is being handled, so the service method itself does not have to pass them around.

These values are deliberately *not* stored in an `io.grpc.Context`: logging frameworks read from
thread-local storage, so the interceptor sets and clears the `ThreadContext` around each callback
using `CloseableThreadContext`.

### Build the example

The examples require `grpc-java` to already be built. You are strongly encouraged to check out a
git release tag, since there will already be a build of gRPC available. Otherwise you must follow
[COMPILING](../../COMPILING.md).

From the `grpc-java/examples/example-log4j2` directory:
```
$ ../gradlew installDist
```

This creates the scripts `build/install/example-log4j2/bin/custom-log-server` and
`build/install/example-log4j2/bin/custom-log-client`.

### Run the example

1. To start the server on its default port of 50051, run:
```
$ ./build/install/example-log4j2/bin/custom-log-server
```

2. In a different terminal window, run the client:
```
$ ./build/install/example-log4j2/bin/custom-log-client
```

The server logs a line for each request that includes the contextual values appended by `%X`:

```
2026/09/14 15:22:12:686 PDT INFO CustomLogServer - Got a request {requestId=3e6c256d-6e87-411e-8bf3-fbf81e7ce0e6, clientName=my.domain.name}
```

Log statements made outside of an RPC, such as the server's startup message, are unaffected:

```
2026/09/14 15:22:04:132 PDT INFO CustomLogServer - Server started, listening on 50051
```

### Why each callback sets the context

gRPC does not guarantee that every callback for a call runs on the same thread, and an application
that supplies its own call executor may have each callback handled by a different worker. Logging
frameworks read from thread-local storage, so the values have to be established on whichever thread
is actually running the code that logs. That is why each `ServerCall.Listener` callback re-populates
the `ThreadContext` and clears it again on the way out, instead of the interceptor setting it once
and leaving it.

The context is also established around `next.startCall()`, so that interceptors further down the
chain see these values while their own `interceptCall()` runs.

For more information, refer to gRPC Java's [README](../../README.md) and
[tutorial](https://grpc.io/docs/languages/java/basics).
71 changes: 71 additions & 0 deletions examples/example-log4j2/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
plugins {
id 'application' // Provide convenience executables for trying out the examples.
id 'com.google.protobuf' version '0.9.5'
// Generate IntelliJ IDEA's .idea & .iml project files
id 'idea'
id 'java'
}

repositories {
mavenCentral()
mavenLocal()
}

java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

// IMPORTANT: You probably want the non-SNAPSHOT version of gRPC. Make sure you
// are looking at a tagged version of the example and not "master"!

// Feel free to delete the comment at the next line. It is just for safely
// updating the version in our release process.
def grpcVersion = '1.85.0-SNAPSHOT' // CURRENT_GRPC_VERSION
def protocVersion = '3.25.8'
def log4jVersion = '2.26.1'

dependencies {
implementation "io.grpc:grpc-protobuf:${grpcVersion}"
implementation "io.grpc:grpc-stub:${grpcVersion}"
implementation "org.apache.logging.log4j:log4j-api:${log4jVersion}"
runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}"
// The Log4j 2 implementation is only needed at runtime; the example compiles against the API.
runtimeOnly "org.apache.logging.log4j:log4j-core:${log4jVersion}"
}

protobuf {
protoc { artifact = "com.google.protobuf:protoc:${protocVersion}" }
plugins {
grpc { artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}" }
}
generateProtoTasks {
all()*.plugins { grpc {} }
}
}

startScripts.enabled = false

task customLogServer(type: CreateStartScripts) {
mainClass = 'io.grpc.examples.logcontext.CustomLogServer'
applicationName = 'custom-log-server'
outputDir = new File(project.buildDir, 'tmp/scripts/' + name)
classpath = startScripts.classpath
}

task customLogClient(type: CreateStartScripts) {
mainClass = 'io.grpc.examples.logcontext.CustomLogClient'
applicationName = 'custom-log-client'
outputDir = new File(project.buildDir, 'tmp/scripts/' + name)
classpath = startScripts.classpath
}

application {
applicationDistribution.into('bin') {
from(customLogServer)
from(customLogClient)
filePermissions {
unix(0755)
}
}
}
17 changes: 17 additions & 0 deletions examples/example-log4j2/settings.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
pluginManagement {
// https://issuetracker.google.com/issues/342522142#comment8
// use D8/R8 8.0.44 or 8.1.44 with AGP 7.4 if needed.
buildscript {
repositories {
mavenCentral()
maven {
url = uri("https://storage.googleapis.com/r8-releases/raw")
}
}
dependencies {
classpath("com.android.tools:r8:8.1.44")
}
}
}

rootProject.name = 'example-log4j2'
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* Copyright 2026 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.grpc.examples.logcontext;

import io.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.ClientCall;
import io.grpc.ClientInterceptor;
import io.grpc.ForwardingClientCall.SimpleForwardingClientCall;
import io.grpc.Grpc;
import io.grpc.InsecureChannelCredentials;
import io.grpc.ManagedChannel;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
import io.grpc.StatusRuntimeException;
import io.grpc.examples.helloworld.GreeterGrpc;
import io.grpc.examples.helloworld.HelloReply;
import io.grpc.examples.helloworld.HelloRequest;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
* A client like {@link io.grpc.examples.helloworld.HelloWorldClient} that sends the header
* {@link CustomLogServer} logs.
*
* <p>The client itself does not use Log4j 2; it only supplies the {@code clientName} header that
* {@link HeaderServerInterceptor} copies into the server's logging context.
*/
public class CustomLogClient {

private static final Logger logger = Logger.getLogger(CustomLogClient.class.getName());

// Resolved once rather than per RPC: InetAddress.getLocalHost() may block on a DNS lookup, and
// start() runs on the calling thread.
private static final String LOCAL_HOST_NAME = localHostName();

private final GreeterGrpc.GreeterBlockingStub blockingStub;

/** Construct a client for accessing the server using the existing channel. */
public CustomLogClient(Channel channel) {
// 'channel' here is a Channel, not a ManagedChannel, so it is not this code's responsibility to
// shut it down.
blockingStub = GreeterGrpc.newBlockingStub(channel).withInterceptors(new ClientInterceptor() {
@Override
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
return new SimpleForwardingClientCall<ReqT, RespT>(next.newCall(method, callOptions)) {
@Override
public void start(Listener<RespT> responseListener, Metadata headers) {
headers.put(HeaderServerInterceptor.CLIENT_NAME_KEY, LOCAL_HOST_NAME);
super.start(responseListener, headers);
}
};
}
});
}

private static String localHostName() {
try {
return InetAddress.getLocalHost().getHostName();
} catch (UnknownHostException e) {
logger.log(Level.WARNING, "Unable to determine the local host name", e);
return "unknown";
}
}

/** Say hello to the server. */
public void greet(String name) {
logger.info("Will try to greet " + name + " ...");
HelloRequest request = HelloRequest.newBuilder().setName(name).build();
HelloReply response;
try {
response = blockingStub.sayHello(request);
} catch (StatusRuntimeException e) {
logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
return;
}
logger.info("Greeting: " + response.getMessage());
}

/**
* Greet the server. If provided, the first element of {@code args} is the name to use in the
* greeting. The second argument is the target server.
*/
public static void main(String[] args) throws Exception {
String user = "world";
// Access a service running on the local machine on port 50051
String target = "localhost:50051";
if (args.length > 0) {
if ("--help".equals(args[0])) {
System.err.println("Usage: [name [target]]");
System.err.println("");
System.err.println(" name The name you wish to be greeted by. Defaults to " + user);
System.err.println(" target The server to connect to. Defaults to " + target);
System.exit(1);
}
user = args[0];
}
if (args.length > 1) {
target = args[1];
}

ManagedChannel channel = Grpc.newChannelBuilder(target, InsecureChannelCredentials.create())
.build();
try {
CustomLogClient client = new CustomLogClient(channel);
client.greet(user);
} finally {
channel.shutdownNow().awaitTermination(5, TimeUnit.SECONDS);
}
}
}
Loading
Loading