Overview
The Caller service is a Go-based microservice that acts as an HTTP client proxy. It receives gRPC requests with URLs and makes HTTP GET calls to those URLs, returning status codes and response sizes. This service demonstrates the asynchronous database pattern where logging happens after the response is sent.
Source code: services/internal/caller/service.go Entry point: services/cmd/caller/main.go
Technology Stack
Language : Go
Framework : Connect-Go (gRPC-compatible)
Database : PostgreSQL via pgx/v5
Protocol : Protocol Buffers
Observability : OpenTelemetry
HTTP Version : HTTP/2 (h2c)
Configuration
Environment Variables
HTTP server port for the caller service
PostgreSQL connection string Example: postgresql://devuser:devpass@postgres:5432/caller_db
OTEL_EXPORTER_OTLP_ENDPOINT
OpenTelemetry collector endpoint
OTEL_SERVICE_NAME
string
default: "caller-service"
Service name for distributed tracing
Docker Compose Configuration
caller :
build :
context : .
dockerfile : deploy/docker/caller/Dockerfile
environment :
PORT : "8081"
DATABASE_URL : "postgresql://devuser:devpass@postgres:5432/caller_db"
OTEL_SERVICE_NAME : "caller-service"
networks :
- app
depends_on :
- postgres
API Reference
Protocol Buffer Definition
The service is defined in proto/caller/v1/caller.proto:
syntax = "proto3" ;
package caller.v1 ;
service CallerService {
rpc CallExternal ( CallExternalRequest ) returns ( CallExternalResponse ) {}
}
message CallExternalRequest {
string url = 1 ;
}
message CallExternalResponse {
int32 status_code = 1 ;
int32 body_length = 2 ;
}
CallExternal RPC
Makes an HTTP GET request to the specified URL and returns metadata about the response.
Valid HTTP/HTTPS URL to call Must be a valid URI that can be parsed by Go’s url.ParseRequestURI()
HTTP status code from the external URL Examples: 200, 404, 500
Size of the response body in bytes Body content is discarded after reading to save memory
Example Request
# Using grpcurl
grpcurl -plaintext -d '{"url": "https://httpbin.org/get"}' \
localhost:8081 \
caller.v1.CallerService/CallExternal
Example Response
{
"status_code" : 200 ,
"body_length" : 384
}
Implementation Details
Service Structure
From services/internal/caller/service.go:19-27:
type Service struct {
httpClient * http . Client
timeout time . Duration
pool * pgxpool . Pool
}
func NewService (
httpClient * http . Client ,
timeout time . Duration ,
pool * pgxpool . Pool ,
) * Service {
return & Service {
httpClient : httpClient ,
timeout : timeout ,
pool : pool ,
}
}
Request Flow
Validate URL : Parse and validate the URL format
Create HTTP Request : Build GET request with context
Execute Request : Make HTTP call with timeout
Read Response : Discard body while counting bytes
Return Response : Send status and size immediately
Async Database Write : Log call in background goroutine
From services/internal/caller/service.go:29-76:
func ( s * Service ) CallExternal (
ctx context . Context ,
req * connect . Request [ callerv1 . CallExternalRequest ],
) ( * connect . Response [ callerv1 . CallExternalResponse ], error ) {
targetURL := req . Msg . GetUrl ()
if _ , err := url . ParseRequestURI ( targetURL ); err != nil {
return nil , connect . NewError ( connect . CodeInvalidArgument ,
fmt . Errorf ( "invalid URL: %w " , err ))
}
rpcCtx , cancel := context . WithTimeout ( ctx , s . timeout )
defer cancel ()
httpReq , err := http . NewRequestWithContext ( rpcCtx , http . MethodGet , targetURL , nil )
if err != nil {
return nil , connect . NewError ( connect . CodeInternal ,
fmt . Errorf ( "build request: %w " , err ))
}
httpResp , err := s . httpClient . Do ( httpReq )
if err != nil {
if rpcCtx . Err () != nil {
return nil , connect . NewError ( connect . CodeDeadlineExceeded , rpcCtx . Err ())
}
return nil , connect . NewError ( connect . CodeUnavailable ,
fmt . Errorf ( "call failed: %w " , err ))
}
defer httpResp . Body . Close ()
n , err := io . Copy ( io . Discard , httpResp . Body )
if err != nil {
return nil , connect . NewError ( connect . CodeInternal ,
fmt . Errorf ( "read response: %w " , err ))
}
statusCode := int32 ( httpResp . StatusCode )
bodyLength := int32 ( n )
// Asynchronous DB write pattern
if s . pool != nil {
capturedURL := targetURL
go func () {
_ , err := s . pool . Exec ( context . Background (),
"INSERT INTO call_logs (url, status_code, body_length) VALUES ($1, $2, $3)" ,
capturedURL , statusCode , bodyLength )
if err != nil {
slog . Error ( "failed to insert call log" , "error" , err )
}
}()
}
resp := connect . NewResponse ( & callerv1 . CallExternalResponse {
StatusCode : statusCode ,
BodyLength : bodyLength ,
})
return resp , nil
}
Timeout Configuration
RPC Timeout : 2 seconds for external HTTP calls
HTTP Client Timeout : 2 seconds (configured in main.go)
Server Read Timeout : 5 seconds
Server Write Timeout : 30 seconds
Database Schema
The service uses a call_logs table to record all external API calls:
CREATE TABLE call_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
url TEXT NOT NULL ,
status_code INT NOT NULL ,
body_length INT NOT NULL ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now ()
);
Asynchronous Database Pattern
The Caller service uses an asynchronous write pattern that differs from the Greeter service:
if s . pool != nil {
capturedURL := targetURL
go func () {
_ , err := s . pool . Exec ( context . Background (),
"INSERT INTO call_logs (url, status_code, body_length) VALUES ($1, $2, $3)" ,
capturedURL , statusCode , bodyLength )
if err != nil {
slog . Error ( "failed to insert call log" , "error" , err )
}
}()
}
Benefits :
Lower latency - response returns immediately
No database backpressure affecting clients
Better throughput under high load
Tradeoffs :
Possible log loss if service crashes before goroutine completes
No guarantee of data persistence
Errors are only logged, not returned to client
Database writes happen in a separate goroutine using context.Background() to avoid cancellation. If the service crashes between the response and the database write, logs may be lost.
Error Handling
The service handles multiple error scenarios:
Invalid URL
if _ , err := url . ParseRequestURI ( targetURL ); err != nil {
return nil , connect . NewError ( connect . CodeInvalidArgument ,
fmt . Errorf ( "invalid URL: %w " , err ))
}
Returns: Code.INVALID_ARGUMENT
Timeout Errors
if rpcCtx . Err () != nil {
return nil , connect . NewError ( connect . CodeDeadlineExceeded , rpcCtx . Err ())
}
Returns: Code.DEADLINE_EXCEEDED
Network Errors
return nil , connect . NewError ( connect . CodeUnavailable ,
fmt . Errorf ( "call failed: %w " , err ))
Returns: Code.UNAVAILABLE
Response Read Errors
if err != nil {
return nil , connect . NewError ( connect . CodeInternal ,
fmt . Errorf ( "read response: %w " , err ))
}
Returns: Code.INTERNAL
Service Dependencies
Upstream Dependencies
PostgreSQL : Optional, service runs without DB but won’t persist logs
External APIs : Any URL provided by the caller
Downstream Consumers
Greeter Service : Primary consumer for external API calls
Other Internal Services : Any service needing HTTP proxy functionality
Observability
Structured Logging
The service uses Go’s slog package:
logger . InfoContext ( ctx , "starting caller service" , "port" , port )
slog . Error ( "failed to insert call log" , "error" , err )
Distributed Tracing
OpenTelemetry instrumentation captures:
RPC call spans
HTTP client requests
Database operations (in background goroutines)
Error tracking
Health Checks
Health endpoint at /healthz performs database ping:
mux . HandleFunc ( "/healthz" , func ( w http . ResponseWriter , r * http . Request ) {
if dbPool != nil {
if err := dbPool . Ping ( r . Context ()); err != nil {
w . WriteHeader ( http . StatusServiceUnavailable )
_ , _ = w . Write ([] byte ( "db unhealthy \n " ))
return
}
}
w . WriteHeader ( http . StatusOK )
_ , _ = w . Write ([] byte ( "ok \n " ))
})
Testing
Test the service using grpcurl:
# Call httpbin
grpcurl -plaintext -d '{"url": "https://httpbin.org/get"}' \
localhost:8081 \
caller.v1.CallerService/CallExternal
# Test 404 response
grpcurl -plaintext -d '{"url": "https://httpbin.org/status/404"}' \
localhost:8081 \
caller.v1.CallerService/CallExternal
# Test invalid URL
grpcurl -plaintext -d '{"url": "not-a-valid-url"}' \
localhost:8081 \
caller.v1.CallerService/CallExternal
# Health check
curl http://localhost:8081/healthz
Latency : ~2-100ms (depends on external API response time)
Throughput : High - async DB writes don’t block responses
Database Write : Asynchronous, zero latency impact on responses
Timeout Budget : 2 seconds total
Memory Usage : Low - response bodies are discarded via io.Copy(io.Discard, ...)
Security Considerations
URL Validation
The service validates URLs before making requests:
if _ , err := url . ParseRequestURI ( targetURL ); err != nil {
return nil , connect . NewError ( connect . CodeInvalidArgument ,
fmt . Errorf ( "invalid URL: %w " , err ))
}
SSRF Protection
The service does not currently implement SSRF (Server-Side Request Forgery) protection. In production, you should:
Block requests to private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
Block requests to localhost/127.0.0.1
Maintain an allowlist of permitted domains
Implement request throttling per URL
Response Size Limits
Consider implementing maximum response size limits:
// Example: limit to 10MB
maxBytes := int64 ( 10 * 1024 * 1024 )
limitedReader := io . LimitReader ( httpResp . Body , maxBytes )
n , err := io . Copy ( io . Discard , limitedReader )
Common Issues
Timeout Errors
Error : Code.DEADLINE_EXCEEDED
Cause : External API taking >2 seconds to respond
Solution : Increase timeout or investigate external API performance
Database Connection Failed
Log : database unavailable, running without DB
Impact : Service runs but doesn’t persist call logs
Solution : Check DATABASE_URL and ensure PostgreSQL is accessible
Missing Call Logs
Symptom : Successful calls not appearing in database
Cause : Asynchronous write failure or service shutdown before goroutine completes
Solution : Check error logs for "failed to insert call log" messages
Comparison: Sync vs Async Patterns
Aspect Caller (Async) Greeter (Sync) Latency Impact None +1-5ms Data Guarantee Fire-and-forget Guaranteed before response Error Handling Logged only Can fail the request Backpressure No DB backpressure DB issues affect clients Best For High-throughput logging Critical data persistence
Greeter Service Primary consumer of Caller service
Services Overview Learn about async vs sync patterns