Skip to main content

A modern annotation-based library for registering FastAPI services with Consul service discovery

Project description

Service Discovery

A Python library for microservice registration and discovery using Consul. This is the Python counterpart to service-discovery-java, providing decorator-based service registration and client-side load balancing for FastAPI applications.

Installation

pip install prtc-service-discovery

Note: While the package is named prtc-service-discovery on PyPI, you import it as service_discovery in your code.

Prerequisites

  • Python 3.10+
  • FastAPI application
  • Consul server (for service registration/discovery)

Quick Start

Service Registration

from fastapi import FastAPI, APIRouter
from service_discovery import register_service, create_consul_lifespan

app = FastAPI(lifespan=create_consul_lifespan)

api_router = APIRouter(prefix="/api/v1")

@register_service("user-service")
class UserService:
    router = api_router
    
    @router.get("/users/{id}")
    async def get_user(id: str):
        return {"id": id, "name": "John Doe"}

app.include_router(api_router)

@app.get("/health")
async def health():
    return {"status": "healthy"}

Service Discovery

from service_discovery import create_service_discovery

# Discover services
discovery = create_service_discovery()

# Get all SERVICE-tagged services
services = await discovery.get_services()
# Returns: {"user-service": ["http://10.0.0.1:8080/api/v1", "http://10.0.0.2:8080/api/v1"]}

# Get a random URI (client-side load balancing)
uri = await discovery.get_service_uri("user-service")
# Returns: "http://10.0.0.1:8080/api/v1"

# Get all URIs for a service
uris = await discovery.get_all_service_uris("user-service")
# Returns: ["http://10.0.0.1:8080/api/v1", "http://10.0.0.2:8080/api/v1"]

# Cleanup
await discovery.close()

Configuration

Configuration is handled through environment variables. All settings are optional with sensible defaults:

# Consul connection
CONSUL_HOST=localhost           # Consul server host (default: localhost)
CONSUL_PORT=8500               # Consul server port (default: 8500)

# Service networking
ACCESS_HOST=my-service.local   # Hostname/IP other services use to reach this service
ACCESS_PORT=8080              # Port other services use to reach this service

# Feature flags
ENABLE_REGISTRATION=true       # Enable/disable Consul registration (default: true)

# Health check networking (optional, defaults to ACCESS_HOST:ACCESS_PORT)
HEALTH_HOST=0.0.0.0           # Interface for health checks
HEALTH_PORT=8080              # Port for health checks

Service Types

The library provides three decorators for different service types:

Decorator Consul Tag Discoverable Use Case
@register_service SERVICE ✅ Yes REST APIs, gRPC services, and other client-facing services
@register_worker WORKER ❌ No
@register_indexer INDEXER ❌ No

Only services registered with @register_service are discoverable via the ServiceDiscovery client. Workers and indexers are registered in Consul for monitoring but are not included in service discovery results.

Testing

Unit Testing

from service_discovery import get_service_registry

def test_service_registration():
    get_service_registry().clear()
    
    @register_service("test-service", base_route="/api/v1")
    class TestService:
        pass
    
    services = get_service_registry().get_all_services()
    assert len(services) == 1
    assert services[0].name == "test-service"

Integration Testing

from service_discovery.testing import ConsulRegistrationTestBase, ExpectedService

class TestMyApp(ConsulRegistrationTestBase):
    def get_expected_services(self) -> list[ExpectedService]:
        return [
            ExpectedService.api_service("user-service", 8080),
            ExpectedService.worker("pdf-processor", 8080),
        ]
    
    def create_app(self) -> FastAPI:
        from my_app import app
        return app

The base test class automatically verifies:

  • Service registration with correct metadata
  • Health check configuration
  • Service deregistration on shutdown

Advanced Usage

Custom Configuration

from service_discovery import DiscoveryConfig, create_consul_lifespan

config = DiscoveryConfig(
    consul__host="consul.prod",
    consul__port=8500,
    access__host="api.example.com",
    access__port=443,
    health__host="internal.example.com",  # Separate health check network
    health__port=8080,
    enable_registration=True
)

app = FastAPI(lifespan=lambda app: create_consul_lifespan(app, config))

Multiple Service Registrations

@register_service("auth-service", base_route="/api/auth/v1")
class AuthService:
    router = APIRouter()

