Architecture

This document provides an overview of the kibana-py architecture, design patterns, and key architectural decisions.

Project Structure

kibana/                      # Main package
├── __init__.py             # Public API exports
├── _version.py             # Version string
├── exceptions.py           # Exception hierarchy
├── serializer.py           # JSON serialization
├── observability.py        # OpenTelemetry integration
├── _utils.py               # Internal utilities
├── _sync/                  # Synchronous client implementation
│   ├── __init__.py
│   └── client/
│       ├── __init__.py     # Kibana client exports
│       ├── _base.py        # BaseClient implementation
│       ├── actions.py      # ActionsClient
│       ├── spaces.py       # SpacesClient
│       ├── saved_objects.py # SavedObjectsClient
│       ├── status.py       # StatusClient
│       └── utils.py        # NamespaceClient and utilities
└── _async/                 # Asynchronous client implementation
    ├── __init__.py
    └── client/
        ├── __init__.py
        ├── _base.py        # AsyncBaseClient
        ├── actions.py      # AsyncActionsClient
        ├── spaces.py       # AsyncSpacesClient
        ├── saved_objects.py # AsyncSavedObjectsClient
        ├── status.py       # AsyncStatusClient
        └── utils.py        # AsyncNamespaceClient

Core Architectural Principles

1. Consistency Over Convenience

All API clients follow identical patterns for similar operations:

  • Space support works the same way across all clients

  • Error handling is consistent

  • Parameter naming follows conventions

  • Response handling is uniform

2. Extensibility by Design

The architecture supports the full Kibana REST API:

  • Base classes provide common functionality

  • New clients can be added without architectural changes

  • Composition and inheritance patterns scale to dozens of clients

  • Plugin-specific APIs follow the same patterns

3. Developer Experience First

  • Pythonic interfaces over direct API mapping

  • Automatic configuration detection

  • Clear error messages with actionable guidance

  • Consistent parameter naming and behavior

4. Performance and Efficiency

  • Zero overhead when features aren’t used

  • Lazy initialization of client properties

  • Efficient caching with configurable TTL

  • Minimal API calls through intelligent validation

Client Hierarchy

Base Client Architecture

BaseClient (transport, auth, core request handling)
├── NamespaceClient (space support, common utilities)
│   ├── ActionsClient (connector operations)
│   ├── SavedObjectsClient (saved object operations)
│   ├── SpacesClient (space management)
│   └── [Future API clients]
└── SpaceScopedKibana (space context wrapper)

BaseClient

The BaseClient provides core functionality:

class BaseClient:
    """Base client with transport and authentication."""

    def __init__(
        self,
        hosts: str | list[str] | None = None,
        *,
        api_key: str | tuple[str, str] | None = None,
        basic_auth: tuple[str, str] | None = None,
        bearer_auth: str | None = None,
        _transport: Transport | None = None,
    ):
        # Initialize transport
        # Set up authentication
        # Configure serialization

Responsibilities:

  • Transport initialization and management

  • Authentication header resolution

  • Request execution via elastic-transport

  • Response processing and error handling

  • Options pattern for per-request configuration

NamespaceClient

The NamespaceClient extends BaseClient with space support:

class NamespaceClient(BaseClient):
    """Base client with space support."""

    def __init__(
        self,
        base_client: BaseClient,
        *,
        default_space_id: str | None = None,
        validate_spaces: bool = True,
    ):
        # Inherit from base client
        # Set up space validation
        # Initialize cache

Responsibilities:

  • Space path construction (/s/{space_id}/api/...)

  • Space ID format validation

  • Space existence validation with caching

  • Cache management (5-minute TTL)

  • Error enhancement with space context

API Clients

Individual API clients inherit from NamespaceClient:

class ActionsClient(NamespaceClient):
    """Client for Kibana Actions API."""

    def create(self, *, name: str, ..., space_id: str | None = None):
        path = self._build_space_path("/api/actions/connector", space_id)
        return self.perform_request(method="POST", path=path, body=...)

Responsibilities:

  • API-specific method implementations

  • Request body construction

  • Response parsing

  • API-specific error handling

Key Design Patterns

1. Space Support Pattern

All clients that support spaces use the same pattern:

def method(
    self,
    *,
    param: str,
    space_id: str | None = None,
    validate_space: bool | None = None,
) -> ObjectApiResponse[dict[str, Any]]:
    """Method with space support."""
    # Override validation if specified
    original_validate = self._validate_spaces
    if validate_space is not None:
        self._validate_spaces = validate_space

    try:
        # Build space-aware path
        path = self._build_space_path("/api/endpoint", space_id)

        # Make request
        return self.perform_request(method="POST", path=path, body=...)
    finally:
        # Restore original setting
        self._validate_spaces = original_validate

