Compare commits
5 الالتزامات
a42f19f5d5
...
main
| المؤلف | SHA1 | التاريخ | |
|---|---|---|---|
| 4946d19ea7 | |||
| 9896be5d9a | |||
| 576f02390b | |||
| bbfa5901c2 | |||
| 99edc19170 |
@@ -14,7 +14,7 @@
|
|||||||
},
|
},
|
||||||
"resourceTier": "t1",
|
"resourceTier": "t1",
|
||||||
"build": {
|
"build": {
|
||||||
"dockerfile": "q1-deploy-monitor/Dockerfile",
|
"dockerfile": "Dockerfile",
|
||||||
"context": "q1-deploy-monitor"
|
"context": "q1-deploy-monitor"
|
||||||
},
|
},
|
||||||
"container": {
|
"container": {
|
||||||
|
|||||||
12
Dockerfile
Normal file
12
Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
FROM node:16.3.0-alpine
|
||||||
|
|
||||||
|
COPY app.js dashboard.js index.html package.json /app/
|
||||||
|
|
||||||
|
RUN mkdir -p /logs && touch /logs/logs.txt
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
المرجع في مشكلة جديدة
حظر مستخدم