In the previous chapter, Frontend State Management (Pinia), we completed the frontend loop, allowing the browser to react instantly to changes on a single server.
But what if your infrastructure isn't just one computer? What if you have a Web Server in New York, a Database Server in London, and a Backup Server in Tokyo? Opening three different tabs to monitor them is tedious.
In this chapter, we explore the Agent & RPC System. This allows a central Dozzle instance to connect to multiple remote machines, gathering logs and stats into one unified dashboard.
To understand this architecture, think of a TV News Network.
In Dozzle:
Before two computers can talk, they need to agree on a language. Dozzle uses Protocol Buffers (Protobuf). We define a "Contract" in a .proto file. This file describes exactly what functions the Agent must provide.
Located in protos/rpc.proto, this is the heart of the system.
// protos/rpc.proto (Simplified)
syntax = "proto3";
service AgentService {
// "Hey Agent, give me a list of containers"
rpc ListContainers(ListContainersRequest) returns (ListContainersResponse) {}
// "Hey Agent, keep sending me log lines for this container"
rpc StreamLogs(StreamLogsRequest) returns (stream StreamLogsResponse) {}
}
Beginner Note:
rpcstands for Remote Procedure Call. It means "Running a function on someone else's computer." The keywordstreammeans the connection stays open, and data keeps flowing (like a video call).
The Agent is a small program that runs on the remote machine. It doesn't have a UI. Its only job is to listen for commands from the HQ and execute them.
It implements the "Contract" we defined above. Let's look at internal/agent/server.go.
When the HQ asks for logs, the Agent calls its local Docker Adapter and pipes the results into the network connection.
// internal/agent/server.go
func (s *server) StreamLogs(in *pb.StreamLogsRequest, out pb.AgentService_StreamLogsServer) error {
// 1. Find the container on this local machine
c, _ := s.service.FindContainer(out.Context(), in.ContainerId, ...)
// 2. Create a channel to receive logs from Docker
events := make(chan *container.LogEvent)
// 3. Start reading Docker logs in background
go s.service.StreamLogs(out.Context(), c, ..., events)
// ... (continued below)
As logs arrive from Docker, the Agent wraps them in a Protobuf message and sends them over the wire.
// 4. Loop through events as they happen
for event := range events {
// 5. Send to HQ via gRPC
out.Send(&pb.StreamLogsResponse{
Event: logEventToPb(event), // Convert to Protobuf format
})
}
return nil
}
Back at HQ (the main Dozzle instance), we need a way to talk to these agents. We create a gRPC Client.
This code, found in internal/agent/client.go, acts like a translator. It takes Dozzle's internal commands and converts them into gRPC calls.
// internal/agent/client.go
func (c *Client) StreamContainerLogs(ctx context.Context, id string, ..., events chan<- *container.LogEvent) error {
// 1. Call the remote Agent
stream, err := c.client.StreamLogs(ctx, &pb.StreamLogsRequest{
ContainerId: id,
// ... params
})
// 2. Process the incoming stream
return sendLogs(stream, events)
}
Let's visualize the journey of a log message from a remote server to your screen.
One challenge with gRPC is that it requires strict typing, but logs can be messy (text, JSON, complex objects).
To solve this, we use a helper to convert our internal LogEvent into a pb.LogEvent (Protobuf Log Event).
In internal/agent/server.go:
func logEventToPb(event *container.LogEvent) *pb.LogEvent {
// We use "Any" or specific types to handle different log formats
return &pb.LogEvent{
ContainerId: event.ContainerID,
Timestamp: timestamppb.New(time.Unix(event.Timestamp, 0)),
RawMessage: string(event.RawMessage),
// ... mapping other fields
}
}
You can't just let anyone walk into a Newsroom, and you can't let just anyone connect to your Agent. That would give them root access to your logs!
Dozzle uses mTLS (Mutual TLS).
In internal/support/cli/agent_command.go, we enforce this:
// internal/support/cli/agent_command.go
func (a *AgentCmd) Run(...) error {
// Load certificates from disk
certs, _ := ReadCertificates(..., args.CertPath, args.KeyPath)
// Create the secure server
server, _ := agent.NewServer(clientService, certs, ...)
// Listen on TCP port (default :7007)
listener, _ := net.Listen("tcp", args.Agent.Addr)
return server.Serve(listener)
}
Remember the Client Adapter interface from Chapter 1?
The genius of Dozzle's design is that the Agent Client (running on HQ) implements the exact same interface as the Local Docker Client.
To the rest of the application (The Store, The UI), it doesn't matter if the logs are coming from localhost or a server in Tokyo. They just call ListContainers().
Dozzle -> Docker SocketDozzle -> gRPC Client -> Network -> gRPC Server -> Docker SocketWe have successfully scaled our application!
Your Dozzle instance can now monitor an entire fleet of servers. However, staring at logs all day is tiring. What if a container crashes while you are asleep? You need to be alerted.
Next Chapter: Notification Manager
Generated by Code IQ