Skip to content

Commit 6f6bef4

Browse files
committed
add: lab12 solution
1 parent 4f324be commit 6f6bef4

16 files changed

Lines changed: 747 additions & 14 deletions

app_python/Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ RUN pip install --no-cache-dir -r requirements.txt
2020
# Copy the rest of the application code
2121
COPY . .
2222

23-
# Change ownership of the application files to the non-root user
24-
RUN chown -R appuser:appuser /app
23+
# Prepare writable data directory for persistent visits storage
24+
RUN mkdir -p /data && chown -R appuser:appuser /app /data
2525

2626
# Switch to non-root user
2727
USER appuser

app_python/README.md

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ DEBUG=true python app.py
6767
Once running, access the service at:
6868
- **Main endpoint**: http://localhost:5000/
6969
- **Health check**: http://localhost:5000/health
70+
- **Visits counter**: http://localhost:5000/visits
7071
- **Prometheus metrics**: http://localhost:5000/metrics
7172
- **Interactive API docs**: http://localhost:5000/docs
7273

@@ -83,9 +84,21 @@ docker build -t devops-info-service .
8384
Run the container mapping port 5000:
8485

8586
```bash
86-
docker run -p 5000:5000 devops-info-service
87+
docker run -p 5000:5000 -v "${PWD}/data:/data" devops-info-service
8788
```
8889

90+
### Run with Docker Compose (Recommended for persistence)
91+
92+
```bash
93+
docker compose up --build -d
94+
curl http://localhost:5000/
95+
curl http://localhost:5000/visits
96+
docker compose restart
97+
curl http://localhost:5000/visits
98+
```
99+
100+
The visits counter is stored in `./data/visits` on your host and survives container restarts.
101+
89102
### Push to Docker Hub
90103

91104
```bash
@@ -126,7 +139,8 @@ Returns comprehensive service and system information.
126139
"uptime_seconds": 500,
127140
"uptime_human": "12 hours, 8 minutes",
128141
"current_time": "2026-01-28T19:18:42.601851+00:00",
129-
"timezone": "UTC"
142+
"timezone": "UTC",
143+
"visits": 42
130144
},
131145
"request": {
132146
"client_ip": "127.0.0.1",
@@ -144,6 +158,16 @@ Returns comprehensive service and system information.
144158
"path": "/health",
145159
"method": "GET",
146160
"description": "Health check"
161+
},
162+
{
163+
"path": "/metrics",
164+
"method": "GET",
165+
"description": "Prometheus metrics"
166+
},
167+
{
168+
"path": "/visits",
169+
"method": "GET",
170+
"description": "Current visits counter"
147171
}
148172
]
149173
}
@@ -174,6 +198,20 @@ Exposes Prometheus-compatible metrics for monitoring.
174198

175199
**Status Code:** 200 OK
176200

201+
### GET `/visits`
202+
203+
Returns the current persisted visits counter.
204+
205+
**Response:**
206+
```json
207+
{
208+
"visits": 42,
209+
"visits_file": "/data/visits"
210+
}
211+
```
212+
213+
**Status Code:** 200 OK
214+
177215
## Configuration
178216

179217
The application supports the following environment variables:
@@ -183,6 +221,9 @@ The application supports the following environment variables:
183221
| `HOST` | `0.0.0.0` | Host address to bind the server |
184222
| `PORT` | `5000` | Port number to listen on |
185223
| `DEBUG` | `False` | Enable debug mode with auto-reload |
224+
| `LOG_LEVEL` | `INFO` | JSON log level |
225+
| `DATA_DIR` | `/data` | Directory for persistent data files |
226+
| `VISITS_FILE` | `/data/visits` | Full path to visits counter file |
186227

187228
## Technology Stack
188229

@@ -195,6 +236,8 @@ The application supports the following environment variables:
195236
```
196237
app_python/
197238
├── app.py # Main application
239+
├── docker-compose.yml # Local container orchestration
240+
├── data/ # Local persistent data directory
198241
├── requirements.txt # All dependencies
199242
├── .gitignore # Git ignore rules
200243
├── README.md # This file
@@ -249,6 +292,8 @@ pytest --cov=. --cov-report=html
249292
Tests are organized by endpoint functionality:
250293
- `TestRootEndpoint`: Tests for the main `/` endpoint
251294
- `TestHealthEndpoint`: Tests for the `/health` endpoint
295+
- `TestMetricsEndpoint`: Tests for the `/metrics` endpoint
296+
- `TestVisitsEndpoint`: Tests for the `/visits` endpoint and persistence behavior
252297
- `TestErrorHandling`: Tests for error scenarios
253298
- `TestResponseConsistency`: Tests for response consistency
254299

app_python/app.py

Lines changed: 105 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import socket
88
import platform
99
import logging
10+
from threading import Lock
1011
from time import perf_counter
1112
from datetime import datetime, timezone
1213
from typing import Dict, Any
@@ -39,6 +40,11 @@ def add_fields(self, log_record, record, message_dict):
3940

4041
# Application startup time
4142
start_time = datetime.now(timezone.utc)
43+
visits_lock = Lock()
44+
45+
# Persistent visits counter configuration
46+
DEFAULT_DATA_DIR = os.getenv('DATA_DIR', '/data')
47+
DEFAULT_VISITS_FILE = os.getenv('VISITS_FILE', os.path.join(DEFAULT_DATA_DIR, 'visits'))
4248

4349
# Prometheus metrics (RED method + app-specific metrics)
4450
http_requests_total = Counter(
@@ -161,6 +167,81 @@ def get_system_info() -> Dict[str, Any]:
161167
}
162168

163169