Benefits:

  • Consistent API across all clients

  • Automatic space validation

  • Per-operation validation override

  • Efficient caching

2. Authentication Resolution

Authentication is resolved in order of precedence:

  1. API key (string or tuple format)

  2. Basic auth (username/password tuple)

  3. Bearer token (string)

def resolve_auth_headers(
    api_key: str | tuple[str, str] | None = None,
    basic_auth: tuple[str, str] | None = None,
    bearer_auth: str | None = None,
) -> dict[str, str]:
    """Resolve authentication to HTTP headers."""
    if api_key:
        # Handle API key
    elif basic_auth:
        # Handle basic auth
    elif bearer_auth:
        # Handle bearer token
    return headers

3. Error Handling

Custom exception hierarchy rooted at KibanaException:

class KibanaException(Exception):
    """Base exception for all Kibana errors."""

class ApiError(KibanaException):
    """Base class for API errors."""

    def __init__(self, message: str, meta: ApiResponseMeta, body: Any):
        self.message = message
        self.meta = meta
        self.body = body

class NotFoundError(ApiError):
    """404 Not Found."""

class BadRequestError(ApiError):
    """400 Bad Request."""

class SpaceNotFoundError(NotFoundError):
    """Space not found error."""

    def __init__(self, space_id: str, *args, **kwargs):
        self.space_id = space_id
        super().__init__(*args, **kwargs)

HTTP status codes are mapped to specific exceptions:

HTTP_EXCEPTIONS = {
    400: BadRequestError,
    401: UnauthorizedError,
    403: ForbiddenError,
    404: NotFoundError,
    409: ConflictError,
    # ...
}

4. Serialization

Abstract Serializer base class with multiple implementations:

class Serializer(ABC):
    """Abstract serializer interface."""

    @abstractmethod
    def dumps(self, data: Any) -> bytes:
        """Serialize data to bytes."""

    @abstractmethod
    def loads(self, data: bytes) -> Any:
        """Deserialize bytes to data."""

class JSONSerializer(Serializer):
    """Standard library JSON serializer."""

class OrjsonSerializer(Serializer):
    """High-performance orjson serializer."""

The serializer is auto-selected based on availability:

  • OrjsonSerializer if orjson is installed

  • JSONSerializer as fallback

5. Options Pattern

Per-request configuration via the options() method:

# Override request timeout
response = client.options(request_timeout=30).actions.get(id="connector-id")

# Add custom headers
response = client.options(headers={"X-Custom": "value"}).actions.get(id="connector-id")

# Combine multiple options
response = client.options(
    request_timeout=30,
    headers={"X-Custom": "value"}
).actions.get(id="connector-id")

Sync/Async Architecture

Dual Implementation

kibana-py provides both synchronous and asynchronous clients:

  • Sync: kibana._sync.client.*

  • Async: kibana._async.client.*

Both implementations follow the same patterns and provide identical APIs.

Shared Components

Components shared between sync and async:

  • Exception hierarchy (exceptions.py)

  • Serialization (serializer.py)

  • Observability (observability.py)

  • Utilities (_utils.py)

Implementation Differences

Key differences between sync and async:

# Sync
class BaseClient:
    def perform_request(self, method: str, path: str, **kwargs):
        return self._transport.perform_request(method, path, **kwargs)

# Async
class AsyncBaseClient:
    async def perform_request(self, method: str, path: str, **kwargs):
        return await self._transport.perform_request(method, path, **kwargs)

Observability Integration

OpenTelemetry Support

Built-in OpenTelemetry instrumentation:

from kibana import configure_opentelemetry

configure_opentelemetry(
    enabled=True,
    service_name="my-app",
    exporter="otlp",
    endpoint="http://localhost:4317"
)

Features:

  • Automatic span creation for API calls

  • Trace context propagation

  • Log-trace correlation

  • Structured logging

  • APM integration

Instrumentation Points

  • HTTP requests to Kibana API

  • Space validation operations

  • Cache hits/misses

  • Error conditions

Caching Strategy

Space Validation Cache

Space validation results are cached to minimize API calls:

class NamespaceClient:
    def __init__(self, ...):
        self._space_cache: dict[str, tuple[bool, float]] = {}
        self._cache_ttl: float = 300.0  # 5 minutes

    def _is_space_cached(self, space_id: str) -> bool:
        """Check if space validation is cached."""
        if space_id in self._space_cache:
            is_valid, timestamp = self._space_cache[space_id]
            if time.time() - timestamp < self._cache_ttl:
                return True
        return False

