Skip to content

Commit e1c8043

Browse files
authored
Merge pull request #87 from daily-co/pcc-604
Add /readyz and /livez Kubernetes health check endpoints
2 parents 56fbbbf + c9b066c commit e1c8043

File tree

2 files changed

+92
-1
lines changed

2 files changed

+92
-1
lines changed

pipecat-base/CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,26 @@ All notable changes to the **Pipecat Cloud Base Images** will be documented in t
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### Added
11+
12+
- Added `/readyz` and `/livez` Kubernetes health check endpoints. `/livez`
13+
always returns 200 OK. `/readyz` returns 200 OK by default, but customers can
14+
override the readiness check by defining a `readyz()` function in their
15+
`bot.py` that returns either a `bool` or a `dict` with a `ready` key:
16+
```python
17+
# Simple
18+
def readyz() -> bool:
19+
return is_healthy()
20+
21+
# With details
22+
def readyz() -> dict:
23+
if not db_ok():
24+
return {"ready": False, "reason": "database unavailable"}
25+
return {"ready": True}
26+
```
27+
828
## [0.1.13] - 2026-01-19
929

1030
### Added

pipecat-base/app.py

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
#!/usr/bin/env python
22

3+
import asyncio
34
import base64
5+
import inspect
46
import json
57
import sys
68
from contextlib import asynccontextmanager
79
from os import environ
8-
from typing import Annotated, List, Optional
10+
from typing import Annotated, Callable, List, Optional, Union
911

1012
import aiohttp
13+
import bot as bot_module
1114
from bot import bot
1215
from fastapi import BackgroundTasks, FastAPI, Header, HTTPException, Query, Request, WebSocket
16+
from fastapi.responses import JSONResponse
1317
from fastapi.websockets import WebSocketState
1418
from feature_manager import FeatureKeys, FeatureManager
1519
from loguru import logger
@@ -22,6 +26,33 @@
2226
from pipecatcloud_system import add_lifespan_to_app, app
2327
from waiting_server import Config, WaitingServer
2428

29+
# ------------------------------------------------------------
30+
# Health check functions (readyz can be overridden by customer bot.py)
31+
# ------------------------------------------------------------
32+
ReadyzResult = Union[bool, dict]
33+
34+
35+
def _default_readyz() -> bool:
36+
"""Default readiness check - always returns True."""
37+
return True
38+
39+
40+
# Try to import customer-defined readyz function, fall back to default
41+
readyz_func: Callable[[], ReadyzResult] = getattr(bot_module, "readyz", _default_readyz)
42+
43+
44+
async def _call_readyz_func(func: Callable[[], ReadyzResult]) -> ReadyzResult:
45+
"""Call readyz function, handling both sync and async."""
46+
try:
47+
if asyncio.iscoroutinefunction(func) or inspect.iscoroutinefunction(func):
48+
return await func()
49+
else:
50+
return func()
51+
except Exception as e:
52+
logger.warning(f"Health check function raised exception: {e}")
53+
return {"ready": False, "error": str(e)}
54+
55+
2556
# Global state dictionary
2657
GLOBALS = {}
2758

@@ -100,7 +131,47 @@ async def run_bot(args: SessionArguments, transport_type: Optional[str] = None):
100131
GLOBALS["pipecat_session_body"] = None
101132

102133

134+
# ------------------------------------------------------------
135+
# Health check routes (Kubernetes probes)
136+
# ------------------------------------------------------------
137+
@app.get("/readyz")
138+
async def readyz():
139+
"""Kubernetes readiness probe endpoint.
140+
141+
Override by defining a `readyz()` function in your bot.py that returns either:
142+
- bool: True for ready, False for not ready
143+
- dict: Must contain "ready" key (bool), can include additional info
144+
"""
145+
result = await _call_readyz_func(readyz_func)
146+
147+
# Handle bool return type
148+
if isinstance(result, bool):
149+
if result:
150+
return JSONResponse(content={"status": "ok"}, status_code=200)
151+
return JSONResponse(content={"status": "not ready"}, status_code=503)
152+
153+
# Handle dict return type
154+
if isinstance(result, dict):
155+
is_ready = result.get("ready", False)
156+
status_code = 200 if is_ready else 503
157+
return JSONResponse(content=result, status_code=status_code)
158+
159+
# Unexpected return type
160+
logger.warning(f"readyz() returned unexpected type: {type(result)}")
161+
return JSONResponse(
162+
content={"status": "error", "error": "invalid readyz return type"}, status_code=503
163+
)
164+
165+
166+
@app.get("/livez")
167+
async def livez():
168+
"""Kubernetes liveness probe endpoint."""
169+
return JSONResponse(content={"status": "ok"}, status_code=200)
170+
171+
172+
# ------------------------------------------------------------
103173
# Basic routes (always available)
174+
# ------------------------------------------------------------
104175
@app.post("/bot")
105176
async def handle_bot_request(
106177
body: dict,

0 commit comments

Comments
 (0)