@register_worker("email-worker", base_route="/api/workers/email/v1")
class EmailWorker:
    router = APIRouter()

# Include all routers
for service in [AuthService(), EmailWorker()]:
    app.include_router(service.router)

Service Discovery Patterns

# Singleton pattern
discovery = create_service_discovery()

async def make_user_service_call(user_id: str):
    # Get a random instance (client-side load balancing)
    uri = await discovery.get_service_uri("user-service")
    if not uri:
        raise ServiceUnavailableError("user-service not found")
    
    async with httpx.AsyncClient() as client:
        response = await client.get(f"{uri}/users/{user_id}")
        return response.json()

# Context manager pattern
async def get_all_services():
    discovery = create_service_discovery()
    try:
        return await discovery.get_services()
    finally:
        await discovery.close()

Architecture Notes

Service Discovery

  • Only discovers services tagged with SERVICE (not WORKER or INDEXER)
  • Uses Consul's catalog API for real-time service discovery
  • Implements client-side random load balancing
  • Caches service data with configurable refresh interval (default: 30s)
  • Automatic retry and error handling for network failures

Service Registration

  • Automatic registration on app startup via FastAPI lifespan events
  • Built-in health check endpoint configuration
  • Generates unique service IDs per instance (consistent within a session)
  • Automatically extracts base routes from FastAPI routers
  • Graceful deregistration on shutdown

Key Differences from Java Library

  • No Stork integration (uses simple random selection for load balancing)
  • Discovery returns HTTP URIs directly instead of stork:// URIs
  • No static service configuration fallback
  • Async-first design using python-consul2
  • Decorator-based registration instead of annotations

Development

Setup

This project uses Hatchling for packaging (not Poetry, as libraries should not pin dependencies). For development, create a virtual environment:

# Create and activate virtual environment
python -m venv .venv
source .venv/bin/activate  # On macOS/Linux
# .venv\Scripts\activate   # On Windows

# Install package in editable mode with dev dependencies
pip install -e ".[dev,test]"

Running Tests & Quality Checks

# Run tests
make test           # All tests
make test-unit      # Unit tests only
make test-example   # Example app tests

# Code quality
make format         # Format with black
make lint          # Lint with ruff
make type-check    # Type check with mypy
make all           # All checks + tests

Troubleshooting

Services not registering

  • Ensure ENABLE_REGISTRATION=true (or not set, as true is the default)
  • Check that ACCESS_HOST and ACCESS_PORT are set correctly
  • Verify Consul is reachable at CONSUL_HOST:CONSUL_PORT
  • Check application logs for registration errors

Services not discoverable

  • Verify the service is registered with @register_service (not @register_worker or @register_indexer)
  • Check that the service is healthy in Consul UI
  • Ensure the service has the SERVICE tag in Consul

Health checks failing

  • Verify the /health endpoint is accessible at HEALTH_HOST:HEALTH_PORT
  • If using separate health check networking, ensure HEALTH_HOST is reachable from Consul
  • Check that the health endpoint returns a 2xx status code

License

Proprietary - Perceptic Technologies Ltd.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

prtc_service_discovery-0.3.0.tar.gz (40.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

prtc_service_discovery-0.3.0-py3-none-any.whl (20.2 kB view details)

Uploaded Python 3

File details

Details for the file prtc_service_discovery-0.3.0.tar.gz.

File metadata

  • Download URL: prtc_service_discovery-0.3.0.tar.gz
  • Upload date:
  • Size: 40.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.12.9

File hashes

Hashes for prtc_service_discovery-0.3.0.tar.gz
Algorithm Hash digest
SHA256 c49e516235de9d87e33b1ffb099f250be6ba39899a5432f2316db43e575e9460
MD5 bc37e8ed4ba768a07b9ba4550067ea13
BLAKE2b-256 f021c204a97b29c2ab0c42a58bcbf839aa91bce105c99bd8275d4d74a2893fa4

See more details on using hashes here.

File details

Details for the file prtc_service_discovery-0.3.0-py3-none-any.whl.

File metadata

File hashes

Hashes for prtc_service_discovery-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 566e5d98140a67816adec4e020c2f97716f0993164c8aba187344b520153ec31
MD5 13090ce91eb915f46e1fdc385bddd9bd
BLAKE2b-256 9bc1ce1055b3e3cff7740d511e072abfd647ca9098c6f57a60adce2c58e80820

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page