### Start and Configure Gruf gRPC Server Programmatically Source: https://context7.com/bigcommerce/gruf/llms.txt Programmatically start a Gruf server with custom host, pool size, and SSL settings. Services can be registered automatically or manually, and interceptors can be added and ordered. ```ruby # Starting the server programmatically (e.g. in a custom binstub) server = Gruf::Server.new( hostname: '0.0.0.0:9001', pool_size: 10, max_waiting_requests: 20, use_ssl: false ) # Services are registered automatically from controllers, or manually: server.add_service(::Demo::ThingService) # Interceptors can also be managed on the server directly server.add_interceptor(JwtAuthInterceptor) server.insert_interceptor_before(JwtAuthInterceptor, TraceInterceptor) server.start! # blocks until SIGINT/SIGTERM/SIGQUIT ``` -------------------------------- ### Example Proto Definition Source: https://github.com/bigcommerce/gruf/wiki/Server:-Generating-Stubs Define your gRPC service and message structures using Protocol Buffers syntax. This file serves as the contract for your service. ```proto syntax = "proto3"; package demo; service Jobs { rpc GetJob(GetJobReq) returns (GetJobResp) { } } message GetJobReq { uint64 id = 1; } message GetJobResp { uint64 id = 1; string name = 2; } ``` -------------------------------- ### Generate gRPC Stubs with grpc_tools_ruby_protoc Source: https://github.com/bigcommerce/gruf/wiki/Server:-Generating-Stubs Use the `grpc_tools_ruby_protoc` command-line tool to generate Ruby server and message stubs from your .proto file. Ensure you have `grpc` and `grpc-tools` gems installed. ```bash grpc_tools_ruby_protoc --ruby_out=./ --grpc_out=./ ./Jobs.proto ``` -------------------------------- ### Run Gruf Server Source: https://github.com/bigcommerce/gruf/wiki/Server:-Running-Gruf-and-Command-Line-Options Execute this command from your project's root directory to start the Gruf server. It automatically binds controllers and updates the process title. The server responds to SIGINT and SIGTERM signals for safe shutdown. ```bash bundle exec gruf ``` -------------------------------- ### Gruf Command-Line Options Source: https://github.com/bigcommerce/gruf/wiki/Server:-Running-Gruf-and-Command-Line-Options These options can be used when starting the Gruf server to modify its behavior. They override any configurations set in the Gruf configure block or initializer. ```bash --host ``` ```bash --health-check ``` ```bash --services ``` ```bash --suppress-default-interceptors ``` ```bash --backtrace-on-error ``` -------------------------------- ### Configure Gruf Server Hooks Source: https://context7.com/bigcommerce/gruf/llms.txt Implement custom server lifecycle hooks by defining a class that inherits from Gruf::Hooks::Base and using Gruf.configure to register it. Hooks can perform actions before server start or after server stop. ```ruby # Hooks run around the server lifecycle (before_server_start / after_server_stop) class MyServerHook < Gruf::Hooks::Base def before_server_start(server:) logger.info 'Registering with service discovery...' ServiceDiscovery.register(host: '0.0.0.0', port: 9001) end def after_server_stop(server:) logger.info 'Deregistering from service discovery...' ServiceDiscovery.deregister end end Gruf.configure do |c| c.hooks.use(MyServerHook) end ``` -------------------------------- ### Implement Custom Request-Tagging Interceptor in Gruf Source: https://context7.com/bigcommerce/gruf/llms.txt Define a custom server interceptor by inheriting from Gruf::Interceptors::ServerInterceptor. This example adds a trace ID to the request context and logs the start and completion of RPC calls. ```ruby class TraceInterceptor < Gruf::Interceptors::ServerInterceptor def call trace_id = request.metadata.fetch('x-trace-id', SecureRandom.uuid) request.context[:trace_id] = trace_id logger.info "[#{trace_id}] #{request.method_name} started" result = yield logger.info "[#{trace_id}] #{request.method_name} completed" result end end ``` -------------------------------- ### Synchronized Client Example Source: https://github.com/bigcommerce/gruf/wiki/Client:-Synchronized-Calls Demonstrates how Gruf::SynchronizedClient prevents redundant RPC calls. The first thread makes the call, and the second thread waits for the result without making a new RPC. ```ruby require 'gruf' require 'thwait' id = args[:id].to_i.presence || 1 client = ::Gruf::SynchronizedClient.new(service: ::Demo::ThingService) thread1 = Thread.new { client.call(:GetMyThing, id: id) } thread2 = Thread.new { client.call(:GetMyThing, id: id) } ThreadsWait.all_waits(thread1, thread2) ``` -------------------------------- ### Mock Client for RSpec Tests Source: https://github.com/bigcommerce/gruf/wiki/Client:-Testing In RSpec tests, inject a double (mock) of the client to simulate responses and verify interactions. This example shows how to mock the `call` method. ```ruby describe Accounts::Service do let(:service) { described_class.new(client: client) } let(:client) { double(:client, call: call_result) } describe '#create' do let(:name) { "Bob's Burgers" } let(:call_result) { CreateAccountResp.new(name: name) } subject { service.create(name: name) } it 'creates the account' do expect(subject).to be_a(AccountEntity) expect(subject.name).to eq name end end end ``` -------------------------------- ### Implement Outbound Client Interceptor in Gruf Source: https://context7.com/bigcommerce/gruf/llms.txt Create a custom client interceptor by inheriting from Gruf::Interceptors::ClientInterceptor. This example injects a trace header into outbound metadata and logs request details. ```ruby class OutboundTraceInterceptor < Gruf::Interceptors::ClientInterceptor def call(request_context:) trace_id = Thread.current[:trace_id] || SecureRandom.uuid # Inject the trace header into outbound metadata request_context.metadata['x-trace-id'] = trace_id logger.debug "→ #{request_context.method} [#{request_context.type}]" start = Process.clock_gettime(Process::CLOCK_MONOTONIC) result = yield elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start).round(3) logger.debug "← #{request_context.method} completed in #{elapsed}s" result end end ``` -------------------------------- ### Get Client-Side Wall-Clock Execution Time Source: https://context7.com/bigcommerce/gruf/llms.txt Access `execution_time` to retrieve the client-side wall-clock execution time in seconds. ```ruby puts response.execution_time # => 0.034 ``` -------------------------------- ### Get Server-Reported Execution Time Source: https://context7.com/bigcommerce/gruf/llms.txt Retrieve `internal_execution_time` for the server-reported execution time in milliseconds, available in trailing metadata. ```ruby puts response.internal_execution_time # => 31.2 ``` -------------------------------- ### Implement Failing Logic in an Interceptor Source: https://github.com/bigcommerce/gruf/wiki/Server:-Interceptors Interceptors can halt the request by using the `fail!` method, similar to controllers. This example shows how to fail a request based on a condition within the yielded result. ```ruby class MyFailingInterceptor < ::Gruf::Interceptors::ServerInterceptor def call result = yield # this returns the protobuf message unless result.dont_hijack # we'll assume this "dont_hijack" attribute exists on the message for this example fail!(:internal, :hijacked, 'Hijack all the things!') end result end end ``` -------------------------------- ### Configure Basic Authentication on Client Source: https://github.com/bigcommerce/gruf/wiki/Server:-Authentication Set up client-side authentication by providing username and password options. Ensure environment variables are set for credentials. ```ruby require 'gruf' id = args[:id].to_i.presence || 1 options = { username: ENV.fetch('DEMO_THING_SERVICE_USERNAME'), password: ENV.fetch('DEMO_THING_SERVICE_PASSWORD') } begin client = ::Gruf::Client.new(service: ::Demo::ThingService, options: options) response = client.call(:GetMyThing, id: id) puts response.message.inspect rescue Gruf::Client::Error => e puts e.error.inspect end ``` -------------------------------- ### Instantiate Client with Hostname and Options Source: https://github.com/bigcommerce/gruf/wiki/Client:-Making-Your-First-Call Creates a Gruf client bound to a specific service, with optional parameters like hostname, username, and password for basic authentication. ```ruby client = Gruf::Client.new( service: ::Demo::Jobs, options: { hostname: '0.0.0.0:9003', username: 'admin', password: 'password', } ) ``` -------------------------------- ### Instantiate Client with Hostname Option Source: https://github.com/bigcommerce/gruf/wiki/Client:-Installation Create a new Gruf client instance, specifying the service and providing the hostname in the options if a default client host is not configured. ```ruby client = ::Gruf::Client.new(service: ::Demo::ThingService, options: {hostname: 'grpc.service.com:9003'}) ``` -------------------------------- ### Instantiate Client and Make a Call Source: https://github.com/bigcommerce/gruf/wiki/Client:-Making-Your-First-Call Instantiates a Gruf client for a specific service and makes a call. Requires service stubs to be required first. The service name does not include the '::Service' postfix. ```ruby require 'gruf' id = args[:id].to_i.presence || 1 begin client = ::Gruf::Client.new(service: ::Demo::Jobs) response = client.call(:GetJob, id: id) puts response.message.inspect # This will output the ::Demo::GetJobResp object rescue Gruf::Client::Error => e puts e.error.inspect # If an error occurs, this will be the underlying error object end ``` -------------------------------- ### Configure Server SSL Source: https://github.com/bigcommerce/gruf/wiki/Server:-Authentication Enable SSL for the server and specify the paths to the certificate and key files. This is recommended for credentialed authentication when using TLS. ```ruby Gruf.configure do |c| c.use_ssl = true c.ssl_crt_file = "#{Rails.root}/config/ssl/#{Rails.env}.crt" c.ssl_key_file = "#{Rails.root}/config/ssl/#{Rails.env}.key" end ``` -------------------------------- ### Configure Basic Authentication on Server Source: https://github.com/bigcommerce/gruf/wiki/Server:-Authentication Use this to configure basic authentication by providing an array of username and password pairs. This allows for unique credentials per service or easy credential rotation. ```ruby Gruf.configure do |c| c.interceptors.use( Gruf::Interceptors::Authentication::Basic, credentials: [{ username: 'my-username-here', password: 'my-password-here', },{ username: 'another-username', password: 'another-password', },{ password: 'a-password-only' }] ) end ``` -------------------------------- ### Instantiate Client with Specific Service Source: https://github.com/bigcommerce/gruf/wiki/Client:-Making-Your-First-Call Creates a Gruf client bound to a specific service. The service name does not include the '::Service' postfix, as Gruf appends it automatically. ```ruby client = ::Gruf::Client.new(service: ::Demo::Jobs) ``` -------------------------------- ### Define a Basic Server Interceptor Source: https://github.com/bigcommerce/gruf/wiki/Server:-Interceptors Create a simple interceptor by extending `Gruf::Interceptor::ServerInterceptor` and implementing the `call` method. Ensure you yield control to the next interceptor or the service method. ```ruby class MyInterceptor < ::Gruf::Interceptors::ServerInterceptor def call yield end end ``` -------------------------------- ### Configure Default Client Host Source: https://github.com/bigcommerce/gruf/wiki/Client:-Installation Set the default host for Gruf clients in an initializer. If this is not set, you must provide the hostname when creating a client. ```ruby require 'gruf' Gruf.configure do |c| c.default_client_host = 'grpc.service.com:9003' end ``` -------------------------------- ### Configure Gruf Server Options Source: https://context7.com/bigcommerce/gruf/llms.txt Configure global options for the Gruf server, including binding address, controller paths, logging, error handling, TLS, and thread-pool tuning. Settings can be overridden by environment variables. ```ruby Gruf.configure do |c| # Server binding c.server_binding_url = '0.0.0.0:9001' # Where Gruf autoloads controller files from (default: app/rpc) c.controllers_path = Rails.root.join('app', 'rpc').to_s # Logger – defaults to Rails.logger when in Rails c.logger = Logger.new($stdout).tap { |l| l.level = Logger::INFO } # Append structured error info into gRPC trailing metadata c.append_server_errors_to_trailing_metadata = true # Return the actual exception message to clients (set false for generic messages in production) c.use_exception_message = true c.internal_error_message = 'Internal Server Error' # Include stack traces in error metadata (useful in development) c.backtrace_on_error = false c.backtrace_limit = 10 # TLS (prefer linkerd/service-mesh instead) c.use_ssl = false # c.ssl_crt_file = 'config/ssl/production.crt' # c.ssl_key_file = 'config/ssl/production.key' # Default host for all outbound Gruf::Client instances c.default_client_host = '0.0.0.0:9001' # gRPC thread-pool tuning c.rpc_server_options = { pool_size: GRPC::RpcServer::DEFAULT_POOL_SIZE, max_waiting_requests: GRPC::RpcServer::DEFAULT_MAX_WAITING_REQUESTS, poll_period: GRPC::RpcServer::DEFAULT_POLL_PERIOD, pool_keep_alive: GRPC::Pool::DEFAULT_KEEP_ALIVE, server_args: {} } # Enable the built-in gRPC health check endpoint c.health_check_enabled = true # Custom health check logic (must return Grpc::Health::V1::HealthCheckResponse) c.health_check_hook = proc do |_request, _error| status = MyApp.healthy? ? :SERVING : :NOT_SERVING Grpc::Health::V1::HealthCheckResponse.new(status: status) end # Use default interceptors (Rails reloader, AR connection reset, output timer) c.use_default_interceptors = true end ``` -------------------------------- ### Configure Interceptors Globally with Gruf.configure Source: https://github.com/bigcommerce/gruf/wiki/Server:-Interceptors The recommended way to add interceptors is through the `Gruf.configure` block using the `use` method. This allows for global configuration of interceptors. ```ruby Gruf.configure do |c| c.interceptors.use(MyInterceptor, option_foo: 'value 123') end ``` -------------------------------- ### Require Gruf Library Source: https://github.com/bigcommerce/gruf/wiki/Client:-Installation Include this line in an initializer or before using Gruf to load the library. ```ruby require 'gruf' ``` -------------------------------- ### Client-Side Basic Authentication Configuration Source: https://context7.com/bigcommerce/gruf/llms.txt Configure the Gruf::Client to pass basic authentication credentials when making calls to a service secured with Basic Auth. ```ruby # Client side — pass credentials to Gruf::Client client = Gruf::Client.new( service: ::Demo::ThingService, options: { hostname: 'things-service:9001', username: 'service-a', password: ENV['SERVICE_A_PASSWORD'] } ) response = client.call(:GetThing, id: 1) ``` -------------------------------- ### Define a Gruf Controller Source: https://github.com/bigcommerce/gruf/wiki/Server:-Creating-Controllers Create a controller that extends `Gruf::Controllers::Base` and binds a gRPC service. Ensure the file structure adheres to Zeitwerk standards for autoloading. ```ruby module Demo class JobsController < ::Gruf::Controllers::Base bind ::Demo::Jobs::Service ## # @return [Demo::GetJobResp] The job response # def get_job job = ::Job.find(request.message.id) Demo::GetJobResp.new(id: job.id) rescue ActiveRecord::RecordNotFound => _e fail!(:not_found, :job_not_found, "Failed to find Job with ID: #{request.message.id}") rescue StandardError => e set_debug_info(e.message, e.backtrace[0..4]) fail!(:internal, :internal, "ERROR: #{e.message}") end end end ``` -------------------------------- ### Configure Client SSL Certificate Source: https://github.com/bigcommerce/gruf/wiki/Server:-Authentication Configure the client to use SSL by specifying the public certificate directly or via a file path. This is for client-side TLS encryption. ```ruby ::Gruf::Client.new( service: Demo::ThingService, options: { ssl_certificate: 'x509 public certificate here', # OR ssl_certificate_file: '/path/to/my.crt' } ) ``` -------------------------------- ### Register a Custom Hook with Gruf Source: https://github.com/bigcommerce/gruf/wiki/Server:-Hooks Configure Gruf to use your custom hook by adding it to the `c.hooks.use` configuration. You can pass options to the hook during registration. ```ruby Gruf.configure do |c| c.hooks.use(MyHook, option_foo: 'value 123') end ``` -------------------------------- ### Configure StatsD Interceptor Source: https://github.com/bigcommerce/gruf/wiki/Server:-Instrumentation-and-Logging Enable StatsD support by configuring the interceptor with your StatsD host, port, and a prefix for metrics. This measures counts and timings for each endpoint. ```ruby Gruf.configure do |c| c.interceptors.use( Gruf::Interceptors::Instrumentation::Statsd, client: ::Statsd.new('my.statsd.host', 8125), prefix: 'my_application_prefix.rpc' ) end ``` -------------------------------- ### Manage Interceptor Order with insert_before and insert_after Source: https://github.com/bigcommerce/gruf/wiki/Server:-Interceptors Control the execution order of interceptors using `insert_before` and `insert_after` within the `Gruf.configure` block. Interceptors run in FIFO order by default. ```ruby Gruf.configure do |c| c.interceptors.use(Interceptor1) c.interceptors.use(Interceptor2) c.interceptors.insert_before(Interceptor2, Interceptor3) # 3 will now happen before 2 c.interceptors.insert_after(Interceptor1, Interceptor4) # 4 will now happen after 1 end ``` -------------------------------- ### Configure Request Logging Formatter and Options Source: https://github.com/bigcommerce/gruf/blob/main/UPGRADING.md Configure Gruf's request logging to use the logstash formatter and optionally log parameters. It is recommended to use a blocklist for sensitive or large data when logging parameters. ```ruby Gruf.configure do |c| c.instrumentation_options[:request_logging] = { formatter: :logstash, log_parameters: false } end ``` -------------------------------- ### Configure Gruf Server Binding URL Source: https://github.com/bigcommerce/gruf/wiki/Server:-Installation Set the server binding URL for Gruf. This specifies the address and port where the gRPC server will listen for incoming connections. ```ruby Gruf.configure do |c| c.server_binding_url = 'grpc.service.com:9003' end ``` -------------------------------- ### Extend Gruf::Client for Accounts Service Source: https://github.com/bigcommerce/gruf/wiki/Client:-Testing Extend Gruf::Client to create a custom client for a specific gRPC service. This involves defining the service and options for initialization. ```ruby module Accounts class Client < ::Gruf::Client def initialize(hostname:, options: {}) opts = { service: ::Services::Accounts, options: { hostname: hostname # and other things if needed } }.merge(options) super(opts) end end end ``` -------------------------------- ### Configure Built-in Basic Authentication Interceptor Source: https://context7.com/bigcommerce/gruf/llms.txt Set up the Basic Authentication interceptor to validate the 'Authorization: Basic ' header against configured credentials. Supports multiple credential pairs and per-method bypass for specific methods like health checks. ```ruby Gruf.configure do |c| c.interceptors.use( Gruf::Interceptors::Authentication::Basic, credentials: [ # Multiple valid credentials supported { username: 'service-a', password: ENV['SERVICE_A_PASSWORD'] }, { username: 'service-b', password: ENV['SERVICE_B_PASSWORD'] }, # Password-only (no username check) { password: ENV['LEGACY_SHARED_SECRET'] } ], # Methods that skip authentication (e.g. the health check) excluded_methods: ['grpc.health.v1.health.check'] ) end ``` -------------------------------- ### Define gRPC Method Source: https://github.com/bigcommerce/gruf/wiki/Server:-Creating-Controllers Implement a Ruby method that corresponds to a gRPC service method. The method name should use Ruby's underscore convention. ```ruby def get_job end ``` -------------------------------- ### Make an RPC Call Source: https://github.com/bigcommerce/gruf/wiki/Client:-Making-Your-First-Call Makes an RPC call to the specified method with provided arguments. The response object contains message, trailing_metadata, and execution_time. ```ruby response = client.call(:GetJob, id: id) puts response.message.inspect ``` -------------------------------- ### Thundering-Herd Protection with Gruf::SynchronizedClient Source: https://context7.com/bigcommerce/gruf/llms.txt Use Gruf::SynchronizedClient to prevent cascading load spikes by synchronizing concurrent requests with identical parameters. Unsynchronized methods and cache expiry can be configured. ```ruby client = Gruf::SynchronizedClient.new( service: ::Demo::ThingService, options: { hostname: 'things-service:9001', # Methods listed here bypass synchronization entirely unsynchronized_methods: [:CreateThing, :DeleteThing], # How long (seconds) to cache results for other waiters internal_cache_expiry: 30 } ) # 100 concurrent threads calling with the same params will produce # exactly 1 real gRPC call; the other 99 receive a cached result. threads = 100.times.map do Thread.new { client.call(:GetThing, id: 99) } end responses = threads.map(&:value) puts responses.map(&:message).map(&:id).uniq # => [99] ``` -------------------------------- ### Define gRPC Controller for Unary RPC Source: https://context7.com/bigcommerce/gruf/llms.txt Implement a unary RPC handler by subclassing Gruf::Controllers::Base and defining a method for the RPC. Handle potential errors like record not found. ```ruby class ThingsController < Gruf::Controllers::Base bind ::Demo::ThingService ## # Unary RPC def get_thing thing = Thing.find(request.message.id) Demo::GetThingResponse.new(thing: serialize_thing(thing)) rescue ActiveRecord::RecordNotFound fail!(:not_found, :thing_not_found, "Thing #{request.message.id} not found") end ## # Server-streaming RPC def list_things Thing.where(active: true).find_each do |thing| # yield each message to the stream yield Demo::GetThingResponse.new(thing: serialize_thing(thing)) end end ## # Client-streaming RPC def create_things created = [] request.messages do |msg| created << Thing.create!(name: msg.name) end Demo::CreateThingsResponse.new(count: created.size) rescue ActiveRecord::RecordInvalid => e fail!(:invalid_argument, :validation_failed, e.message) end private def serialize_thing(thing) Demo::Thing.new(id: thing.id, name: thing.name) end end ``` -------------------------------- ### Inject Custom Client into Service Class Source: https://github.com/bigcommerce/gruf/wiki/Client:-Testing Inject the custom client into your service class. This allows for easy replacement of the client with a mock during testing. ```ruby module Accounts class Service def initialize(client: nil) @client = client || ::Accounts::Client.new(hostname: 'my.hostname:1234') end def create(name:) response = @client.call(:CreateAccount, name: name.to_s) AccountEntity.new(name: response.message.name) rescue Gruf::Client::Error => e raise Accounts::Errors::FailedCreation, e.message end end end ``` -------------------------------- ### Attach Outbound Client Interceptor to Gruf Client Source: https://context7.com/bigcommerce/gruf/llms.txt Instantiate a Gruf client and provide an array of interceptor instances in the `client_options`. This ensures all outbound calls made through this client are processed by the specified interceptors. ```ruby client = Gruf::Client.new( service: ::Demo::ThingService, client_options: { interceptors: [OutboundTraceInterceptor.new] } ) ``` -------------------------------- ### Define and Use a Gruf Client Interceptor Source: https://github.com/bigcommerce/gruf/wiki/Client:-Interceptors Define a custom interceptor by extending `Gruf::Interceptors::ClientInterceptor` and implement the `call` method. Pass an instance of your interceptor to the `interceptors` option within `client_options` when creating a `Gruf::Client`. ```ruby class MyInterceptor < Gruf::Interceptors::ClientInterceptor def call(request_context:) logger.info "Got method #{request_context.method}!" yield end end client = ::Gruf::Client.new( service: ::Demo::Jobs, client_options: { interceptors: [MyInterceptor.new] } ) ``` -------------------------------- ### Add Interceptor to Gruf Server Instance Source: https://github.com/bigcommerce/gruf/wiki/Server:-Interceptors Manually add an interceptor to a `Gruf::Server` instance, optionally passing configuration options. ```ruby server = Gruf::Server.new server.add_interceptor(MyInterceptor, option_foo: 'value 123') ``` -------------------------------- ### Making Outbound gRPC Calls with Gruf::Client Source: https://context7.com/bigcommerce/gruf/llms.txt Instantiate Gruf::Client once per service for structured error handling, timing, and Basic Auth. Supports params hash or pre-built protobuf messages, and custom metadata. ```ruby # Instantiate once per service, reuse across requests client = Gruf::Client.new( service: ::Demo::ThingService, options: { hostname: 'things-service:9001', # Basic auth (optional) username: 'grpc', password: ENV['THINGS_SERVICE_PASSWORD'], # TLS ssl_certificate_file: 'config/certs/things-service.crt' }, client_options: { timeout: 5 # seconds } ) # --- Unary call with a params hash --- begin response = client.call(:GetThing, id: 42) puts response.message.inspect # => puts response.execution_time # => 0.021 (client-side, in seconds) puts response.internal_execution_time # => 0.018 (server-reported, from trailing metadata) puts response.trailing_metadata # => {"timer"=>"0.018", ...} rescue Gruf::Client::Error => e # Structured error deserialized from gRPC trailing metadata puts e.error.code # => :not_found puts e.error.app_code # => :thing_not_found puts e.error.message # => "Thing 42 not found" puts e.error.field_errors # => [] rescue GRPC::DeadlineExceeded puts 'Request timed out' end # --- Pre-built protobuf message --- req = Demo::GetThingRequest.new(id: 42) response = client.call(:GetThing, req) # --- With custom metadata --- response = client.call(:GetThing, { id: 42 }, { 'x-trace-id' => SecureRandom.uuid }) ``` -------------------------------- ### Bind gRPC Service to Controller Source: https://github.com/bigcommerce/gruf/wiki/Server:-Creating-Controllers Use the `bind` method within your controller to associate it with a specific gRPC service stub. ```ruby class JobsController < ::Gruf::Controllers::Base bind ::Demo::Jobs::Service ``` -------------------------------- ### Add Gruf Gem Source: https://github.com/bigcommerce/gruf/wiki/Client:-Installation Add this line to your Gemfile to include the Gruf library in your project. ```ruby gem 'gruf' ``` -------------------------------- ### Check Deadline and Cancellation Status Source: https://context7.com/bigcommerce/gruf/llms.txt Inspect `deadline` for the request's deadline and `cancelled` to check if the request was cancelled. ```ruby puts response.deadline.inspect # => 2024-03-15 10:22:06 UTC ``` ```ruby puts response.cancelled # => false ``` -------------------------------- ### Define a Custom Gruf Hook Source: https://github.com/bigcommerce/gruf/wiki/Server:-Hooks Extend `Gruf::Hooks::Base` to create custom hooks. Implement methods corresponding to available hook insertion points. Exceptions raised will halt the execution chain. ```ruby class MyHook < Gruf::Hooks::Base def before_server_start(server:) # do my thing before the server starts end def after_server_stop(server:) # do my thing after the server stops end end ``` -------------------------------- ### Handle gRPC Errors with fail! Source: https://github.com/bigcommerce/gruf/wiki/Server:-Creating-Controllers Use the `fail!` helper method to raise gRPC errors with specific status codes, app error codes, and messages. Gruf serializes these into trailing metadata. ```ruby def get_job job = ::Job.find(request.message.id) Demo::GetJobResp.new(id: job.id) rescue ActiveRecord::RecordNotFound => _e fail!(:not_found, :job_not_found, "Failed to find Job with ID: #{request.message.id}") rescue StandardError => e set_debug_info(e.message, e.backtrace[0..4]) fail!(:internal, :internal, "ERROR: #{e.message}") end ``` -------------------------------- ### Configure Request Logging Formatter Source: https://github.com/bigcommerce/gruf/wiki/Server:-Instrumentation-and-Logging Customize request logging by specifying a formatter, such as `:logstash`. Ensure sensitive parameters are blocklisted if `log_parameters` is enabled. ```ruby Gruf.configure do |c| c.interceptors.use( Gruf::Interceptors::Instrumentation::RequestLogging::Interceptor, formatter: :logstash ) end ``` -------------------------------- ### Manage Interceptor Pipeline with Gruf::Interceptors::Registry Source: https://context7.com/bigcommerce/gruf/llms.txt Use Gruf::Interceptors::Registry to manage server interceptors. This allows appending, inserting before/after, removing, and clearing interceptors. Interceptors can also be managed directly on a Gruf::Server instance. ```ruby registry = Gruf.interceptors # the global registry # Append to the end of the pipeline registry.use(MyMetricsInterceptor, statsd_client: $statsd) # Insert before an existing interceptor registry.insert_before(JwtAuthInterceptor, TraceInterceptor) # Insert after an existing interceptor registry.insert_after(TraceInterceptor, JwtAuthInterceptor) # Remove an interceptor registry.remove(MyMetricsInterceptor) # Inspect what's registered puts registry.list.inspect # => [TraceInterceptor, JwtAuthInterceptor, Gruf::Interceptors::ActiveRecord::ConnectionReset, ...] puts registry.count # => 3 # Clear everything (useful in tests) registry.clear # Or manage interceptors on the server object directly server = Gruf::Server.new server.add_interceptor(JwtAuthInterceptor) server.insert_interceptor_before(JwtAuthInterceptor, TraceInterceptor) server.remove_interceptor(TraceInterceptor) puts server.list_interceptors ``` -------------------------------- ### Manually Build and Fail a Gruf::Error Source: https://context7.com/bigcommerce/gruf/llms.txt Construct a `Gruf::Error` instance manually, setting the gRPC code, application code, message, and optionally adding field errors and debug information. The `fail!` method can then be used to raise this error against the active call. ```ruby # Building and failing an error manually (e.g. in an interceptor) error = Gruf::Error.new( code: :not_found, app_code: :order_not_found, message: 'Order 123 does not exist' ) error.add_field_error(:order_id, :not_found, 'No order with ID 123') error.set_debug_info('ActiveRecord::RecordNotFound', caller) error.fail!(request.active_call) ``` -------------------------------- ### Access Full Trailing Metadata Source: https://context7.com/bigcommerce/gruf/llms.txt View `trailing_metadata` to access all metadata sent from the server. ```ruby puts response.trailing_metadata # => {"timer"=>"31.2", "x-request-id"=>"abc123"} ``` -------------------------------- ### Gruf::SynchronizedClient Source: https://context7.com/bigcommerce/gruf/llms.txt Provides thundering-herd protection for gRPC calls by using a per-call mutex to ensure that concurrent requests with identical parameters share a single in-flight gRPC call. ```APIDOC ## Gruf::SynchronizedClient ### Description Extends `Gruf::Client` with per-call mutex for thundering-herd protection. Concurrent requests with identical parameters share a single in-flight gRPC call, and subsequent waiters receive a cached copy of the result. ### Initialization ```ruby client = Gruf::SynchronizedClient.new( service: ::Demo::ThingService, options: { hostname: 'things-service:9001', # Methods listed here bypass synchronization entirely unsynchronized_methods: [:CreateThing, :DeleteThing], # How long (seconds) to cache results for other waiters internal_cache_expiry: 30 } ) ``` ### Usage Example ```ruby # 100 concurrent threads calling with the same params will produce # exactly 1 real gRPC call; the other 99 receive a cached result. threads = 100.times.map do Thread.new { client.call(:GetThing, id: 99) } end responses = threads.map(&:value) puts responses.map(&:message).map(&:id).uniq # => [99] ``` ``` -------------------------------- ### Return gRPC Response Source: https://github.com/bigcommerce/gruf/wiki/Server:-Creating-Controllers Construct and return the appropriate gRPC response message object from your controller method. This is required unless an exception is raised. ```ruby def get_job job = ::Job.find(request.message.id) Demo::GetJobResp.new(id: job.id) end ``` -------------------------------- ### Synchronized Client with Unsynchronized Methods Source: https://github.com/bigcommerce/gruf/wiki/Client:-Synchronized-Calls Shows how to disable synchronized call behavior for specific methods using the `unsynchronized_methods` option. This allows parallel execution for specified methods, similar to Gruf::Client. ```ruby require 'gruf' require 'thwait' id = args[:id].to_i.presence || 1 client = ::Gruf::SynchronizedClient.new(service: ::Demo::ThingService, options: { unsynchronized_methods: [:GetMyThing] }) thread1 = Thread.new { client.call(:GetMyThing, id: id) } thread2 = Thread.new { client.call(:GetMyThing, id: id) } ThreadsWait.all_waits(thread1, thread2) ``` -------------------------------- ### Inspect Request Metadata Source: https://context7.com/bigcommerce/gruf/llms.txt Check `metadata` to see any metadata that was sent with the request. This is typically an empty hash if no custom metadata was provided. ```ruby puts response.metadata # => {} ``` -------------------------------- ### Implementing Custom Server Interceptors in Gruf Source: https://context7.com/bigcommerce/gruf/llms.txt Subclass Gruf::Interceptors::ServerInterceptor and implement the #call method to wrap inbound RPC calls. Interceptors can access request context and yield to proceed. Handle errors using fail!. ```ruby # Custom authentication interceptor class JwtAuthInterceptor < Gruf::Interceptors::ServerInterceptor def call token = request.metadata['authorization'].to_s.sub('Bearer ', '') payload = JWT.decode(token, ENV['JWT_SECRET'], true, algorithm: 'HS256').first # Store decoded user in shared context so controllers can read it request.context[:current_user] = User.find(payload['user_id']) yield # proceed to the next interceptor / controller rescue JWT::DecodeError, ActiveRecord::RecordNotFound fail!(:unauthenticated, :invalid_token, 'Invalid or expired token') end end ``` -------------------------------- ### Gruf::Client Source: https://context7.com/bigcommerce/gruf/llms.txt Making outbound gRPC calls with structured error handling, timing, and Basic Auth support. The `#call` method accepts the RPC method name, parameters, metadata, and options. ```APIDOC ## Gruf::Client ### Description A client for making outbound gRPC calls. It extends `SimpleDelegator` over the generated gRPC stub, adding structured error handling, timing, and Basic Auth support. ### Initialization Instantiate once per service, reuse across requests. ```ruby client = Gruf::Client.new( service: ::Demo::ThingService, options: { hostname: 'things-service:9001', # Basic auth (optional) username: 'grpc', password: ENV['THINGS_SERVICE_PASSWORD'], # TLS ssl_certificate_file: 'config/certs/things-service.crt' }, client_options: { timeout: 5 # seconds } ) ``` ### Making Calls `#call` accepts the RPC method name as a symbol, a params hash (or a pre-built protobuf message), optional metadata, and options. #### Unary call with a params hash ```ruby begin response = client.call(:GetThing, id: 42) puts response.message.inspect # => puts response.execution_time # => 0.021 (client-side, in seconds) puts response.internal_execution_time # => 0.018 (server-reported, from trailing metadata) puts response.trailing_metadata # => {"timer"=>"0.018", ...} rescue Gruf::Client::Error => e # Structured error deserialized from gRPC trailing metadata puts e.error.code # => :not_found puts e.error.app_code # => :thing_not_found puts e.error.message # => "Thing 42 not found" puts e.error.field_errors # => [] rescue GRPC::DeadlineExceeded puts 'Request timed out' end ``` #### Using a pre-built protobuf message ```ruby req = Demo::GetThingRequest.new(id: 42) response = client.call(:GetThing, req) ``` #### With custom metadata ```ruby response = client.call(:GetThing, { id: 42 }, { 'x-trace-id' => SecureRandom.uuid }) ``` ``` -------------------------------- ### Inspect Deserialized Protobuf Response Message Source: https://context7.com/bigcommerce/gruf/llms.txt Use `inspect` to view the deserialized protobuf response message object. ```ruby puts response.message.inspect # => ``` -------------------------------- ### Accessing Request Context in Gruf Controllers Source: https://context7.com/bigcommerce/gruf/llms.txt Access the incoming protobuf message, gRPC metadata, and shared context within a Gruf controller. Use request type flags for introspection and access service/method names. ```ruby class OrdersController < Gruf::Controllers::Base bind ::Orders::OrderService def get_order # Access the incoming protobuf message order_id = request.message.id # Read gRPC metadata headers sent by the client trace_id = request.metadata['x-trace-id'] auth_token = request.metadata['authorization'] # Shared context set by upstream interceptors current_user = request.context[:current_user] # Request type introspection puts request.request_response? # => true for unary puts request.server_streamer? # => false puts request.method_name # => "orders.order_service.get_order" puts request.service_key # => "orders.order_service" order = Order.find_by!(id: order_id, user: current_user) Orders::GetOrderResponse.new(id: order.id, status: order.status) end end ``` -------------------------------- ### Gruf Controller Binding to Service Source: https://github.com/bigcommerce/gruf/blob/main/UPGRADING.md In Gruf 2.0, controllers bind to services using `bind ::Rpc::ThingService::Service`. Methods no longer take request/call arguments directly; use the `request` object instead. Extend from `Gruf::Controllers::Base`. ```ruby class ThingController < ::Gruf::Controllers::Base bind ::Rpc::ThingService::Service ## # Get the thing # def get_thing thing = Rpc::Thing.new(id: request.message.id, name: 'Foo') Rpc::GetThingResponse.new(thing: thing) end end ``` -------------------------------- ### Register Interceptors Globally in Gruf Source: https://context7.com/bigcommerce/gruf/llms.txt Configure Gruf to use interceptors globally. The order of registration matters as it determines the execution order of the interceptor pipeline. You can also exclude specific methods from interceptor logic. ```ruby Gruf.configure do |c| c.interceptors.use(TraceInterceptor) c.interceptors.use(JwtAuthInterceptor) # Exclude certain methods from JWT auth c.interceptors.use( Gruf::Interceptors::Authentication::Basic, credentials: [{ username: 'svc', password: ENV['SVC_PASSWORD'] }], excluded_methods: ['grpc.health.v1.health.check'] ) end ``` -------------------------------- ### Gruf::Interceptors::ServerInterceptor Source: https://context7.com/bigcommerce/gruf/llms.txt Custom server interceptors that wrap each inbound RPC call. Subclass this and implement `#call`, which must `yield` to invoke the next interceptor or the controller action. ```APIDOC ## Gruf::Interceptors::ServerInterceptor ### Description Base class for custom server interceptors. Interceptors wrap each inbound RPC call and are executed in registration order. They have access to `request`, `error`, and `options`. ### Implementation Subclass `Gruf::Interceptors::ServerInterceptor` and implement the `#call` method. The `#call` method must `yield` to invoke the next interceptor or the controller action. ### Example: Custom authentication interceptor ```ruby class JwtAuthInterceptor < Gruf::Interceptors::ServerInterceptor def call token = request.metadata['authorization'].to_s.sub('Bearer ', '') payload = JWT.decode(token, ENV['JWT_SECRET'], true, algorithm: 'HS256').first # Store decoded user in shared context so controllers can read it request.context[:current_user] = User.find(payload['user_id']) yield # proceed to the next interceptor / controller rescue JWT::DecodeError, ActiveRecord::RecordNotFound fail!(:unauthenticated, :invalid_token, 'Invalid or expired token') end end ``` ``` -------------------------------- ### Configure Structured Request Logging Interceptor Source: https://context7.com/bigcommerce/gruf/llms.txt Configure the built-in RequestLogging::Interceptor for Rails-style structured logs. Options include formatter (plain/logstash), logging parameters, redacting sensitive data, ignoring specific methods, and overriding log levels per gRPC status. ```ruby Gruf.configure do |c| c.interceptors.use( Gruf::Interceptors::Instrumentation::RequestLogging::Interceptor, # :plain (default) or :logstash for JSON-structured output formatter: :logstash, # Log the deserialized request params (careful with sensitive data) log_parameters: true, # Redact sensitive fields using dot-notation paths blocklist: ['metadata.authorization', 'message.password', 'message.credit_card.number'], redacted_string: '[FILTERED]', # Skip logging for certain methods (e.g. health checks) ignore_methods: ['grpc.health.v1.health.check'], # Override default log levels per gRPC status class log_levels: { 'GRPC::NotFound' => :warn, 'GRPC::Internal' => :fatal } ) end ``` -------------------------------- ### Configure Gruf RPC Server Pool Size Source: https://github.com/bigcommerce/gruf/wiki/Server:-Installation Customize the thread pool size for the underlying GRPC::RpcServer. This controls the number of concurrent requests the server can handle. Ensure your database connection pool size matches or exceeds this value if database calls are required. ```ruby Gruf.configure do |c| # The size of the underlying thread pool. No more concurrent requests can be made # than the size of the thread pool. c.rpc_server_options = c.rpc_server_options.merge(pool_size: 100) end ``` -------------------------------- ### Set Debug Info for Errors Source: https://github.com/bigcommerce/gruf/wiki/Server:-Creating-Controllers Capture and set detailed debugging information, such as exception messages and backtraces, in error responses. This is particularly useful for diagnosing issues in distributed systems. ```ruby begin DoBadThing.call rescue StandardError => e set_debug_info(e.message, e.backtrace) end ``` -------------------------------- ### Customize Health Check with a Hook Source: https://github.com/bigcommerce/gruf/wiki/Server:-Health-Checking Customize the health check response by providing a `health_check_hook` Proc. This hook allows you to test external dependencies like databases and return a `::Grpc::Health::V1::HealthCheckResponse` indicating their status. ```ruby Gruf.configure do |config| config.health_check_hook = lambda do |_request, _error| db_connected = ::ActiveRecord::Base.connected? ::Grpc::Health::V1::HealthCheckResponse.new( status: db_connected ? ::Grpc::Health::V1::HealthCheckResponse::ServingStatus::SERVING : ::Grpc::Health::V1::HealthCheckResponse::ServingStatus::NOT_SERVING ) end end ``` -------------------------------- ### Catch Structured Errors on the Client Side Source: https://context7.com/bigcommerce/gruf/llms.txt Handle Gruf client errors by rescuing Gruf::Client::Error and accessing detailed error information like code, app_code, message, and field errors. ```ruby begin client.call(:CreateProduct, name: 'X', price: -5) rescue Gruf::Client::Error => e err = e.error puts err.code # => :invalid_argument puts err.app_code # => :validation_failed puts err.message # => "Validation failed" err.field_errors.each do |fe| puts "#{fe.field_name}: #{fe.error_code} — #{fe.message}" end # => name: too_short — Name must be at least 3 characters # => price: invalid — Price must be positive end ``` -------------------------------- ### Enable Health Check in Gruf Configuration Source: https://github.com/bigcommerce/gruf/wiki/Server:-Health-Checking Enable the gRPC health check by setting `health_check_enabled` to true within the `Gruf.configure` block. This is an alternative to using environment variables or command-line flags. ```ruby Gruf.configure do |c| c.health_check_enabled = true end ``` -------------------------------- ### Accessing Instrumented Response Data Source: https://context7.com/bigcommerce/gruf/llms.txt Utilize the Gruf::Response object returned by Gruf::Client#call to access the protobuf message, timing information, metadata, and cancellation status of a gRPC operation. ```ruby response = client.call(:GetThing, id: 42) ``` -------------------------------- ### Handle Gruf Client Errors Source: https://github.com/bigcommerce/gruf/wiki/Client:-Making-Your-First-Call Catches Gruf::Client::Error exceptions, which are raised for failed gRPC calls. Specific error classes are derivatives of Gruf::Client::Error, matching GRPC::BadStatus codes. ```ruby rescue Gruf::Client::Error => e puts e.error.inspect # If an error occurs, this will be the underlying error object ``` -------------------------------- ### Gruf::Controllers::Request Source: https://context7.com/bigcommerce/gruf/llms.txt Accessing the gRPC request context within controllers and server interceptors. This includes the protobuf message, metadata, request type flags, and a shared context hash. ```APIDOC ## Gruf::Controllers::Request ### Description Wraps the active gRPC call, providing access to the incoming protobuf message, call metadata, request type flags, and a shared `context` hash for passing data between interceptors. ### Usage Accessible via `request` inside any controller or server interceptor. ### Example ```ruby class OrdersController < Gruf::Controllers::Base bind ::Orders::OrderService def get_order # Access the incoming protobuf message order_id = request.message.id # Read gRPC metadata headers sent by the client trace_id = request.metadata['x-trace-id'] auth_token = request.metadata['authorization'] # Shared context set by upstream interceptors current_user = request.context[:current_user] # Request type introspection puts request.request_response? # => true for unary puts request.server_streamer? # => false puts request.method_name # => "orders.order_service.get_order" puts request.service_key # => "orders.order_service" order = Order.find_by!(id: order_id, user: current_user) Orders::GetOrderResponse.new(id: order.id, status: order.status) end end ``` ``` -------------------------------- ### Pass Contextual Data via Interceptor Source: https://github.com/bigcommerce/gruf/wiki/Server:-Interceptors Add data to the request's context hash within an interceptor. This data can then be accessed by subsequent interceptors or the Gruf controller. ```ruby class MyInterceptor < ::Gruf::Interceptors::ServerInterceptor def call request.context[:foo] = 'bar' yield end end ``` -------------------------------- ### Access Request Message Source: https://github.com/bigcommerce/gruf/wiki/Server:-Creating-Controllers Utilize the `request` object within your controller method to access the incoming gRPC message and its attributes. ```ruby def get_job job = ::Job.find(request.message.id) end ```