fix healthcheck
Browse files- client/src/components/ServiceStatus.jsx +14 -68
- client/src/contexts/ServiceStatusContext.jsx +5 -20
- client/src/pages/Tutorial.jsx +2 -2
- server/api/models.py +4 -10
- server/api/routes/health.py +10 -32
- server/services/flux_client.py +18 -25
client/src/components/ServiceStatus.jsx
CHANGED
@@ -1,4 +1,4 @@
|
|
1 |
-
import React
|
2 |
import { Box, Typography } from "@mui/material";
|
3 |
import { styled } from "@mui/system";
|
4 |
import { useServiceStatus } from "../contexts/ServiceStatusContext";
|
@@ -10,8 +10,6 @@ const StatusDot = styled("div")(({ status }) => ({
|
|
10 |
backgroundColor:
|
11 |
status === "healthy"
|
12 |
? "#4caf50"
|
13 |
-
: status === "initializing"
|
14 |
-
? "#FFA726"
|
15 |
: status === "unhealthy"
|
16 |
? "#f44336"
|
17 |
: "#9e9e9e",
|
@@ -28,82 +26,30 @@ const ServiceLabel = styled(Box)({
|
|
28 |
},
|
29 |
});
|
30 |
|
31 |
-
const ServiceStatusContainer = styled("div")({
|
32 |
-
position: "absolute",
|
33 |
-
top: 16,
|
34 |
-
left: 16,
|
35 |
-
padding: 8,
|
36 |
-
borderRadius: 4,
|
37 |
-
boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
|
38 |
-
zIndex: 1000,
|
39 |
-
});
|
40 |
-
|
41 |
export function ServiceStatus() {
|
42 |
const { services } = useServiceStatus();
|
43 |
|
44 |
-
const fetchServiceHealth = async (service) => {
|
45 |
-
try {
|
46 |
-
const response = await fetch(`/api/health/${service}`);
|
47 |
-
const data = await response.json();
|
48 |
-
|
49 |
-
if (response.ok) {
|
50 |
-
return {
|
51 |
-
status: data.status,
|
52 |
-
latency: data.latency,
|
53 |
-
error: null,
|
54 |
-
};
|
55 |
-
} else {
|
56 |
-
return {
|
57 |
-
status: data.status || "unhealthy",
|
58 |
-
latency: null,
|
59 |
-
error: data.error || "Service unavailable",
|
60 |
-
};
|
61 |
-
}
|
62 |
-
} catch (error) {
|
63 |
-
console.error(`Error checking ${service} health:`, error);
|
64 |
-
return {
|
65 |
-
status: "unhealthy",
|
66 |
-
latency: null,
|
67 |
-
error: error.message,
|
68 |
-
};
|
69 |
-
}
|
70 |
-
};
|
71 |
-
|
72 |
-
useEffect(() => {
|
73 |
-
const checkHealth = async () => {
|
74 |
-
const mistralHealth = await fetchServiceHealth("mistral");
|
75 |
-
const fluxHealth = await fetchServiceHealth("flux");
|
76 |
-
|
77 |
-
// Assuming you want to update the state with the fetched health data
|
78 |
-
// This is a placeholder and should be replaced with actual state management logic
|
79 |
-
console.log("Updating health status:", {
|
80 |
-
mistral: mistralHealth,
|
81 |
-
flux: fluxHealth,
|
82 |
-
});
|
83 |
-
};
|
84 |
-
|
85 |
-
// Initial check
|
86 |
-
checkHealth();
|
87 |
-
|
88 |
-
// Check every 30 seconds
|
89 |
-
const interval = setInterval(checkHealth, 30000);
|
90 |
-
|
91 |
-
return () => clearInterval(interval);
|
92 |
-
}, []);
|
93 |
-
|
94 |
return (
|
95 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
96 |
{Object.entries(services).map(([service, { status, latency, error }]) => (
|
97 |
<ServiceLabel key={service}>
|
98 |
<StatusDot status={status} />
|
99 |
<Typography>
|
100 |
{service.charAt(0).toUpperCase() + service.slice(1)}
|
101 |
-
{/* {status === "healthy" && latency && ` (${
|
102 |
-
{/* {
|
103 |
-
{status === "unhealthy" && error && ` (${error})`} */}
|
104 |
</Typography>
|
105 |
</ServiceLabel>
|
106 |
))}
|
107 |
-
</
|
108 |
);
|
109 |
}
|
|
|
1 |
+
import React from "react";
|
2 |
import { Box, Typography } from "@mui/material";
|
3 |
import { styled } from "@mui/system";
|
4 |
import { useServiceStatus } from "../contexts/ServiceStatusContext";
|
|
|
10 |
backgroundColor:
|
11 |
status === "healthy"
|
12 |
? "#4caf50"
|
|
|
|
|
13 |
: status === "unhealthy"
|
14 |
? "#f44336"
|
15 |
: "#9e9e9e",
|
|
|
26 |
},
|
27 |
});
|
28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
29 |
export function ServiceStatus() {
|
30 |
const { services } = useServiceStatus();
|
31 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
32 |
return (
|
33 |
+
<Box
|
34 |
+
sx={{
|
35 |
+
position: "absolute",
|
36 |
+
top: 16,
|
37 |
+
left: 16,
|
38 |
+
display: "flex",
|
39 |
+
alignItems: "center",
|
40 |
+
zIndex: 1000,
|
41 |
+
}}
|
42 |
+
>
|
43 |
{Object.entries(services).map(([service, { status, latency, error }]) => (
|
44 |
<ServiceLabel key={service}>
|
45 |
<StatusDot status={status} />
|
46 |
<Typography>
|
47 |
{service.charAt(0).toUpperCase() + service.slice(1)}
|
48 |
+
{/* {status === "healthy" && latency && ` (${latency}ms)`} */}
|
49 |
+
{/* {error && ` - ${error}`} */}
|
|
|
50 |
</Typography>
|
51 |
</ServiceLabel>
|
52 |
))}
|
53 |
+
</Box>
|
54 |
);
|
55 |
}
|
client/src/contexts/ServiceStatusContext.jsx
CHANGED
@@ -63,26 +63,11 @@ export function ServiceStatusProvider({ children }) {
|
|
63 |
};
|
64 |
|
65 |
useEffect(() => {
|
66 |
-
console.log("ServiceStatusProvider mounted,
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
};
|
72 |
-
|
73 |
-
// Premier check immédiat
|
74 |
-
checkHealth();
|
75 |
-
|
76 |
-
// Mettre en place l'intervalle
|
77 |
-
console.log("Setting up health check interval...");
|
78 |
-
const interval = setInterval(checkHealth, 30000);
|
79 |
-
|
80 |
-
// Cleanup
|
81 |
-
return () => {
|
82 |
-
console.log("Cleaning up health check interval...");
|
83 |
-
clearInterval(interval);
|
84 |
-
};
|
85 |
-
}, []);
|
86 |
|
87 |
return (
|
88 |
<ServiceStatusContext.Provider value={{ services, areServicesHealthy }}>
|
|
|
63 |
};
|
64 |
|
65 |
useEffect(() => {
|
66 |
+
console.log("ServiceStatusProvider mounted, checking services health...");
|
67 |
+
// Un seul check au montage du composant
|
68 |
+
fetchServiceHealth("mistral");
|
69 |
+
fetchServiceHealth("flux");
|
70 |
+
}, []); // Pas d'intervalle, juste une vérification au montage
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
71 |
|
72 |
return (
|
73 |
<ServiceStatusContext.Provider value={{ services, areServicesHealthy }}>
|
client/src/pages/Tutorial.jsx
CHANGED
@@ -75,7 +75,7 @@ export function Tutorial() {
|
|
75 |
zIndex: 10,
|
76 |
textAlign: "center",
|
77 |
mt: 2,
|
78 |
-
maxWidth: isMobile ? "85%" : "
|
79 |
opacity: 0.8,
|
80 |
color: "white",
|
81 |
px: isMobile ? 3 : 0,
|
@@ -105,7 +105,7 @@ export function Tutorial() {
|
|
105 |
sx={{
|
106 |
position: "relative",
|
107 |
flex: { xs: "none", sm: 1 },
|
108 |
-
width: { xs: "
|
109 |
maxWidth: { xs: "160px", sm: "200px" },
|
110 |
"&::before": {
|
111 |
content: '""',
|
|
|
75 |
zIndex: 10,
|
76 |
textAlign: "center",
|
77 |
mt: 2,
|
78 |
+
maxWidth: isMobile ? "85%" : "75%",
|
79 |
opacity: 0.8,
|
80 |
color: "white",
|
81 |
px: isMobile ? 3 : 0,
|
|
|
105 |
sx={{
|
106 |
position: "relative",
|
107 |
flex: { xs: "none", sm: 1 },
|
108 |
+
width: { xs: "75%", sm: "auto" },
|
109 |
maxWidth: { xs: "160px", sm: "200px" },
|
110 |
"&::before": {
|
111 |
content: '""',
|
server/api/models.py
CHANGED
@@ -2,7 +2,6 @@ from pydantic import BaseModel, Field, validator
|
|
2 |
from typing import List, Optional, Dict, Any, Union
|
3 |
from core.constants import GameConfig
|
4 |
import re
|
5 |
-
from enum import Enum
|
6 |
|
7 |
class Choice(BaseModel):
|
8 |
id: int
|
@@ -85,13 +84,8 @@ class StoryResponse(BaseModel):
|
|
85 |
raise ValueError('Must have exactly 2 choices for story progression')
|
86 |
return v
|
87 |
|
88 |
-
class ServiceStatus(str, Enum):
|
89 |
-
HEALTHY = "healthy"
|
90 |
-
UNHEALTHY = "unhealthy"
|
91 |
-
INITIALIZING = "initializing"
|
92 |
-
|
93 |
class HealthCheckResponse(BaseModel):
|
94 |
-
status:
|
95 |
-
service: str
|
96 |
-
latency: Optional[float] = None
|
97 |
-
error: Optional[str] = None
|
|
|
2 |
from typing import List, Optional, Dict, Any, Union
|
3 |
from core.constants import GameConfig
|
4 |
import re
|
|
|
5 |
|
6 |
class Choice(BaseModel):
|
7 |
id: int
|
|
|
84 |
raise ValueError('Must have exactly 2 choices for story progression')
|
85 |
return v
|
86 |
|
|
|
|
|
|
|
|
|
|
|
87 |
class HealthCheckResponse(BaseModel):
|
88 |
+
status: str = Field(..., description="The health status of the service (healthy/unhealthy)")
|
89 |
+
service: str = Field(..., description="The name of the service being checked")
|
90 |
+
latency: Optional[float] = Field(None, description="The latency of the service in milliseconds")
|
91 |
+
error: Optional[str] = Field(None, description="Error message if the service is unhealthy")
|
server/api/routes/health.py
CHANGED
@@ -58,43 +58,21 @@ def get_health_router(mistral_client: MistralClient, flux_client: FluxClient) ->
|
|
58 |
start_time = time.time()
|
59 |
try:
|
60 |
# Try to generate a test image with a timeout
|
61 |
-
is_healthy
|
62 |
flux_client.check_health(),
|
63 |
timeout=5.0 # Même timeout que Mistral
|
64 |
)
|
65 |
|
66 |
-
|
67 |
-
|
68 |
-
if is_healthy:
|
69 |
-
print(f"Flux health check successful. Latency: {latency}ms")
|
70 |
-
return HealthCheckResponse(
|
71 |
-
status="healthy",
|
72 |
-
service="flux",
|
73 |
-
latency=latency
|
74 |
-
)
|
75 |
-
elif status == "initializing":
|
76 |
-
print("Flux service is initializing")
|
77 |
-
raise HTTPException(
|
78 |
-
status_code=503,
|
79 |
-
detail=HealthCheckResponse(
|
80 |
-
status="initializing",
|
81 |
-
service="flux",
|
82 |
-
latency=None,
|
83 |
-
error="Service is initializing"
|
84 |
-
).dict()
|
85 |
-
)
|
86 |
-
else:
|
87 |
-
print(f"Flux health check failed: {status}")
|
88 |
-
raise HTTPException(
|
89 |
-
status_code=503,
|
90 |
-
detail=HealthCheckResponse(
|
91 |
-
status="unhealthy",
|
92 |
-
service="flux",
|
93 |
-
latency=None,
|
94 |
-
error=status
|
95 |
-
).dict()
|
96 |
-
)
|
97 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
98 |
except asyncio.TimeoutError:
|
99 |
print("Flux health check failed: timeout")
|
100 |
raise HTTPException(
|
|
|
58 |
start_time = time.time()
|
59 |
try:
|
60 |
# Try to generate a test image with a timeout
|
61 |
+
is_healthy = await asyncio.wait_for(
|
62 |
flux_client.check_health(),
|
63 |
timeout=5.0 # Même timeout que Mistral
|
64 |
)
|
65 |
|
66 |
+
if not is_healthy:
|
67 |
+
raise Exception("Failed to generate test image")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
68 |
|
69 |
+
latency = (time.time() - start_time) * 1000 # Convert to milliseconds
|
70 |
+
print(f"Flux health check successful. Latency: {latency}ms")
|
71 |
+
return HealthCheckResponse(
|
72 |
+
status="healthy",
|
73 |
+
service="flux",
|
74 |
+
latency=latency
|
75 |
+
)
|
76 |
except asyncio.TimeoutError:
|
77 |
print("Flux health check failed: timeout")
|
78 |
raise HTTPException(
|
server/services/flux_client.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1 |
import os
|
2 |
import aiohttp
|
3 |
-
from typing import Optional
|
4 |
|
5 |
class FluxClient:
|
6 |
def __init__(self, api_key: str):
|
@@ -18,7 +18,7 @@ class FluxClient:
|
|
18 |
width: int,
|
19 |
height: int,
|
20 |
num_inference_steps: int = 5,
|
21 |
-
guidance_scale: float = 9.0) ->
|
22 |
"""Génère une image à partir d'un prompt."""
|
23 |
try:
|
24 |
# Ensure dimensions are multiples of 8
|
@@ -29,6 +29,7 @@ class FluxClient:
|
|
29 |
print(f"Headers: Authorization: Bearer {self.api_key[:4]}...")
|
30 |
print(f"Request body: {prompt[:100]}...")
|
31 |
|
|
|
32 |
session = await self._get_session()
|
33 |
async with session.post(
|
34 |
self.endpoint,
|
@@ -49,59 +50,51 @@ class FluxClient:
|
|
49 |
) as response:
|
50 |
print(f"Response status code: {response.status}")
|
51 |
print(f"Response headers: {response.headers}")
|
52 |
-
|
53 |
-
# Vérifier si le modèle est en cours d'initialisation
|
54 |
-
if response.status == 503:
|
55 |
-
error_content = await response.text()
|
56 |
-
if "currently loading" in error_content.lower() or "initializing" in error_content.lower():
|
57 |
-
return None, "initializing"
|
58 |
-
return None, "unavailable"
|
59 |
|
60 |
if response.status == 200:
|
61 |
content = await response.read()
|
62 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
63 |
else:
|
64 |
error_content = await response.text()
|
65 |
print(f"Error from Flux API: {response.status}")
|
66 |
print(f"Response content: {error_content}")
|
67 |
-
return None
|
68 |
|
69 |
except Exception as e:
|
70 |
print(f"Error in FluxClient.generate_image: {str(e)}")
|
71 |
import traceback
|
72 |
print(f"Traceback: {traceback.format_exc()}")
|
73 |
-
return None
|
74 |
|
75 |
async def close(self):
|
76 |
if self._session:
|
77 |
await self._session.close()
|
78 |
self._session = None
|
79 |
|
80 |
-
async def check_health(self) ->
|
81 |
"""
|
82 |
Vérifie la disponibilité du service Flux en tentant de générer une petite image.
|
83 |
|
84 |
Returns:
|
85 |
-
|
86 |
-
- is_healthy: True si le service est disponible
|
87 |
-
- status: "healthy", "initializing", ou message d'erreur
|
88 |
"""
|
89 |
try:
|
90 |
# Test simple prompt pour générer une petite image
|
91 |
-
test_image
|
92 |
prompt="test image, simple circle",
|
93 |
width=64, # Petite image pour le test
|
94 |
height=64,
|
95 |
num_inference_steps=1 # Minimum d'étapes pour être rapide
|
96 |
)
|
97 |
|
98 |
-
|
99 |
-
return True, "healthy"
|
100 |
-
elif status == "initializing":
|
101 |
-
return False, "initializing"
|
102 |
-
else:
|
103 |
-
return False, status or "unavailable"
|
104 |
-
|
105 |
except Exception as e:
|
106 |
print(f"Health check failed: {str(e)}")
|
107 |
-
|
|
|
1 |
import os
|
2 |
import aiohttp
|
3 |
+
from typing import Optional
|
4 |
|
5 |
class FluxClient:
|
6 |
def __init__(self, api_key: str):
|
|
|
18 |
width: int,
|
19 |
height: int,
|
20 |
num_inference_steps: int = 5,
|
21 |
+
guidance_scale: float = 9.0) -> Optional[bytes]:
|
22 |
"""Génère une image à partir d'un prompt."""
|
23 |
try:
|
24 |
# Ensure dimensions are multiples of 8
|
|
|
29 |
print(f"Headers: Authorization: Bearer {self.api_key[:4]}...")
|
30 |
print(f"Request body: {prompt[:100]}...")
|
31 |
|
32 |
+
|
33 |
session = await self._get_session()
|
34 |
async with session.post(
|
35 |
self.endpoint,
|
|
|
50 |
) as response:
|
51 |
print(f"Response status code: {response.status}")
|
52 |
print(f"Response headers: {response.headers}")
|
53 |
+
print(f"Response content type: {response.headers.get('content-type', 'unknown')}")
|
|
|
|
|
|
|
|
|
|
|
|
|
54 |
|
55 |
if response.status == 200:
|
56 |
content = await response.read()
|
57 |
+
content_length = len(content)
|
58 |
+
print(f"Received successful response with content length: {content_length}")
|
59 |
+
if isinstance(content, bytes):
|
60 |
+
print("Response content is bytes (correct)")
|
61 |
+
else:
|
62 |
+
print(f"Warning: Response content is {type(content)}")
|
63 |
+
return content
|
64 |
else:
|
65 |
error_content = await response.text()
|
66 |
print(f"Error from Flux API: {response.status}")
|
67 |
print(f"Response content: {error_content}")
|
68 |
+
return None
|
69 |
|
70 |
except Exception as e:
|
71 |
print(f"Error in FluxClient.generate_image: {str(e)}")
|
72 |
import traceback
|
73 |
print(f"Traceback: {traceback.format_exc()}")
|
74 |
+
return None
|
75 |
|
76 |
async def close(self):
|
77 |
if self._session:
|
78 |
await self._session.close()
|
79 |
self._session = None
|
80 |
|
81 |
+
async def check_health(self) -> bool:
|
82 |
"""
|
83 |
Vérifie la disponibilité du service Flux en tentant de générer une petite image.
|
84 |
|
85 |
Returns:
|
86 |
+
bool: True si le service est disponible, False sinon
|
|
|
|
|
87 |
"""
|
88 |
try:
|
89 |
# Test simple prompt pour générer une petite image
|
90 |
+
test_image = await self.generate_image(
|
91 |
prompt="test image, simple circle",
|
92 |
width=64, # Petite image pour le test
|
93 |
height=64,
|
94 |
num_inference_steps=1 # Minimum d'étapes pour être rapide
|
95 |
)
|
96 |
|
97 |
+
return test_image is not None
|
|
|
|
|
|
|
|
|
|
|
|
|
98 |
except Exception as e:
|
99 |
print(f"Health check failed: {str(e)}")
|
100 |
+
raise
|