Cache Characteristics:

  • 5-minute TTL by default

  • Per-client cache (not global)

  • Automatic invalidation on TTL expiry

  • Manual cache clearing available

Extension Points

Adding New API Clients

To add a new API client:

  1. Inherit from NamespaceClient:

    class NewAPIClient(NamespaceClient):
        """Client for New API."""
    
  2. Implement methods:

    def create(self, *, name: str, space_id: str | None = None):
        path = self._build_space_path("/api/new-endpoint", space_id)
        return self.perform_request(method="POST", path=path, body=...)
    
  3. Add to main client:

    class Kibana(BaseClient):
        @property
        def new_api(self) -> NewAPIClient:
            if not hasattr(self, "_new_api"):
                self._new_api = NewAPIClient(self)
            return self._new_api
    

See Adding Space Support to New API Clients for detailed instructions.

Custom Authentication

To add custom authentication:

  1. Extend resolve_auth_headers:

    def resolve_auth_headers(..., custom_auth: str | None = None):
        if custom_auth:
            return {"X-Custom-Auth": custom_auth}
        # ... existing logic
    
  2. Update BaseClient:

    class BaseClient:
        def __init__(self, ..., custom_auth: str | None = None):
            headers = resolve_auth_headers(..., custom_auth=custom_auth)
    

Custom Serializers

To add a custom serializer:

class CustomSerializer(Serializer):
    """Custom serializer implementation."""

    def dumps(self, data: Any) -> bytes:
        # Custom serialization logic
        pass

    def loads(self, data: bytes) -> Any:
        # Custom deserialization logic
        pass

Design Decisions

Why NamespaceClient?

Decision: Create a separate NamespaceClient base class for space support.

Rationale:

  • Not all Kibana APIs support spaces

  • Separates concerns (base functionality vs. space support)

  • Allows clients to opt-in to space support

  • Provides consistent space support across all clients

Alternatives Considered:

  • Adding space support directly to BaseClient (rejected: not all APIs support spaces)

  • Manual space path construction in each client (rejected: inconsistent, error-prone)

Why Lazy Property Initialization?

Decision: Use lazy initialization for API client properties.

Rationale:

  • Zero overhead for unused clients

  • Simpler main client initialization

  • Allows per-client configuration

Implementation:

@property
def actions(self) -> ActionsClient:
    if not hasattr(self, "_actions"):
        self._actions = ActionsClient(self)
    return self._actions

Why Keyword-Only Parameters?

Decision: Use keyword-only parameters for all client methods.

Rationale:

  • Prevents positional argument errors

  • Makes code more readable

  • Allows adding new parameters without breaking changes

  • Follows Python best practices

Implementation:

def create(
    self,
    *,  # Force keyword-only
    name: str,
    config: dict[str, Any],
):
    pass

Why Separate Sync/Async Implementations?

Decision: Maintain separate _sync and _async packages.

Rationale:

  • Clear separation of concerns

  • No async overhead for sync users

  • Easier to maintain and test

  • Follows patterns from other Python clients

Alternatives Considered:

  • Single implementation with async/await everywhere (rejected: overhead for sync users)

  • Wrapper approach (rejected: complexity, performance)

Performance Considerations

Space Validation Overhead

Space validation adds minimal overhead:

  • First call: ~50-100ms (API call)

  • Cached calls: <1ms (cache lookup)

  • Cache hit ratio: >95% in typical usage

Memory Usage

  • Base client: ~1KB

  • Each API client: ~500 bytes

  • Space cache: ~100 bytes per space

  • Total for typical usage: <10KB

Network Efficiency

  • Connection pooling via elastic-transport

  • HTTP keep-alive enabled by default

  • Configurable retry logic

  • Request/response compression support

Testing Architecture

Unit Tests

  • Mock transport layer

  • Test client logic in isolation

  • Fast execution (<10 seconds total)

  • High coverage (>90%)

Integration Tests

  • Real Kibana instance

  • Test actual API interactions

  • Resource cleanup

  • Graceful degradation

See Testing Guide for detailed testing guidelines.

Future Considerations

Planned Enhancements

  • Bulk Operations: Consistent bulk operation support across clients

  • Async Improvements: Enhanced async patterns and concurrency

  • Plugin APIs: Support for plugin-specific APIs

  • Advanced Caching: Configurable cache strategies

Extensibility

The architecture is designed to support:

  • 50+ API clients without changes

  • Plugin-specific APIs

  • Custom authentication methods

  • Advanced caching strategies

  • API versioning

Additional Resources