mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-11-23 23:13:10 -05:00
Add /health & /ready for every service (#1147)
This commit is contained in:
commit
1caff13adc
@ -14,6 +14,7 @@ import { showsH } from "./controllers/shows/shows";
|
|||||||
import { staffH } from "./controllers/staff";
|
import { staffH } from "./controllers/staff";
|
||||||
import { studiosH } from "./controllers/studios";
|
import { studiosH } from "./controllers/studios";
|
||||||
import { videosReadH, videosWriteH } from "./controllers/videos";
|
import { videosReadH, videosWriteH } from "./controllers/videos";
|
||||||
|
import { db } from "./db";
|
||||||
import type { KError } from "./models/error";
|
import type { KError } from "./models/error";
|
||||||
|
|
||||||
export const base = new Elysia({ name: "base" })
|
export const base = new Elysia({ name: "base" })
|
||||||
@ -58,6 +59,33 @@ export const base = new Elysia({ name: "base" })
|
|||||||
detail: { description: "Check if the api is healthy." },
|
detail: { description: "Check if the api is healthy." },
|
||||||
response: { 200: t.Object({ status: t.Literal("healthy") }) },
|
response: { 200: t.Object({ status: t.Literal("healthy") }) },
|
||||||
})
|
})
|
||||||
|
.get(
|
||||||
|
"/ready",
|
||||||
|
async ({ status }) => {
|
||||||
|
try {
|
||||||
|
await db.execute("select 1");
|
||||||
|
return { status: "healthy", database: "healthy" } as const;
|
||||||
|
} catch (e) {
|
||||||
|
return status(500, {
|
||||||
|
status: "unhealthy",
|
||||||
|
database: e,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
detail: { description: "Check if the api is healthy." },
|
||||||
|
response: {
|
||||||
|
200: t.Object({
|
||||||
|
status: t.Literal("healthy"),
|
||||||
|
database: t.Literal("healthy"),
|
||||||
|
}),
|
||||||
|
500: t.Object({
|
||||||
|
status: t.Literal("unhealthy"),
|
||||||
|
database: t.Any(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
.as("global");
|
.as("global");
|
||||||
|
|
||||||
export const prefix = "/api";
|
export const prefix = "/api";
|
||||||
|
|||||||
@ -120,8 +120,8 @@ export const migrate = async () => {
|
|||||||
await db.execute(
|
await db.execute(
|
||||||
sql.raw(`
|
sql.raw(`
|
||||||
create extension if not exists pg_trgm;
|
create extension if not exists pg_trgm;
|
||||||
SET pg_trgm.word_similarity_threshold = 0.4;
|
set pg_trgm.word_similarity_threshold = 0.4;
|
||||||
ALTER DATABASE "${postgresConfig.database}" SET pg_trgm.word_similarity_threshold = 0.4;
|
alter database "${postgresConfig.database}" set pg_trgm.word_similarity_threshold = 0.4;
|
||||||
`),
|
`),
|
||||||
);
|
);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|||||||
@ -259,7 +259,7 @@ const touchUser = `-- name: TouchUser :exec
|
|||||||
update
|
update
|
||||||
keibi.users
|
keibi.users
|
||||||
set
|
set
|
||||||
last_used = now()::timestamptz
|
last_seen = now()::timestamptz
|
||||||
where
|
where
|
||||||
pk = $1
|
pk = $1
|
||||||
`
|
`
|
||||||
|
|||||||
27
auth/main.go
27
auth/main.go
@ -59,7 +59,27 @@ func (v *Validator) Validate(i any) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) CheckHealth(c echo.Context) error {
|
func (h *Handler) CheckHealth(c echo.Context) error {
|
||||||
return c.JSON(200, struct{ Status string }{Status: "healthy"})
|
return c.JSON(200, struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
}{Status: "healthy"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) CheckReady(c echo.Context) error {
|
||||||
|
_, err := h.rawDb.Exec(c.Request().Context(), "select 1")
|
||||||
|
|
||||||
|
status := "healthy"
|
||||||
|
db := "healthy"
|
||||||
|
ret := 200
|
||||||
|
if err != nil {
|
||||||
|
status = "unhealthy"
|
||||||
|
db = err.Error()
|
||||||
|
ret = 500
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(ret, struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Database string `json:"database"`
|
||||||
|
}{Status: status, Database: db})
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetenvOr(env string, def string) string {
|
func GetenvOr(env string, def string) string {
|
||||||
@ -135,6 +155,7 @@ func OpenDatabase() (*pgxpool.Pool, error) {
|
|||||||
|
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
db *dbc.Queries
|
db *dbc.Queries
|
||||||
|
rawDb *pgxpool.Pool
|
||||||
config *Configuration
|
config *Configuration
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -210,7 +231,8 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
h := Handler{
|
h := Handler{
|
||||||
db: dbc.New(db),
|
db: dbc.New(db),
|
||||||
|
rawDb: db,
|
||||||
}
|
}
|
||||||
conf, err := LoadConfiguration(h.db)
|
conf, err := LoadConfiguration(h.db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -228,6 +250,7 @@ func main() {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
g.GET("/health", h.CheckHealth)
|
g.GET("/health", h.CheckHealth)
|
||||||
|
g.GET("/ready", h.CheckReady)
|
||||||
|
|
||||||
r.GET("/users", h.ListUsers)
|
r.GET("/users", h.ListUsers)
|
||||||
r.GET("/users/:id", h.GetUser)
|
r.GET("/users/:id", h.GetUser)
|
||||||
|
|||||||
@ -47,7 +47,7 @@ limit 1;
|
|||||||
update
|
update
|
||||||
keibi.users
|
keibi.users
|
||||||
set
|
set
|
||||||
last_used = now()::timestamptz
|
last_seen = now()::timestamptz
|
||||||
where
|
where
|
||||||
pk = $1;
|
pk = $1;
|
||||||
|
|
||||||
|
|||||||
@ -19,7 +19,7 @@ GET {{host}}/users/me
|
|||||||
Authorization: Bearer {{jwt}}
|
Authorization: Bearer {{jwt}}
|
||||||
HTTP 200
|
HTTP 200
|
||||||
[Captures]
|
[Captures]
|
||||||
register_info: body
|
register_id: jsonpath "$.id"
|
||||||
[Asserts]
|
[Asserts]
|
||||||
jsonpath "$.username" == "login-user"
|
jsonpath "$.username" == "login-user"
|
||||||
|
|
||||||
@ -48,7 +48,7 @@ Authorization: Bearer {{jwt}}
|
|||||||
HTTP 200
|
HTTP 200
|
||||||
[Asserts]
|
[Asserts]
|
||||||
jsonpath "$.username" == "login-user"
|
jsonpath "$.username" == "login-user"
|
||||||
body == {{register_info}}
|
jsonpath "$.id" == {{register_id}}
|
||||||
|
|
||||||
|
|
||||||
# Invalid password login
|
# Invalid password login
|
||||||
|
|||||||
@ -190,6 +190,9 @@ func (h *Handler) GetMe(c echo.Context) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if len(dbuser) == 0 {
|
||||||
|
return c.JSON(403, "Invalid jwt token, couldn't find user.")
|
||||||
|
}
|
||||||
|
|
||||||
user := MapDbUser(&dbuser[0].User)
|
user := MapDbUser(&dbuser[0].User)
|
||||||
for _, oidc := range dbuser {
|
for _, oidc := range dbuser {
|
||||||
|
|||||||
@ -91,7 +91,8 @@ services:
|
|||||||
- images:/app/images
|
- images:/app/images
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.http.routers.api.rule=PathPrefix(`/api/`) || PathPrefix(`/swagger`)"
|
- "traefik.http.routers.swagger.rule=PathPrefix(`/swagger`)"
|
||||||
|
- "traefik.http.routers.api.rule=PathPrefix(`/api/`)"
|
||||||
- "traefik.http.routers.api.middlewares=phantom-token"
|
- "traefik.http.routers.api.middlewares=phantom-token"
|
||||||
- "traefik.http.middlewares.phantom-token.forwardauth.address=http://auth:4568/auth/jwt"
|
- "traefik.http.middlewares.phantom-token.forwardauth.address=http://auth:4568/auth/jwt"
|
||||||
- "traefik.http.middlewares.phantom-token.forwardauth.authRequestHeaders=Authorization,Cookie,X-Api-Key"
|
- "traefik.http.middlewares.phantom-token.forwardauth.authRequestHeaders=Authorization,Cookie,X-Api-Key"
|
||||||
|
|||||||
@ -61,7 +61,8 @@ services:
|
|||||||
- images:/app/images
|
- images:/app/images
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.http.routers.api.rule=PathPrefix(`/api/`) || PathPrefix(`/swagger`)"
|
- "traefik.http.routers.swagger.rule=PathPrefix(`/swagger`)"
|
||||||
|
- "traefik.http.routers.api.rule=PathPrefix(`/api/`)"
|
||||||
- "traefik.http.routers.api.middlewares=phantom-token"
|
- "traefik.http.routers.api.middlewares=phantom-token"
|
||||||
- "traefik.http.middlewares.phantom-token.forwardauth.address=http://auth:4568/auth/jwt"
|
- "traefik.http.middlewares.phantom-token.forwardauth.address=http://auth:4568/auth/jwt"
|
||||||
- "traefik.http.middlewares.phantom-token.forwardauth.authRequestHeaders=Authorization,Cookie,X-Api-Key"
|
- "traefik.http.middlewares.phantom-token.forwardauth.authRequestHeaders=Authorization,Cookie,X-Api-Key"
|
||||||
|
|||||||
@ -49,6 +49,12 @@ async def get_db():
|
|||||||
yield cast(Connection, db)
|
yield cast(Connection, db)
|
||||||
|
|
||||||
|
|
||||||
|
# because https://github.com/fastapi/fastapi/pull/10353
|
||||||
|
async def get_db_fapi():
|
||||||
|
async with get_db() as db:
|
||||||
|
yield db
|
||||||
|
|
||||||
|
|
||||||
async def migrate(migrations_dir="./migrations"):
|
async def migrate(migrations_dir="./migrations"):
|
||||||
async with get_db() as db:
|
async with get_db() as db:
|
||||||
_ = await db.execute(
|
_ = await db.execute(
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, BackgroundTasks, Security
|
from asyncpg import Connection
|
||||||
|
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Security
|
||||||
|
|
||||||
|
from scanner.database import get_db_fapi
|
||||||
|
|
||||||
from ..fsscan import create_scanner
|
from ..fsscan import create_scanner
|
||||||
from ..jwt import validate_bearer
|
from ..jwt import validate_bearer
|
||||||
@ -26,3 +29,19 @@ async def trigger_scan(
|
|||||||
await scanner.scan()
|
await scanner.scan()
|
||||||
|
|
||||||
tasks.add_task(run)
|
tasks.add_task(run)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/health")
|
||||||
|
def get_health():
|
||||||
|
return {"status": "healthy"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/ready")
|
||||||
|
async def get_ready(db: Annotated[Connection, Depends(get_db_fapi)]):
|
||||||
|
try:
|
||||||
|
_ = await db.execute("select 1")
|
||||||
|
return {"status": "healthy", "database": "healthy"}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500, detail={"status": "unhealthy", "database": str(e)}
|
||||||
|
)
|
||||||
|
|||||||
@ -145,6 +145,7 @@ func main() {
|
|||||||
g.Use(RequireCorePlayPermission)
|
g.Use(RequireCorePlayPermission)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
api.RegisterHealthHandlers(e.Group("/video"), metadata.Database)
|
||||||
api.RegisterStreamHandlers(g, transcoder)
|
api.RegisterStreamHandlers(g, transcoder)
|
||||||
api.RegisterMetadataHandlers(g, metadata)
|
api.RegisterMetadataHandlers(g, metadata)
|
||||||
api.RegisterPProfHandlers(e)
|
api.RegisterPProfHandlers(e)
|
||||||
|
|||||||
40
transcoder/src/api/health.go
Normal file
40
transcoder/src/api/health.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
type health struct {
|
||||||
|
db *pgxpool.Pool
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterHealthHandlers(e *echo.Group, db *pgxpool.Pool) {
|
||||||
|
h := health{db}
|
||||||
|
e.GET("/health", h.CheckHealth)
|
||||||
|
e.GET("/ready", h.CheckReady)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *health) CheckHealth(c echo.Context) error {
|
||||||
|
return c.JSON(200, struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
}{Status: "healthy"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *health) CheckReady(c echo.Context) error {
|
||||||
|
_, err := h.db.Exec(c.Request().Context(), "select 1")
|
||||||
|
|
||||||
|
status := "healthy"
|
||||||
|
db := "healthy"
|
||||||
|
ret := 200
|
||||||
|
if err != nil {
|
||||||
|
status = "unhealthy"
|
||||||
|
ret = 500
|
||||||
|
db = err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(ret, struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Database string `json:"database"`
|
||||||
|
}{Status: status, Database: db})
|
||||||
|
}
|
||||||
@ -27,7 +27,7 @@ func (s *MetadataService) ExtractSubs(ctx context.Context, info *MediaInfo) (any
|
|||||||
log.Printf("Couldn't extract subs: %v", err)
|
log.Printf("Couldn't extract subs: %v", err)
|
||||||
return set(nil, err)
|
return set(nil, err)
|
||||||
}
|
}
|
||||||
_, err = s.database.Exec(ctx, `update gocoder.info set ver_extract = $2 where sha = $1`, info.Sha, ExtractVersion)
|
_, err = s.Database.Exec(ctx, `update gocoder.info set ver_extract = $2 where sha = $1`, info.Sha, ExtractVersion)
|
||||||
return set(nil, err)
|
return set(nil, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -157,7 +157,7 @@ func (s *MetadataService) GetKeyframes(info *MediaInfo, isVideo bool, idx uint32
|
|||||||
}
|
}
|
||||||
|
|
||||||
kf.info.ready.Wait()
|
kf.info.ready.Wait()
|
||||||
tx, _ := s.database.Begin(ctx)
|
tx, _ := s.Database.Begin(ctx)
|
||||||
tx.Exec(
|
tx.Exec(
|
||||||
ctx,
|
ctx,
|
||||||
fmt.Sprintf(`update %s set keyframes = $3 where sha = $1 and idx = $2`, table),
|
fmt.Sprintf(`update %s set keyframes = $3 where sha = $1 and idx = $2`, table),
|
||||||
|
|||||||
@ -21,7 +21,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type MetadataService struct {
|
type MetadataService struct {
|
||||||
database *pgxpool.Pool
|
Database *pgxpool.Pool
|
||||||
lock RunLock[string, *MediaInfo]
|
lock RunLock[string, *MediaInfo]
|
||||||
thumbLock RunLock[string, any]
|
thumbLock RunLock[string, any]
|
||||||
extractLock RunLock[string, any]
|
extractLock RunLock[string, any]
|
||||||
@ -43,7 +43,7 @@ func NewMetadataService() (*MetadataService, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to setup database: %w", err)
|
return nil, fmt.Errorf("failed to setup database: %w", err)
|
||||||
}
|
}
|
||||||
s.database = db
|
s.Database = db
|
||||||
|
|
||||||
storage, err := s.setupStorage(ctx)
|
storage, err := s.setupStorage(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -55,8 +55,8 @@ func NewMetadataService() (*MetadataService, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *MetadataService) Close() error {
|
func (s *MetadataService) Close() error {
|
||||||
if s.database != nil {
|
if s.Database != nil {
|
||||||
s.database.Close()
|
s.Database.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.storage != nil {
|
if s.storage != nil {
|
||||||
@ -174,7 +174,7 @@ func (s *MetadataService) GetMetadata(ctx context.Context, path string, sha stri
|
|||||||
for _, audio := range ret.Audios {
|
for _, audio := range ret.Audios {
|
||||||
audio.Keyframes = nil
|
audio.Keyframes = nil
|
||||||
}
|
}
|
||||||
tx, err := s.database.Begin(ctx)
|
tx, err := s.Database.Begin(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -191,7 +191,7 @@ func (s *MetadataService) GetMetadata(ctx context.Context, path string, sha stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *MetadataService) getMetadata(ctx context.Context, path string, sha string) (*MediaInfo, error) {
|
func (s *MetadataService) getMetadata(ctx context.Context, path string, sha string) (*MediaInfo, error) {
|
||||||
rows, _ := s.database.Query(
|
rows, _ := s.Database.Query(
|
||||||
ctx,
|
ctx,
|
||||||
`select
|
`select
|
||||||
i.sha, i.path, i.extension, i.mime_codec, i.size, i.duration, i.container, i.fonts,
|
i.sha, i.path, i.extension, i.mime_codec, i.size, i.duration, i.container, i.fonts,
|
||||||
@ -214,7 +214,7 @@ func (s *MetadataService) getMetadata(ctx context.Context, path string, sha stri
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
rows, _ = s.database.Query(
|
rows, _ = s.Database.Query(
|
||||||
ctx,
|
ctx,
|
||||||
`select * from gocoder.videos as v where v.sha=$1`,
|
`select * from gocoder.videos as v where v.sha=$1`,
|
||||||
sha,
|
sha,
|
||||||
@ -224,7 +224,7 @@ func (s *MetadataService) getMetadata(ctx context.Context, path string, sha stri
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
rows, _ = s.database.Query(
|
rows, _ = s.Database.Query(
|
||||||
ctx,
|
ctx,
|
||||||
`select * from gocoder.audios as a where a.sha=$1`,
|
`select * from gocoder.audios as a where a.sha=$1`,
|
||||||
sha,
|
sha,
|
||||||
@ -234,7 +234,7 @@ func (s *MetadataService) getMetadata(ctx context.Context, path string, sha stri
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
rows, _ = s.database.Query(
|
rows, _ = s.Database.Query(
|
||||||
ctx,
|
ctx,
|
||||||
`select * from gocoder.subtitles as s where s.sha=$1`,
|
`select * from gocoder.subtitles as s where s.sha=$1`,
|
||||||
sha,
|
sha,
|
||||||
@ -259,7 +259,7 @@ func (s *MetadataService) getMetadata(ctx context.Context, path string, sha stri
|
|||||||
fmt.Printf("Couldn't find external subtitles: %v", err)
|
fmt.Printf("Couldn't find external subtitles: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
rows, _ = s.database.Query(
|
rows, _ = s.Database.Query(
|
||||||
ctx,
|
ctx,
|
||||||
`select * from gocoder.chapters as c where c.sha=$1`,
|
`select * from gocoder.chapters as c where c.sha=$1`,
|
||||||
sha,
|
sha,
|
||||||
@ -282,7 +282,7 @@ func (s *MetadataService) storeFreshMetadata(ctx context.Context, path string, s
|
|||||||
return set(nil, err)
|
return set(nil, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tx, err := s.database.Begin(ctx)
|
tx, err := s.Database.Begin(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return set(ret, err)
|
return set(ret, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -77,7 +77,7 @@ func (s *MetadataService) ExtractThumbs(ctx context.Context, path string, sha st
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return set(nil, err)
|
return set(nil, err)
|
||||||
}
|
}
|
||||||
_, err = s.database.Exec(ctx, `update gocoder.info set ver_thumbs = $2 where sha = $1`, sha, ThumbsVersion)
|
_, err = s.Database.Exec(ctx, `update gocoder.info set ver_thumbs = $2 where sha = $1`, sha, ThumbsVersion)
|
||||||
return set(nil, err)
|
return set(nil, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user