The protoreg plugin provides a shared protobuf registry. It reads .proto definitions for gRPC server reflection, custom interceptors, and dynamic message handling. The RoadRunner v3 bundle includes this plugin.
- Parses and registers
.protofiles at startup - Provides a shared
protoregistryaccessible by other plugins - Supports multiple import paths for proto dependencies
- Automatically resolves and registers proto file dependencies
- Exposes service and method descriptors for reflection-based operations
- Thread-safe registry access
- Custom gRPC Interceptors: Build interceptors that need to inspect or modify messages based on their proto definitions
- Dynamic Message Handling: Deserialize and serialize protobuf messages without compile-time generated code
- Request/Response Validation: Validate gRPC requests against proto schemas
- Logging & Monitoring: Create detailed logs with field-level message inspection
Both grpc and protoreg configuration sections are required to start the plugin. Keep the service definitions in both sections consistent.
The plugin is configured under the protoreg section in your .rr.yaml configuration file:
version: '3'
grpc:
listen: "tcp://127.0.0.1:9001"
proto:
- "proto/serviceapis/service/v1/service.proto"
- "proto/serviceapis/user/v1/user.proto"
protoreg:
proto_path:
- proto/commonapis
- proto/serviceapis
files:
- service/v1/service.proto
- user/v1/user.protoSet server.command to your gRPC worker as shown in the gRPC configuration.
A list of directories where the plugin will search for proto files and their dependencies. These paths work similarly to the --proto-path/-I flag in protoc.
Why it's needed: Proto files often import other proto files (e.g., import "google/protobuf/timestamp.proto"). The import paths tell the parser where to find these dependencies. You typically need:
- A path containing your service definitions
- A path containing shared/common message definitions
- Paths to any third-party proto files (like Google's well-known types)
proto_path:
- proto/commonapis # Shared message definitions
- proto/serviceapis # Service-specific proto definitions
- proto/third_party # External dependencies (e.g., google/api)A list of proto files to parse and register. Paths are relative to the proto_path directories. Use explicit file names. This list does not expand the glob patterns supported by grpc.proto.
Both proto_path and files must contain at least one entry. Blank entries fail initialization. Each file must exist under at least one configured import path.
Why it's needed: This explicitly declares which proto files define the services and messages you want to access at runtime. The plugin will:
- Parse each listed proto file
- Automatically resolve and register all imported dependencies
- Build a registry of all services and their methods
files:
- service/v1/service.proto # Your gRPC service definitions
- user/v1/user.proto # Additional service definitionsRR automatically uses the protoreg registry for server reflection. Configure the same service files in grpc.proto and protoreg.files. Paths in protoreg.files are relative to proto_path; they do not replace grpc.proto.
For the Hello World service, use:
{% code title=".rr.yaml" %}
grpc:
listen: "tcp://127.0.0.1:9001"
proto:
- "proto/helloworld.proto"
protoreg:
proto_path:
- "proto"
files:
- "helloworld.proto"{% endcode %}
Reflection clients can retrieve file and message descriptors for the PHP service. For the preceding listener without TLS, use grpcurl without a local .proto file:
{% code %}
grpcurl -plaintext 127.0.0.1:9001 describe helloworld.Greeter
grpcurl -plaintext -d '{"name":"RoadRunner"}' 127.0.0.1:9001 helloworld.Greeter/SayHello{% endcode %}
Reflection streams do not call unary authentication interceptors. See reflection access controls.
A typical project structure when using this plugin:
my-project/
├── .rr.yaml
├── proto/
│ ├── commonapis/
│ │ └── common/
│ │ └── v1/
│ │ └── message.proto
│ └── serviceapis/
│ └── service/
│ └── v1/
│ └── service.proto
└── ...
proto/commonapis/common/v1/message.proto:
syntax = "proto3";
package common.v1;
message Request {
string id = 1;
string payload = 2;
}
message Response {
string id = 1;
string result = 2;
}proto/serviceapis/service/v1/service.proto:
syntax = "proto3";
package service.v1;
import "common/v1/message.proto";
service MyService {
rpc Process (common.v1.Request) returns (common.v1.Response) {}
rpc Ping (common.v1.Request) returns (common.v1.Response) {}
}Configuration (.rr.yaml):
version: '3'
grpc:
listen: "tcp://127.0.0.1:9001"
proto:
- "proto/serviceapis/service/v1/service.proto"
protoreg:
proto_path:
- proto/commonapis
- proto/serviceapis
files:
- service/v1/service.protoDeclare protoreg.Registry as an initialization dependency:
package myplugin
import "github.com/roadrunner-server/protoreg/v6"
type Plugin struct {
registry protoreg.Registry
}
func (p *Plugin) Init(registry protoreg.Registry) error {
p.registry = registry
return nil
}The interface uses protoresolve.Registry from github.com/jhump/protoreflect/v2/protoresolve and descriptors from github.com/jhump/protoreflect/desc:
type Registry interface {
// Registry returns the parsed descriptors.
Registry() *protoresolve.Registry
// Services maps fully qualified service names to descriptors.
Services() map[string]*desc.ServiceDescriptor
// The path format is "/package.Service/Method" or "package.Service/Method".
FindMethodByFullPath(method string) (*desc.MethodDescriptor, error)
}This unary interceptor logs method descriptors without logging request bodies. Include it with grpc and protoreg in your custom build. Set grpc.interceptors: ["descriptor-logger"] to activate it.
package interceptor
import (
"context"
"log/slog"
"github.com/roadrunner-server/protoreg/v6"
"google.golang.org/grpc"
)
type Logger interface {
NamedLogger(name string) *slog.Logger
}
type Plugin struct {
registry protoreg.Registry
log *slog.Logger
}
func (p *Plugin) Init(registry protoreg.Registry, logger Logger) error {
p.registry = registry
p.log = logger.NamedLogger(p.Name())
return nil
}
func (p *Plugin) Name() string {
return "descriptor-logger"
}
func (p *Plugin) UnaryServerInterceptor() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
method, err := p.registry.FindMethodByFullPath(info.FullMethod)
if err == nil && method != nil {
p.log.Info("grpc method",
"method", method.GetFullyQualifiedName(),
"input_type", method.GetInputType().GetFullyQualifiedName(),
"output_type", method.GetOutputType().GetFullyQualifiedName(),
)
}
return handler(ctx, req)
}
}Ensure all paths in proto_path exist and are accessible. Paths can be absolute or relative to the RoadRunner working directory.
The proto file must exist in one of the configured proto_path. Check that:
- The file path in
filesis relative to an import path, not an absolute path - The directory structure matches the import path
This occurs when the same proto file is included multiple times (e.g., listed in files and also imported by another file). The plugin handles dependencies automatically, so you typically only need to list your top-level service definitions in the files section.
MIT License - see LICENSE file for details.