170+
def get_visits_file_path() -> str:
171+
"""Get visits file path from environment with a safe default."""
172+
return os.getenv('VISITS_FILE', DEFAULT_VISITS_FILE)
173+
174+
175+
def _atomic_write_text(file_path: str, content: str) -> None:
176+
"""Write file content atomically to avoid partial updates."""
177+
temp_file_path = f"{file_path}.tmp"
178+
with open(temp_file_path, 'w', encoding='utf-8') as temp_file:
179+
temp_file.write(content)
180+
os.replace(temp_file_path, file_path)
181+
182+
183+
def _ensure_visits_storage(visits_file_path: str) -> None:
184+
"""Ensure visits counter file exists and contains a valid integer."""
185+
visits_dir = os.path.dirname(visits_file_path)
186+
if visits_dir:
187+
os.makedirs(visits_dir, exist_ok=True)
188+
189+
if not os.path.exists(visits_file_path):
190+
_atomic_write_text(visits_file_path, '0\n')
191+
logger.info('Visits counter initialized', extra={
192+
'visits_file': visits_file_path,
193+
'visits_count': 0,
194+
})
195+
return
196+
197+
try:
198+
with open(visits_file_path, 'r', encoding='utf-8') as visits_file:
199+
int((visits_file.read().strip() or '0'))
200+
except (OSError, ValueError):
201+
logger.warning('Visits counter file was invalid and has been reset', extra={
202+
'visits_file': visits_file_path,
203+
})
204+
_atomic_write_text(visits_file_path, '0\n')
205+
206+
207+
def get_visits_count() -> int:
208+
"""Read current visits count from persistent storage."""
209+
visits_file_path = get_visits_file_path()
210+
211+
with visits_lock:
212+
_ensure_visits_storage(visits_file_path)
213+
try:
214+
with open(visits_file_path, 'r', encoding='utf-8') as visits_file:
215+
return int((visits_file.read().strip() or '0'))
216+
except (OSError, ValueError):
217+
logger.warning('Visits counter read failed, resetting to 0', extra={
218+
'visits_file': visits_file_path,
219+
})
220+
_atomic_write_text(visits_file_path, '0\n')
221+
return 0
222+
223+
224+
def increment_visits_count() -> int:
225+
"""Increment visits count and persist the new value."""
226+
visits_file_path = get_visits_file_path()
227+
228+
with visits_lock:
229+
_ensure_visits_storage(visits_file_path)
230+
231+
try:
232+
with open(visits_file_path, 'r', encoding='utf-8') as visits_file:
233+
current_count = int((visits_file.read().strip() or '0'))
234+
except (OSError, ValueError):
235+
logger.warning('Visits counter read failed during increment, resetting to 0', extra={
236+
'visits_file': visits_file_path,
237+
})
238+
current_count = 0
239+
240+
new_count = current_count + 1
241+
_atomic_write_text(visits_file_path, f'{new_count}\n')
242+
return new_count
243+
244+
164245
@app.get("/")
165246
async def root(request: Request) -> Dict[str, Any]:
166247
"""
@@ -170,6 +251,7 @@ async def root(request: Request) -> Dict[str, Any]:
170251
Dict containing service, system, runtime, request info and available endpoints
171252
"""
172253
devops_info_endpoint_calls_total.labels(endpoint="/").inc()
254+
visits_count = increment_visits_count()
173255

174256
system_info_start = perf_counter()
175257
system_info = get_system_info()
@@ -189,7 +271,8 @@ async def root(request: Request) -> Dict[str, Any]:
189271
"uptime_seconds": uptime['seconds'],
190272
"uptime_human": uptime['human'],
191273
"current_time": datetime.now(timezone.utc).isoformat(),
192-
"timezone": "UTC"
274+
"timezone": "UTC",
275+
"visits": visits_count
193276
},
194277
"request": {
195278
"client_ip": request.client.host if request.client else "unknown",
@@ -212,6 +295,11 @@ async def root(request: Request) -> Dict[str, Any]:
212295
"path": "/metrics",
213296
"method": "GET",
214297
"description": "Prometheus metrics"
298+
},
299+
{
300+
"path": "/visits",
301+
"method": "GET",
302+
"description": "Current visits counter"
215303
}
216304
]
217305
}
@@ -243,14 +331,29 @@ async def metrics() -> Response:
243331
return Response(content=generate_latest(), media_type=CONTENT_TYPE_LATEST)
244332

245333

334+
@app.get("/visits")
335+
async def visits() -> Dict[str, Any]:
336+
"""Return current visits counter value from persistent storage."""
337+
devops_info_endpoint_calls_total.labels(endpoint="/visits").inc()
338+
return {
339+
'visits': get_visits_count(),
340+
'visits_file': get_visits_file_path(),
341+
}
342+
343+
246344
# Startup event
247345
@app.on_event("startup")
248346
async def startup_event():
249347
"""Log application startup"""
348+
visits_file_path = get_visits_file_path()
349+
with visits_lock:
350+
_ensure_visits_storage(visits_file_path)
351+
250352
logger.info("Application started successfully", extra={
251353
"service": "devops-info-service",
252354
"version": "1.0.0",
253-
"startup_time": start_time.isoformat()
355+
"startup_time": start_time.isoformat(),
356+
"visits_file": visits_file_path,
254357
})
255358

256359

app_python/data/.gitkeep

Whitespace-only changes.

app_python/docker-compose.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
services:
2+
devops-info-service:
3+
build: .
4+
container_name: devops-info-service
5+
ports:
6+
- "5000:5000"
7+
environment:
8+
HOST: 0.0.0.0
9+
PORT: "5000"
10+
DEBUG: "false"
11+
LOG_LEVEL: INFO
12+
VISITS_FILE: /data/visits
13+
volumes:
14+
- ./data:/data
15+
restart: unless-stopped

0 commit comments

Comments
 (0)