Compare commits
2 الالتزامات
576f02390b
...
4946d19ea7
| المؤلف | SHA1 | التاريخ | |
|---|---|---|---|
| 4946d19ea7 | |||
| 9896be5d9a |
@@ -1,6 +1,6 @@
|
||||
FROM node:16.3.0-alpine
|
||||
|
||||
COPY q1-deploy-monitor /app
|
||||
COPY app.js dashboard.js index.html package.json /app/
|
||||
|
||||
RUN mkdir -p /logs && touch /logs/logs.txt
|
||||
|
||||
@@ -9,3 +9,4 @@ WORKDIR /app
|
||||
RUN npm install
|
||||
|
||||
CMD ["node", "/app/app.js"]
|
||||
|
||||
|
||||
82
app.js
Normal file
82
app.js
Normal file
@@ -0,0 +1,82 @@
|
||||
const express = require('express')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const app = express()
|
||||
const port = 5000
|
||||
const logDir = '/logs'
|
||||
const logFile = path.join(logDir, 'logs.txt')
|
||||
|
||||
if (!fs.existsSync(logDir)) {
|
||||
fs.mkdirSync(logDir, { recursive: true })
|
||||
}
|
||||
|
||||
function getStatsFromLogFile() {
|
||||
if (!fs.existsSync(logFile)) {
|
||||
return { requestCount: 0, averageResponseTime: 0 }
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(logFile, 'utf8')
|
||||
const lines = content.split('\n').filter(Boolean)
|
||||
const requestCount = lines.length
|
||||
|
||||
if (requestCount === 0) {
|
||||
return { requestCount: 0, averageResponseTime: 0 }
|
||||
}
|
||||
|
||||
const totalResponseTime = lines.reduce((total, line) => {
|
||||
const parts = line.trim().split(' ')
|
||||
const duration = parseFloat(parts[1])
|
||||
return total + (Number.isNaN(duration) ? 0 : duration)
|
||||
}, 0)
|
||||
|
||||
return {
|
||||
requestCount,
|
||||
averageResponseTime: totalResponseTime / requestCount
|
||||
}
|
||||
}
|
||||
|
||||
app.use('/dashboard', express.static(path.join(__dirname, 'dashboard'), { index: false }))
|
||||
|
||||
app.use((req, res, next) => {
|
||||
const startTime = Date.now()
|
||||
const requestTime = new Date(startTime).toISOString()
|
||||
|
||||
res.on('finish', () => {
|
||||
const durationSeconds = (Date.now() - startTime) / 1000
|
||||
const line = `${req.path} ${durationSeconds.toFixed(3)} ${requestTime} ${res.statusCode}\n`
|
||||
|
||||
fs.appendFile(logFile, line, (err) => {
|
||||
if (err) console.error('Failed to write request log:', err)
|
||||
})
|
||||
})
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
app.get('/dashboard', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'dashboard', 'index.html'))
|
||||
})
|
||||
|
||||
app.get('/api/stats', (req, res) => {
|
||||
const { requestCount, averageResponseTime } = getStatsFromLogFile()
|
||||
|
||||
res.json({
|
||||
requestCount,
|
||||
averageResponseTime,
|
||||
status: 'up'
|
||||
})
|
||||
})
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
res.send('Hello World from GHAYMAH')
|
||||
})
|
||||
|
||||
app.get('/health', (req, res) => {
|
||||
res.send('The app up and runing')
|
||||
})
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Example app listening at http://localhost:${port}`)
|
||||
console.log(`Dashboard available at http://localhost:${port}/dashboard`)
|
||||
})
|
||||
49
dashboard.js
Normal file
49
dashboard.js
Normal file
@@ -0,0 +1,49 @@
|
||||
const requestCountEl = document.getElementById('requestCount')
|
||||
const averageTimeEl = document.getElementById('averageTime')
|
||||
const appStatusEl = document.getElementById('appStatus')
|
||||
const statusBadgeEl = document.getElementById('statusBadge')
|
||||
const statusTextEl = document.getElementById('statusText')
|
||||
const lastUpdatedEl = document.getElementById('lastUpdated')
|
||||
|
||||
function setStatus(isUp) {
|
||||
statusBadgeEl.classList.remove('up', 'down')
|
||||
appStatusEl.classList.remove('up', 'down')
|
||||
|
||||
if (isUp) {
|
||||
statusBadgeEl.classList.add('up')
|
||||
appStatusEl.classList.add('up')
|
||||
statusTextEl.textContent = 'Online'
|
||||
appStatusEl.textContent = 'Up'
|
||||
return
|
||||
}
|
||||
|
||||
statusBadgeEl.classList.add('down')
|
||||
appStatusEl.classList.add('down')
|
||||
statusTextEl.textContent = 'Offline'
|
||||
appStatusEl.textContent = 'Down'
|
||||
}
|
||||
|
||||
async function fetchStats() {
|
||||
try {
|
||||
const response = await fetch('/api/stats')
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Stats request failed')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
requestCountEl.textContent = data.requestCount.toLocaleString()
|
||||
averageTimeEl.textContent = `${data.averageResponseTime.toFixed(3)}s`
|
||||
setStatus(data.status === 'up')
|
||||
lastUpdatedEl.textContent = `Last updated: ${new Date().toLocaleTimeString()}`
|
||||
} catch (error) {
|
||||
requestCountEl.textContent = '—'
|
||||
averageTimeEl.textContent = '—'
|
||||
setStatus(false)
|
||||
lastUpdatedEl.textContent = 'Last updated: failed to connect'
|
||||
}
|
||||
}
|
||||
|
||||
fetchStats()
|
||||
setInterval(fetchStats, 5000)
|
||||
50
index.html
Normal file
50
index.html
Normal file
@@ -0,0 +1,50 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Server Dashboard</title>
|
||||
<link rel="stylesheet" href="/dashboard/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="dashboard">
|
||||
<header class="header">
|
||||
<div>
|
||||
<h1>Server Dashboard</h1>
|
||||
<p class="subtitle">Live monitoring for your Node.js application</p>
|
||||
</div>
|
||||
<div class="status-badge" id="statusBadge">
|
||||
<span class="status-dot"></span>
|
||||
<span id="statusText">Checking...</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="cards">
|
||||
<section class="card">
|
||||
<p class="card-label">Total Requests</p>
|
||||
<p class="card-value" id="requestCount">—</p>
|
||||
<p class="card-hint">All requests received by the server</p>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<p class="card-label">Average Response Time</p>
|
||||
<p class="card-value" id="averageTime">—</p>
|
||||
<p class="card-hint">Mean duration in seconds</p>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<p class="card-label">Application Status</p>
|
||||
<p class="card-value status-value" id="appStatus">—</p>
|
||||
<p class="card-hint">Based on health check</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<span id="lastUpdated">Last updated: —</span>
|
||||
<span>Auto-refresh every 5 seconds</span>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="/dashboard/dashboard.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
18
package.json
Normal file
18
package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "nodeapp",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"express": "^4.17.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.1.14"
|
||||
}
|
||||
}
|
||||
158
style.css
Normal file
158
style.css
Normal file
@@ -0,0 +1,158 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #0f172a 100%);
|
||||
min-height: 100vh;
|
||||
color: #e2e8f0;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.dashboard {
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: #f8fafc;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin-top: 0.35rem;
|
||||
color: #94a3b8;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
padding: 0.65rem 1.1rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(15, 23, 42, 0.7);
|
||||
border: 1px solid rgba(148, 163, 184, 0.25);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: #64748b;
|
||||
box-shadow: 0 0 0 0 rgba(100, 116, 139, 0.5);
|
||||
}
|
||||
|
||||
.status-badge.up .status-dot {
|
||||
background: #22c55e;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.status-badge.down .status-dot {
|
||||
background: #ef4444;
|
||||
}
|
||||
|
||||
.status-badge.up {
|
||||
border-color: rgba(34, 197, 94, 0.35);
|
||||
color: #bbf7d0;
|
||||
}
|
||||
|
||||
.status-badge.down {
|
||||
border-color: rgba(239, 68, 68, 0.35);
|
||||
color: #fecaca;
|
||||
}
|
||||
|
||||
.cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: rgba(30, 41, 59, 0.85);
|
||||
border: 1px solid rgba(148, 163, 184, 0.15);
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
|
||||
transition: transform 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: rgba(96, 165, 250, 0.35);
|
||||
}
|
||||
|
||||
.card-label {
|
||||
color: #94a3b8;
|
||||
font-size: 0.85rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 2.4rem;
|
||||
font-weight: 700;
|
||||
color: #f8fafc;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.status-value.up {
|
||||
color: #4ade80;
|
||||
}
|
||||
|
||||
.status-value.down {
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.card-hint {
|
||||
margin-top: 0.75rem;
|
||||
color: #64748b;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
color: #64748b;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.5);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 8px rgba(34, 197, 94, 0);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
body {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
المرجع في مشكلة جديدة
حظر مستخدم