Skip to content

Commit db31eb0

Browse files
Feature/grpc server (#15)
* add new config MAIN_GRPC_PORT; * update and add lib for grpc; * add common server success status; * add common server success status; * refactor lifespan to manage multiple servers; * add interceptor for grpc server logger; * init grpc server; * update README.md; * add PostPRouter V1; * add PostPRouter V1; * remove an unused file; * update instruction about proto;
1 parent 9a7b1cd commit db31eb0

File tree

20 files changed

+895
-423
lines changed

20 files changed

+895
-423
lines changed

Makefile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,8 @@ restart:
1919

2020
format:
2121
isort . && ruff format $(pwd)
22-
.PHONY: format
22+
.PHONY: format
23+
24+
gen-proto:
25+
uv run -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. ./internal/controllers/grpc/protos/*.proto
26+
.PHONY: gen-proto

README.md

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,15 @@ The **Unit of Work pattern** ensures that multiple repository operations are **e
6767

6868
```
6969
internal/
70+
│── app/ # Defines servers and middlewares (protocols)
7071
│── controllers/ # Handles requests and responses (endpoints)
7172
│── domains/ # Core business logic (services, use cases, entities)
7273
│── infrastructures/ # External dependencies (databases, caches, queues)
7374
│── patterns/ # Dependency injection
7475
└── main.py # Application entry point
7576
```
7677

78+
- **App**: Define servers (protocols) and middlewares.
7779
- **Controllers**: Define the API endpoints and route requests to services.
7880
- **Domains**: Contains core business logic, including services and use cases.
7981
- **Infrastructures**: Houses repositories and database interactions.
@@ -183,15 +185,28 @@ Ensure you have the following installed:
183185
Note: Our internal/infrastructures/config_manager including the auto-reload configs function.
184186
```
185187

186-
7. **Start the Application**
188+
7. **Manage proto files**
189+
190+
```sh
191+
This section is optional if you want to develop service with gRPC.
192+
193+
Every proto files are stored at "internal/controllers/grpc/protos"
194+
195+
Use the following command if you want to generate new proto:
196+
197+
# gen-proto
198+
make gen-proto
199+
```
200+
201+
8. **Start the Application**
187202
188203
```sh
189204
python main.py
190205
# or
191206
./run.sh
192207
```
193208
194-
8. **Alternative run with docker**
209+
9. **Alternative run with docker**
195210
196211
```sh
197212
# if you do not want to start from scratch, just run with docker
@@ -211,7 +226,7 @@ Ensure you have the following installed:
211226

212227
```
213228
214-
9. **Access the API**
229+
10. **Access the API**
215230
216231
- Use `http://127.0.0.1:8082/docs` for Swagger UI.
217232
- Use `http://127.0.0.1:8082/redoc` for Redoc documentation.

config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ class AppConfig(BaseSettings):
9292
# === General App Settings ===
9393
main_http_port: Optional[int] = 8080
9494
health_check_http_port: Optional[int] = 5000
95+
main_grpc_port: Optional[int] = 9090
9596
log_level: Optional[str] = "INFO"
9697
uvicorn_workers: Optional[int] = 1
9798

config/.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# === General App Settings ===
22
MAIN_HTTP_PORT=8082
33
HEALTH_CHECK_HTTP_PORT=5000
4+
MAIN_GRPC_PORT=9090
45
LOG_LEVEL=INFO
56
UVICORN_WORKERS=1
67

internal/app/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
from .middlewares import JWTAuthMiddleware
2-
from .servers import init_health_check_server, init_http_server
2+
from .servers import init_grpc_server, init_health_check_server, init_http_server

internal/app/servers.py

Lines changed: 25 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,23 @@
1-
from contextlib import asynccontextmanager
2-
1+
import grpc
32
from fastapi import FastAPI, HTTPException
43
from fastapi.encoders import jsonable_encoder
54
from fastapi.responses import ORJSONResponse
65
from starlette.middleware.cors import CORSMiddleware
76
from starlette.requests import Request
87

9-
from config import app_config
108
from internal.app import JWTAuthMiddleware
9+
from internal.controllers.grpc.protos import post_v1_pb2_grpc
10+
from internal.controllers.grpc.v1.endpoints import PostPRouter as PostPRouterV1
1111
from internal.controllers.http.v1.routes import api_router as api_router_v1
1212
from internal.controllers.responses import DataResponse, MessageResponse
13-
from internal.infrastructures.config_manager import ConfigManager
14-
from internal.patterns import Container, initialize_relational_db
15-
from internal.patterns.dependency_injection import close_relational_db
16-
from utils.logger_utils import get_shared_logger
13+
from internal.patterns import Container
14+
from utils.logger_utils import GRPCLoggingInterceptor, get_shared_logger
1715

1816
logger = get_shared_logger()
1917

20-
app_status = {"alive": True, "status_code": 200, "message": "I'm fine"}
21-
2218

2319
def init_http_server() -> FastAPI:
24-
@asynccontextmanager
25-
async def lifespan(app: FastAPI):
26-
try:
27-
# Get the container instance
28-
container = Container()
29-
30-
# Load config from the config manager
31-
if app_config.cfg_manager_service.enable:
32-
cfg_manager = ConfigManager(
33-
address=app_config.cfg_manager_service.url,
34-
token=app_config.cfg_manager_service.token,
35-
env=app_config.cfg_manager_service.env,
36-
app_config=app_config,
37-
di_container=container,
38-
)
39-
await cfg_manager.load()
40-
await cfg_manager.update_app_config()
41-
logger.info(f"Load config from server successfully")
42-
else:
43-
container.config.from_dict(app_config.model_dump())
44-
logger.info(f"Load config from local successfully")
45-
46-
# Initialize relational database
47-
await initialize_relational_db(container=container)
48-
logger.info("Relational database initialized")
49-
50-
yield
51-
52-
# Close relational database
53-
await close_relational_db(container=container)
54-
logger.info("Relational database closed")
55-
except Exception as exc:
56-
logger.error(f"Main HTTP server crashed due to: {exc}")
57-
app_status["alive"] = False
58-
app_status["status_code"] = 500
59-
app_status["message"] = str(exc)
60-
61-
server_ = FastAPI(default_response_class=ORJSONResponse, lifespan=lifespan)
20+
server_ = FastAPI(default_response_class=ORJSONResponse)
6221

6322
server_.add_middleware(
6423
middleware_class=CORSMiddleware,
@@ -95,13 +54,28 @@ async def http_exception_handler(_: Request, exc: HTTPException):
9554
return server_
9655

9756

98-
def init_health_check_server() -> FastAPI:
57+
def init_health_check_server(app_status: DataResponse) -> FastAPI:
9958
health_check_app = FastAPI()
10059

10160
@health_check_app.get("/health-check")
10261
async def health_check():
103-
if app_status["status_code"] != 200:
104-
logger.info(app_status)
105-
return ORJSONResponse(content=app_status, status_code=app_status["status_code"])
62+
return ORJSONResponse(
63+
content=jsonable_encoder(app_status),
64+
status_code=app_status.message.status_code,
65+
)
10666

10767
return health_check_app
68+
69+
70+
def init_grpc_server(container: Container) -> grpc.aio.Server:
71+
logging_interceptor = GRPCLoggingInterceptor()
72+
server = grpc.aio.server(interceptors=[logging_interceptor])
73+
74+
# Instantiate your service
75+
post_p_router_v1 = PostPRouterV1(container=container)
76+
77+
# Add your service to the gRPC server
78+
post_v1_pb2_grpc.add_PostV1Servicer_to_server(post_p_router_v1, server)
79+
80+
logger.info("gRPC server initialized")
81+
return server
File renamed without changes.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
syntax = "proto3";
2+
3+
// Defines a package for the protocol buffer messages to prevent name clashes.
4+
// It's good practice to include a version number (e.g., v1).
5+
6+
service PostV1 {
7+
rpc Create (CreatePostRequest) returns (CreatePostResponse) {}
8+
rpc GetById (GetPostByIdRequest) returns (GetPostByIdResponse) {}
9+
}
10+
11+
message CreatePostRequest {
12+
string text_content = 1;
13+
string owner_id = 2;
14+
}
15+
16+
message CreatePostResponse {
17+
string id = 1;
18+
}
19+
20+
message GetPostByIdRequest {
21+
string id = 1;
22+
}
23+
24+
message GetPostByIdResponse {
25+
string id = 1;
26+
string text_content = 2;
27+
string created_at = 3;
28+
string updated_at = 4;
29+
}

internal/controllers/grpc/protos/post_v1_pb2.py

Lines changed: 48 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)