Files
Alexander Abakumov 15e744da49 Initial commit: Complete Software Architecture course with 11 modules
- Added comprehensive course structure with 11 modules
- Included theoretical content, practical examples, and tasks
- Multi-language code examples (Java, Rust, Node.js, Python)
- Detailed analysis of architectural approaches and trade-offs
- Complete caching strategies documentation
- Architecture examples from real-world systems (Twitter, Netflix, Uber)
- Structured navigation and learning path
- Ready for educational use
2025-09-09 22:08:07 +03:00

2010 lines
59 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Модуль 5: Мониторинг и наблюдаемость
## Содержание модуля
1. [Основы мониторинга](#основы-мониторинга)
2. [Prometheus и Grafana](#prometheus-и-grafana)
3. [Логирование](#логирование)
4. [Distributed Tracing](#distributed-tracing)
5. [Observability](#observability)
6. [Alerting](#alerting)
7. [SLI/SLO/SLA](#slislosla)
---
## Основы мониторинга
### Что такое мониторинг
**Мониторинг** — процесс сбора, анализа и использования информации о работе системы для обеспечения её стабильности и производительности.
### Цели мониторинга
#### 1. Проактивное обнаружение проблем
- Выявление проблем до их влияния на пользователей
- Предотвращение каскадных отказов
- Оптимизация производительности
#### 2. Реактивное решение проблем
- Быстрая диагностика инцидентов
- Сокращение времени восстановления (MTTR)
- Анализ причин отказов
#### 3. Бизнес-аналитика
- Понимание поведения пользователей
- Анализ производительности бизнес-процессов
- Планирование ресурсов
### Типы мониторинга
#### 1. Infrastructure Monitoring
- CPU, Memory, Disk, Network
- Контейнеры и виртуальные машины
- Базы данных и кэши
#### 2. Application Monitoring
- Метрики приложения
- Производительность кода
- Ошибки и исключения
#### 3. Business Monitoring
- Ключевые бизнес-метрики
- Пользовательская активность
- Конверсии и доходы
### Метрики мониторинга
#### 1. Counters (Счетчики)
**Определение:** Метрики, которые только увеличиваются.
**Технический пример - Advanced Metrics Collection:**
```java
@Component
public class RequestCounter {
private final Counter requestCounter;
private final Counter errorCounter;
private final Timer requestTimer;
private final MeterRegistry meterRegistry;
public RequestCounter(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.requestCounter = Counter.builder("http.requests.total")
.description("Total number of HTTP requests")
.tag("service", "user-service")
.register(meterRegistry);
this.errorCounter = Counter.builder("http.errors.total")
.description("Total number of HTTP errors")
.tag("service", "user-service")
.register(meterRegistry);
this.requestTimer = Timer.builder("http.request.duration")
.description("HTTP request duration")
.tag("service", "user-service")
.register(meterRegistry);
}
public void increment(String method, String endpoint, String status) {
requestCounter.increment(
Tags.of(
"method", method,
"endpoint", endpoint,
"status", status
)
);
if (Integer.parseInt(status) >= 400) {
errorCounter.increment(
Tags.of(
"method", method,
"endpoint", endpoint,
"error_type", getErrorType(status)
)
);
}
}
public Timer.Sample startTimer() {
return Timer.start(meterRegistry);
}
public void recordDuration(Timer.Sample sample, String method, String endpoint) {
sample.stop(requestTimer.tags(
"method", method,
"endpoint", endpoint
));
}
private String getErrorType(String status) {
int statusCode = Integer.parseInt(status);
if (statusCode >= 400 && statusCode < 500) {
return "client_error";
} else if (statusCode >= 500) {
return "server_error";
}
return "unknown";
}
}
// Custom Metrics Service
@Service
public class MetricsService {
private final MeterRegistry meterRegistry;
private final Counter businessCounter;
private final Gauge activeUsersGauge;
private final DistributionSummary orderValueSummary;
public MetricsService(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.businessCounter = Counter.builder("business.events.total")
.description("Business events counter")
.register(meterRegistry);
this.activeUsersGauge = Gauge.builder("active.users")
.description("Number of active users")
.register(meterRegistry, this, MetricsService::getActiveUsersCount);
this.orderValueSummary = DistributionSummary.builder("order.value")
.description("Order value distribution")
.baseUnit("currency")
.register(meterRegistry);
}
public void recordBusinessEvent(String eventType, String userId) {
businessCounter.increment(
Tags.of(
"event_type", eventType,
"user_id", userId
)
);
}
public void recordOrderValue(BigDecimal value, String currency) {
orderValueSummary.record(value.doubleValue());
// Record currency-specific metric
Counter.builder("order.value.by.currency")
.tag("currency", currency)
.register(meterRegistry)
.increment(value.doubleValue());
}
private double getActiveUsersCount() {
// Implementation to get active users count
return 0.0; // Placeholder
}
}
// Metrics Configuration
@Configuration
@EnableConfigurationProperties(MetricsProperties.class)
public class MetricsConfig {
@Bean
public MeterRegistry meterRegistry() {
return new SimpleMeterRegistry();
}
@Bean
public PrometheusMeterRegistry prometheusMeterRegistry() {
return new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
}
@Bean
public CompositeMeterRegistry compositeMeterRegistry(
MeterRegistry simpleRegistry,
PrometheusMeterRegistry prometheusRegistry) {
CompositeMeterRegistry composite = new CompositeMeterRegistry();
composite.add(simpleRegistry);
composite.add(prometheusRegistry);
return composite;
}
@Bean
public TimedAspect timedAspect(MeterRegistry meterRegistry) {
return new TimedAspect(meterRegistry);
}
@Bean
public CountedAspect countedAspect(MeterRegistry meterRegistry) {
return new CountedAspect(meterRegistry);
}
}
// AOP for automatic metrics
@Aspect
@Component
public class MetricsAspect {
private final MeterRegistry meterRegistry;
private final Timer.Sample sample;
public MetricsAspect(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
@Around("@annotation(org.springframework.web.bind.annotation.GetMapping)")
public Object timeGetRequests(ProceedingJoinPoint joinPoint) throws Throwable {
Timer.Sample sample = Timer.start(meterRegistry);
try {
Object result = joinPoint.proceed();
sample.stop(Timer.builder("http.get.requests")
.tag("endpoint", getEndpointName(joinPoint))
.register(meterRegistry));
return result;
} catch (Exception e) {
sample.stop(Timer.builder("http.get.requests")
.tag("endpoint", getEndpointName(joinPoint))
.tag("error", "true")
.register(meterRegistry));
throw e;
}
}
private String getEndpointName(ProceedingJoinPoint joinPoint) {
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
return method.getDeclaringClass().getSimpleName() + "." + method.getName();
}
}
```
**Node.js примеры - метрики с prom-client:**
```typescript
import express, { Request, Response, NextFunction } from 'express';
import client from 'prom-client';
import { PrismaClient } from '@prisma/client';
const app = express();
const prisma = new PrismaClient();
// Регистр метрик
const register = new client.Registry();
// Добавление стандартных метрик Node.js
client.collectDefaultMetrics({ register });
// Кастомные метрики
const httpRequestDuration = new client.Histogram({
name: 'http_request_duration_seconds',
help: 'Duration of HTTP requests in seconds',
labelNames: ['method', 'route', 'status_code'],
buckets: [0.1, 0.3, 0.5, 0.7, 1, 3, 5, 7, 10]
});
const httpRequestTotal = new client.Counter({
name: 'http_requests_total',
help: 'Total number of HTTP requests',
labelNames: ['method', 'route', 'status_code']
});
const activeConnections = new client.Gauge({
name: 'active_connections',
help: 'Number of active connections'
});
const businessEvents = new client.Counter({
name: 'business_events_total',
help: 'Total number of business events',
labelNames: ['event_type', 'status']
});
const orderValue = new client.Histogram({
name: 'order_value_dollars',
help: 'Order value in dollars',
buckets: [10, 50, 100, 500, 1000, 5000, 10000]
});
// Регистрация метрик
register.registerMetric(httpRequestDuration);
register.registerMetric(httpRequestTotal);
register.registerMetric(activeConnections);
register.registerMetric(businessEvents);
register.registerMetric(orderValue);
// Middleware для автоматического сбора метрик
app.use((req: Request, res: Response, next: NextFunction) => {
const start = Date.now();
res.on('finish', () => {
const duration = (Date.now() - start) / 1000;
const route = req.route?.path || req.path;
httpRequestDuration
.labels(req.method, route, res.statusCode.toString())
.observe(duration);
httpRequestTotal
.labels(req.method, route, res.statusCode.toString())
.inc();
});
next();
});
// Service для работы с метриками
class MetricsService {
private activeConnectionsCount = 0;
incrementActiveConnections(): void {
this.activeConnectionsCount++;
activeConnections.set(this.activeConnectionsCount);
}
decrementActiveConnections(): void {
this.activeConnectionsCount--;
activeConnections.set(this.activeConnectionsCount);
}
recordBusinessEvent(eventType: string, status: string): void {
businessEvents.labels(eventType, status).inc();
}
recordOrderValue(value: number): void {
orderValue.observe(value);
}
}
// Order Service с метриками
class OrderService {
constructor(
private orderRepository: OrderRepository,
private metricsService: MetricsService
) {}
async createOrder(request: CreateOrderRequest): Promise<Order> {
const timer = httpRequestDuration.startTimer();
try {
// Бизнес-логика
const order = await this.orderRepository.save({
userId: request.userId,
amount: request.amount,
status: 'CREATED',
createdAt: new Date()
});
// Запись метрик
this.metricsService.recordBusinessEvent('order_created', 'success');
this.metricsService.recordOrderValue(request.amount);
return order;
} catch (error) {
this.metricsService.recordBusinessEvent('order_created', 'error');
throw error;
} finally {
timer({ method: 'POST', route: '/api/orders', status_code: '200' });
}
}
async getOrder(id: number): Promise<Order | null> {
const timer = httpRequestDuration.startTimer();
try {
const order = await this.orderRepository.findById(id);
return order;
} finally {
timer({ method: 'GET', route: '/api/orders/:id', status_code: '200' });
}
}
}
// Repository
interface OrderRepository {
save(order: Omit<Order, 'id'>): Promise<Order>;
findById(id: number): Promise<Order | null>;
}
class PrismaOrderRepository implements OrderRepository {
async save(order: Omit<Order, 'id'>): Promise<Order> {
return await prisma.order.create({ data: order });
}
async findById(id: number): Promise<Order | null> {
return await prisma.order.findUnique({ where: { id } });
}
}
// Dependency injection
const metricsService = new MetricsService();
const orderRepository = new PrismaOrderRepository();
const orderService = new OrderService(orderRepository, metricsService);
// API endpoints
app.post('/api/orders', async (req: Request, res: Response, next: NextFunction) => {
try {
const order = await orderService.createOrder(req.body);
res.json(order);
} catch (error) {
next(error);
}
});
app.get('/api/orders/:id', async (req: Request, res: Response, next: NextFunction) => {
try {
const id = parseInt(req.params.id);
const order = await orderService.getOrder(id);
if (!order) {
res.status(404).json({ error: 'Order not found' });
return;
}
res.json(order);
} catch (error) {
next(error);
}
});
// Endpoint для метрик Prometheus
app.get('/metrics', async (req: Request, res: Response) => {
res.set('Content-Type', register.contentType);
res.end(await register.metrics());
});
// Health check endpoint
app.get('/health', (req: Request, res: Response) => {
res.json({ status: 'healthy', timestamp: new Date().toISOString() });
});
// Middleware для отслеживания активных соединений
app.use((req: Request, res: Response, next: NextFunction) => {
metricsService.incrementActiveConnections();
res.on('finish', () => {
metricsService.decrementActiveConnections();
});
next();
});
// Error handling middleware
app.use((error: Error, req: Request, res: Response, next: NextFunction) => {
console.error(error);
res.status(500).json({ error: 'Internal server error' });
});
app.listen(3000, () => {
console.log('Server running on port 3000');
console.log('Metrics available at http://localhost:3000/metrics');
});
```
**Node.js примеры с библиотекой prom-client для NestJS:**
```typescript
import { Module } from '@nestjs/common';
import { PrometheusModule } from '@willsoto/nestjs-prometheus';
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { Counter, Histogram, Gauge, register } from 'prom-client';
@Module({
imports: [
PrometheusModule.register({
defaultMetrics: {
enabled: true,
},
}),
],
})
export class AppModule {}
// Кастомные метрики
const httpRequestDuration = new Histogram({
name: 'http_request_duration_seconds',
help: 'Duration of HTTP requests in seconds',
labelNames: ['method', 'route', 'status_code'],
buckets: [0.1, 0.3, 0.5, 0.7, 1, 3, 5, 7, 10],
});
const businessEvents = new Counter({
name: 'business_events_total',
help: 'Total number of business events',
labelNames: ['event_type', 'status'],
});
const activeUsers = new Gauge({
name: 'active_users',
help: 'Number of active users',
});
// Регистрация метрик
register.registerMetric(httpRequestDuration);
register.registerMetric(businessEvents);
register.registerMetric(activeUsers);
@Controller('orders')
export class OrderController {
constructor(private orderService: OrderService) {}
@Post()
async createOrder(@Body() createOrderDto: CreateOrderDto) {
const timer = httpRequestDuration.startTimer();
try {
const order = await this.orderService.createOrder(createOrderDto);
// Запись бизнес-метрик
businessEvents.labels('order_created', 'success').inc();
return order;
} catch (error) {
businessEvents.labels('order_created', 'error').inc();
throw error;
} finally {
timer({ method: 'POST', route: '/orders', status_code: '200' });
}
}
@Get(':id')
async getOrder(@Param('id') id: string) {
const timer = httpRequestDuration.startTimer();
try {
const order = await this.orderService.getOrder(+id);
return order;
} finally {
timer({ method: 'GET', route: '/orders/:id', status_code: '200' });
}
}
}
@Controller('metrics')
export class MetricsController {
@Get()
async getMetrics() {
return register.metrics();
}
}
```
**Rust примеры - Метрики с prometheus:**
```rust
// Метрики с prometheus для Actix Web
use actix_web::{web, App, HttpServer, middleware::Logger};
use prometheus::{Counter, Histogram, Gauge, Registry, Encoder, TextEncoder};
use std::sync::Arc;
use std::time::Instant;
// Глобальные метрики
lazy_static::lazy_static! {
static ref HTTP_REQUESTS_TOTAL: Counter = Counter::new(
"http_requests_total",
"Total number of HTTP requests"
).unwrap();
static ref HTTP_REQUEST_DURATION: Histogram = Histogram::new(
"http_request_duration_seconds",
"Duration of HTTP requests in seconds"
).unwrap();
static ref ACTIVE_CONNECTIONS: Gauge = Gauge::new(
"active_connections",
"Number of active connections"
).unwrap();
static ref BUSINESS_EVENTS_TOTAL: Counter = Counter::new(
"business_events_total",
"Total number of business events"
).unwrap();
static ref ORDER_VALUE: Histogram = Histogram::new(
"order_value_dollars",
"Order value in dollars"
).unwrap();
}
// Метрики сервис
pub struct MetricsService {
registry: Registry,
active_connections_count: Arc<std::sync::atomic::AtomicUsize>,
}
impl MetricsService {
pub fn new() -> Self {
let registry = Registry::new();
// Регистрация метрик
registry.register(Box::new(HTTP_REQUESTS_TOTAL.clone())).unwrap();
registry.register(Box::new(HTTP_REQUEST_DURATION.clone())).unwrap();
registry.register(Box::new(ACTIVE_CONNECTIONS.clone())).unwrap();
registry.register(Box::new(BUSINESS_EVENTS_TOTAL.clone())).unwrap();
registry.register(Box::new(ORDER_VALUE.clone())).unwrap();
Self {
registry,
active_connections_count: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
}
}
pub fn increment_active_connections(&self) {
let count = self.active_connections_count.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
ACTIVE_CONNECTIONS.set(count as f64 + 1.0);
}
pub fn decrement_active_connections(&self) {
let count = self.active_connections_count.fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
ACTIVE_CONNECTIONS.set(count as f64 - 1.0);
}
pub fn record_business_event(&self, event_type: &str, status: &str) {
BUSINESS_EVENTS_TOTAL.with_label_values(&[event_type, status]).inc();
}
pub fn record_order_value(&self, value: f64) {
ORDER_VALUE.observe(value);
}
pub fn get_metrics(&self) -> String {
let mut buffer = Vec::new();
let encoder = TextEncoder::new();
let metric_families = self.registry.gather();
encoder.encode(&metric_families, &mut buffer).unwrap();
String::from_utf8(buffer).unwrap()
}
}
// Order Service с метриками
pub struct OrderService {
metrics_service: Arc<MetricsService>,
}
impl OrderService {
pub fn new(metrics_service: Arc<MetricsService>) -> Self {
Self { metrics_service }
}
pub async fn create_order(&self, request: CreateOrderRequest) -> Result<Order, String> {
let timer = HTTP_REQUEST_DURATION.start_timer();
// Бизнес-логика
let order = Order {
id: uuid::Uuid::new_v4(),
user_id: request.user_id,
amount: request.amount,
status: "CREATED".to_string(),
created_at: chrono::Utc::now(),
};
// Запись метрик
self.metrics_service.record_business_event("order_created", "success");
self.metrics_service.record_order_value(request.amount);
timer.observe_duration(std::time::Duration::from_millis(100));
Ok(order)
}
pub async fn get_order(&self, id: uuid::Uuid) -> Result<Option<Order>, String> {
let timer = HTTP_REQUEST_DURATION.start_timer();
// Бизнес-логика
let order = Some(Order {
id,
user_id: 1,
amount: 100.0,
status: "CREATED".to_string(),
created_at: chrono::Utc::now(),
});
timer.observe_duration(std::time::Duration::from_millis(50));
Ok(order)
}
}
// Структуры данных
#[derive(serde::Deserialize)]
pub struct CreateOrderRequest {
pub user_id: u64,
pub amount: f64,
}
#[derive(serde::Serialize)]
pub struct Order {
pub id: uuid::Uuid,
pub user_id: u64,
pub amount: f64,
pub status: String,
pub created_at: chrono::DateTime<chrono::Utc>,
}
// Handlers
async fn create_order(
order_service: web::Data<Arc<OrderService>>,
request: web::Json<CreateOrderRequest>,
) -> actix_web::Result<actix_web::HttpResponse> {
HTTP_REQUESTS_TOTAL.with_label_values(&["POST", "/api/orders", "200"]).inc();
match order_service.create_order(request.into_inner()).await {
Ok(order) => Ok(actix_web::HttpResponse::Ok().json(order)),
Err(e) => Ok(actix_web::HttpResponse::BadRequest().json(serde_json::json!({
"error": e
}))),
}
}
async fn get_order(
order_service: web::Data<Arc<OrderService>>,
path: web::Path<uuid::Uuid>,
) -> actix_web::Result<actix_web::HttpResponse> {
let id = path.into_inner();
HTTP_REQUESTS_TOTAL.with_label_values(&["GET", "/api/orders/:id", "200"]).inc();
match order_service.get_order(id).await {
Ok(Some(order)) => Ok(actix_web::HttpResponse::Ok().json(order)),
Ok(None) => Ok(actix_web::HttpResponse::NotFound().json(serde_json::json!({
"error": "Order not found"
}))),
Err(e) => Ok(actix_web::HttpResponse::BadRequest().json(serde_json::json!({
"error": e
}))),
}
}
async fn metrics(metrics_service: web::Data<Arc<MetricsService>>) -> actix_web::Result<actix_web::HttpResponse> {
let metrics_data = metrics_service.get_metrics();
Ok(actix_web::HttpResponse::Ok()
.content_type("text/plain")
.body(metrics_data))
}
async fn health() -> actix_web::Result<actix_web::HttpResponse> {
Ok(actix_web::HttpResponse::Ok().json(serde_json::json!({
"status": "healthy",
"timestamp": chrono::Utc::now().to_rfc3339()
})))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let metrics_service = Arc::new(MetricsService::new());
let order_service = Arc::new(OrderService::new(metrics_service.clone()));
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(order_service.clone()))
.app_data(web::Data::new(metrics_service.clone()))
.wrap(Logger::default())
.route("/api/orders", web::post().to(create_order))
.route("/api/orders/{id}", web::get().to(get_order))
.route("/metrics", web::get().to(metrics))
.route("/health", web::get().to(health))
})
.bind("127.0.0.1:3000")?
.run()
.await
}
```
**Python примеры - Метрики с prometheus:**
```python
# Метрики с prometheus для FastAPI
from fastapi import FastAPI, Request, Response
from prometheus_client import Counter, Histogram, Gauge, generate_latest, CONTENT_TYPE_LATEST
import time
import asyncio
from typing import Dict, Any
import uuid
from datetime import datetime
app = FastAPI(title="Metrics Service", version="1.0.0")
# Метрики
HTTP_REQUESTS_TOTAL = Counter(
'http_requests_total',
'Total number of HTTP requests',
['method', 'route', 'status_code']
)
HTTP_REQUEST_DURATION = Histogram(
'http_request_duration_seconds',
'Duration of HTTP requests in seconds',
['method', 'route', 'status_code'],
buckets=[0.1, 0.3, 0.5, 0.7, 1, 3, 5, 7, 10]
)
ACTIVE_CONNECTIONS = Gauge(
'active_connections',
'Number of active connections'
)
BUSINESS_EVENTS_TOTAL = Counter(
'business_events_total',
'Total number of business events',
['event_type', 'status']
)
ORDER_VALUE = Histogram(
'order_value_dollars',
'Order value in dollars',
buckets=[10, 50, 100, 500, 1000, 5000, 10000]
)
# Метрики сервис
class MetricsService:
def __init__(self):
self.active_connections_count = 0
def increment_active_connections(self):
self.active_connections_count += 1
ACTIVE_CONNECTIONS.set(self.active_connections_count)
def decrement_active_connections(self):
self.active_connections_count -= 1
ACTIVE_CONNECTIONS.set(self.active_connections_count)
def record_business_event(self, event_type: str, status: str):
BUSINESS_EVENTS_TOTAL.labels(event_type=event_type, status=status).inc()
def record_order_value(self, value: float):
ORDER_VALUE.observe(value)
# Order Service с метриками
class OrderService:
def __init__(self, metrics_service: MetricsService):
self.metrics_service = metrics_service
async def create_order(self, request: Dict[str, Any]) -> Dict[str, Any]:
# Бизнес-логика
order = {
"id": str(uuid.uuid4()),
"user_id": request["user_id"],
"amount": request["amount"],
"status": "CREATED",
"created_at": datetime.utcnow().isoformat()
}
# Запись метрик
self.metrics_service.record_business_event("order_created", "success")
self.metrics_service.record_order_value(request["amount"])
return order
async def get_order(self, order_id: str) -> Dict[str, Any]:
# Бизнес-логика
order = {
"id": order_id,
"user_id": 1,
"amount": 100.0,
"status": "CREATED",
"created_at": datetime.utcnow().isoformat()
}
return order
# Dependency injection
metrics_service = MetricsService()
order_service = OrderService(metrics_service)
# Middleware для автоматического сбора метрик
@app.middleware("http")
async def metrics_middleware(request: Request, call_next):
start_time = time.time()
response = await call_next(request)
duration = time.time() - start_time
route = request.url.path
method = request.method
status_code = str(response.status_code)
HTTP_REQUEST_DURATION.labels(
method=method,
route=route,
status_code=status_code
).observe(duration)
HTTP_REQUESTS_TOTAL.labels(
method=method,
route=route,
status_code=status_code
).inc()
return response
# API endpoints
@app.post("/api/orders")
async def create_order(request: Dict[str, Any]):
order = await order_service.create_order(request)
return order
@app.get("/api/orders/{order_id}")
async def get_order(order_id: str):
order = await order_service.get_order(order_id)
return order
@app.get("/metrics")
async def metrics():
return Response(
generate_latest(),
media_type=CONTENT_TYPE_LATEST
)
@app.get("/health")
async def health():
return {
"status": "healthy",
"timestamp": datetime.utcnow().isoformat()
}
# Middleware для отслеживания активных соединений
@app.middleware("http")
async def active_connections_middleware(request: Request, call_next):
metrics_service.increment_active_connections()
try:
response = await call_next(request)
return response
finally:
metrics_service.decrement_active_connections()
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=3000)
```
#### 2. Gauges (Измерители)
**Определение:** Метрики, которые могут увеличиваться и уменьшаться.
```java
@Component
public class ActiveConnectionsGauge {
private final AtomicInteger activeConnections = new AtomicInteger(0);
private final Gauge activeConnectionsGauge;
public ActiveConnectionsGauge(MeterRegistry meterRegistry) {
this.activeConnectionsGauge = Gauge.builder("active.connections")
.description("Number of active connections")
.register(meterRegistry, this, obj -> obj.activeConnections.get());
}
public void incrementConnections() {
activeConnections.incrementAndGet();
}
public void decrementConnections() {
activeConnections.decrementAndGet();
}
}
```
#### 3. Histograms (Гистограммы)
**Определение:** Метрики для измерения распределения значений.
```java
@Component
public class ResponseTimeHistogram {
private final Timer responseTimeTimer;
public ResponseTimeHistogram(MeterRegistry meterRegistry) {
this.responseTimeTimer = Timer.builder("http.request.duration")
.description("HTTP request duration")
.register(meterRegistry);
}
public void recordResponseTime(String endpoint, Duration duration) {
responseTimeTimer.record(duration);
}
}
```
#### 4. Summaries (Сумматоры)
**Определение:** Метрики для измерения квантилей.
```java
@Component
public class ResponseTimeSummary {
private final DistributionSummary responseTimeSummary;
public ResponseTimeSummary(MeterRegistry meterRegistry) {
this.responseTimeSummary = DistributionSummary.builder("http.request.size")
.description("HTTP request size in bytes")
.register(meterRegistry);
}
public void recordRequestSize(long size) {
responseTimeSummary.record(size);
}
}
```
---
## Prometheus и Grafana
### Prometheus
**Prometheus** — система мониторинга и база данных временных рядов.
#### Архитектура Prometheus
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Application │ │ Application │ │ Application │
│ (Metrics) │ │ (Metrics) │ │ (Metrics) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
└───────────────────────┼───────────────────────┘
│
┌─────────────────┐
│ Prometheus │
│ Server │
└─────────────────┘
│
┌─────────────────┐
│ Grafana │
│ (Dashboard) │
└─────────────────┘
```
#### PromQL (Язык запросов Prometheus)
##### Основные запросы:
```promql
# Простая метрика
http_requests_total
# Метрика с фильтрами
http_requests_total{method="GET", status="200"}
# Агрегация
sum(http_requests_total) by (method)
# Rate (скорость изменения)
rate(http_requests_total[5m])
# Histogram quantiles
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))
```
##### Примеры запросов:
```promql
# Количество запросов в секунду
rate(http_requests_total[5m])
# Среднее время ответа
rate(http_request_duration_seconds_sum[5m]) / rate(http_request_duration_seconds_count[5m])
# 95-й перцентиль времени ответа
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))
# Ошибки в процентах
rate(http_requests_total{status=~"5.."}[5m]) / rate(http_requests_total[5m]) * 100
```
#### Spring Boot + Prometheus
```java
@Configuration
public class PrometheusConfig {
@Bean
public MeterRegistry meterRegistry() {
return new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
}
}
@RestController
public class MetricsController {
private final MeterRegistry meterRegistry;
public MetricsController(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
@GetMapping("/metrics")
public String metrics() {
return ((PrometheusMeterRegistry) meterRegistry).scrape();
}
}
```
### Grafana
**Grafana** — платформа для визуализации метрик и создания дашбордов.
#### Создание дашборда
```json
{
"dashboard": {
"title": "Application Metrics",
"panels": [
{
"title": "Request Rate",
"type": "graph",
"targets": [
{
"expr": "rate(http_requests_total[5m])",
"legendFormat": "{{method}} {{endpoint}}"
}
]
},
{
"title": "Response Time",
"type": "graph",
"targets": [
{
"expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))",
"legendFormat": "95th percentile"
}
]
}
]
}
}
```
#### Алерты в Grafana
```json
{
"alert": {
"name": "High Error Rate",
"message": "Error rate is above 5%",
"conditions": [
{
"query": {
"params": ["A", "5m", "now"]
},
"reducer": {
"type": "last",
"params": []
},
"evaluator": {
"params": [0.05],
"type": "gt"
}
}
]
}
}
```
---
## Логирование
### Принципы логирования
#### 1. Structured Logging
**Определение:** Логирование в структурированном формате (JSON).
**Технический пример - Advanced Structured Logging:**
```java
@Slf4j
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
@Autowired
private MetricsService metricsService;
@PostMapping("/orders")
public ResponseEntity<Order> createOrder(@RequestBody CreateOrderRequest request) {
String correlationId = MDC.get("correlationId");
String userId = MDC.get("userId");
// Structured logging with business context
log.info("Order creation started",
StructuredLogging.builder()
.correlationId(correlationId)
.userId(userId)
.customerId(request.getCustomerId())
.orderValue(request.getTotalValue())
.itemsCount(request.getItems().size())
.currency(request.getCurrency())
.paymentMethod(request.getPaymentMethod())
.build()
);
Timer.Sample sample = Timer.start();
try {
Order order = orderService.createOrder(request);
// Success logging with performance metrics
Duration duration = Duration.ofNanos(sample.stop());
log.info("Order created successfully",
StructuredLogging.builder()
.correlationId(correlationId)
.userId(userId)
.orderId(order.getId())
.customerId(request.getCustomerId())
.status(order.getStatus())
.processingTimeMs(duration.toMillis())
.build()
);
// Record business metrics
metricsService.recordBusinessEvent("order_created", userId);
metricsService.recordOrderValue(request.getTotalValue(), request.getCurrency());
return ResponseEntity.ok(order);
} catch (ValidationException e) {
log.warn("Order validation failed",
StructuredLogging.builder()
.correlationId(correlationId)
.userId(userId)
.customerId(request.getCustomerId())
.errorType("validation_error")
.errorMessage(e.getMessage())
.validationErrors(e.getValidationErrors())
.build()
);
return ResponseEntity.badRequest().body(new ErrorResponse(e.getMessage()));
} catch (PaymentException e) {
log.error("Payment processing failed",
StructuredLogging.builder()
.correlationId(correlationId)
.userId(userId)
.customerId(request.getCustomerId())
.errorType("payment_error")
.errorMessage(e.getMessage())
.paymentMethod(request.getPaymentMethod())
.orderValue(request.getTotalValue())
.build()
);
return ResponseEntity.status(HttpStatus.PAYMENT_REQUIRED)
.body(new ErrorResponse("Payment processing failed"));
} catch (Exception e) {
log.error("Unexpected error during order creation",
StructuredLogging.builder()
.correlationId(correlationId)
.userId(userId)
.customerId(request.getCustomerId())
.errorType("system_error")
.errorMessage(e.getMessage())
.stackTrace(getStackTrace(e))
.build()
);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("Internal server error"));
}
}
}
// Structured Logging Builder
public class StructuredLogging {
private final Map<String, Object> fields = new HashMap<>();
public static Builder builder() {
return new Builder();
}
public static class Builder {
private final StructuredLogging logging = new StructuredLogging();
public Builder correlationId(String correlationId) {
logging.fields.put("correlationId", correlationId);
return this;
}
public Builder userId(String userId) {
logging.fields.put("userId", userId);
return this;
}
public Builder customerId(Long customerId) {
logging.fields.put("customerId", customerId);
return this;
}
public Builder orderId(Long orderId) {
logging.fields.put("orderId", orderId);
return this;
}
public Builder orderValue(BigDecimal value) {
logging.fields.put("orderValue", value);
return this;
}
public Builder itemsCount(Integer count) {
logging.fields.put("itemsCount", count);
return this;
}
public Builder currency(String currency) {
logging.fields.put("currency", currency);
return this;
}
public Builder paymentMethod(String method) {
logging.fields.put("paymentMethod", method);
return this;
}
public Builder status(String status) {
logging.fields.put("status", status);
return this;
}
public Builder processingTimeMs(Long timeMs) {
logging.fields.put("processingTimeMs", timeMs);
return this;
}
public Builder errorType(String errorType) {
logging.fields.put("errorType", errorType);
return this;
}
public Builder errorMessage(String message) {
logging.fields.put("errorMessage", message);
return this;
}
public Builder validationErrors(List<String> errors) {
logging.fields.put("validationErrors", errors);
return this;
}
public Builder stackTrace(String stackTrace) {
logging.fields.put("stackTrace", stackTrace);
return this;
}
public StructuredLogging build() {
return logging;
}
}
public Map<String, Object> getFields() {
return fields;
}
@Override
public String toString() {
try {
return new ObjectMapper().writeValueAsString(fields);
} catch (Exception e) {
return fields.toString();
}
}
}
// Advanced Correlation ID Filter
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CorrelationIdFilter implements Filter {
private static final String CORRELATION_ID_HEADER = "X-Correlation-ID";
private static final String USER_ID_HEADER = "X-User-ID";
private static final String REQUEST_ID_HEADER = "X-Request-ID";
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// Extract or generate correlation ID
String correlationId = httpRequest.getHeader(CORRELATION_ID_HEADER);
if (correlationId == null || correlationId.isEmpty()) {
correlationId = UUID.randomUUID().toString();
}
// Extract user ID
String userId = httpRequest.getHeader(USER_ID_HEADER);
// Generate request ID
String requestId = UUID.randomUUID().toString();
// Set MDC
MDC.put("correlationId", correlationId);
MDC.put("userId", userId);
MDC.put("requestId", requestId);
MDC.put("service", "order-service");
MDC.put("version", "1.0.0");
// Add headers to response
httpResponse.setHeader(CORRELATION_ID_HEADER, correlationId);
httpResponse.setHeader(REQUEST_ID_HEADER, requestId);
try {
chain.doFilter(request, response);
} finally {
MDC.clear();
}
}
}
// Logback Configuration for Structured Logging
// logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!-- Console Appender with JSON format -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<timestamp/>
<logLevel/>
<loggerName/>
<message/>
<mdc/>
<stackTrace/>
</providers>
</encoder>
</appender>
<!-- File Appender with JSON format -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/app/logs/order-service.json</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>/app/logs/order-service.%d{yyyy-MM-dd}.%i.json</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>3GB</totalSizeCap>
</rollingPolicy>
<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<timestamp/>
<logLevel/>
<loggerName/>
<message/>
<mdc/>
<stackTrace/>
</providers>
</encoder>
</appender>
<!-- Error-specific appender -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/app/logs/order-service-errors.json</file>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>/app/logs/order-service-errors.%d{yyyy-MM-dd}.%i.json</fileNamePattern>
<maxFileSize>50MB</maxFileSize>
<maxHistory>90</maxHistory>
</rollingPolicy>
<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<timestamp/>
<logLevel/>
<loggerName/>
<message/>
<mdc/>
<stackTrace/>
</providers>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
<appender-ref ref="ERROR_FILE"/>
</root>
</configuration>
```
#### 2. Log Levels
- **ERROR** — критические ошибки
- **WARN** — предупреждения
- **INFO** — информационные сообщения
- **DEBUG** — отладочная информация
- **TRACE** — детальная трассировка
#### 3. Correlation ID
```java
@Component
public class CorrelationIdFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String correlationId = httpRequest.getHeader("X-Correlation-ID");
if (correlationId == null) {
correlationId = UUID.randomUUID().toString();
}
MDC.put("correlationId", correlationId);
try {
chain.doFilter(request, response);
} finally {
MDC.clear();
}
}
}
```
### ELK Stack (Elasticsearch, Logstash, Kibana)
#### Конфигурация Logstash
```ruby
# logstash.conf
input {
beats {
port => 5044
}
}
filter {
if [fields][service] == "order-service" {
grok {
match => { "message" => "%{TIMESTAMP_ISO8601:timestamp} %{LOGLEVEL:level} %{GREEDYDATA:message}" }
}
date {
match => [ "timestamp", "ISO8601" ]
}
}
}
output {
elasticsearch {
hosts => ["elasticsearch:9200"]
index => "logs-%{+YYYY.MM.dd}"
}
}
```
#### Шаблон индекса Elasticsearch
```json
{
"index_patterns": ["logs-*"],
"template": {
"settings": {
"number_of_shards": 1,
"number_of_replicas": 1
},
"mappings": {
"properties": {
"@timestamp": {
"type": "date"
},
"level": {
"type": "keyword"
},
"message": {
"type": "text"
},
"correlationId": {
"type": "keyword"
},
"service": {
"type": "keyword"
}
}
}
}
}
```
### Fluentd
```ruby
# fluent.conf
<source>
@type tail
path /var/log/application.log
pos_file /var/log/fluentd.log.pos
tag application.logs
format json
</source>
<filter application.logs>
@type record_transformer
<record>
service "order-service"
environment "production"
</record>
</filter>
<match application.logs>
@type elasticsearch
host elasticsearch
port 9200
index_name logs
type_name _doc
</match>
```
---
## Distributed Tracing
### Что такое Distributed Tracing
**Distributed Tracing** — метод отслеживания запросов через распределенную систему.
### OpenTelemetry
```java
@Configuration
public class TracingConfig {
@Bean
public OpenTelemetry openTelemetry() {
return OpenTelemetrySdk.builder()
.setTracerProvider(
SdkTracerProvider.builder()
.addSpanProcessor(BatchSpanProcessor.builder(
JaegerGrpcSpanExporter.builder()
.setEndpoint("http://jaeger:14250")
.build()
).build())
.build()
)
.build();
}
}
@Service
public class OrderService {
private final Tracer tracer;
public OrderService(OpenTelemetry openTelemetry) {
this.tracer = openTelemetry.getTracer("order-service");
}
public Order createOrder(CreateOrderRequest request) {
Span span = tracer.spanBuilder("create-order")
.setAttribute("customer.id", request.getCustomerId())
.setAttribute("order.value", request.getTotalValue().doubleValue())
.startSpan();
try (Scope scope = span.makeCurrent()) {
// бизнес-логика
Order order = processOrder(request);
span.setStatus(StatusCode.OK);
span.setAttribute("order.id", order.getId());
return order;
} catch (Exception e) {
span.setStatus(StatusCode.ERROR, e.getMessage());
span.recordException(e);
throw e;
} finally {
span.end();
}
}
}
```
### Jaeger
#### Конфигурация Jaeger
```yaml
# docker-compose.yml
version: '3.8'
services:
jaeger:
image: jaegertracing/all-in-one:latest
ports:
- "16686:16686"
- "14268:14268"
environment:
- COLLECTOR_OTLP_ENABLED=true
```
#### Анализ трассировок
```java
@Component
public class TraceAnalyzer {
public void analyzeTrace(String traceId) {
// анализ трассировки
List<Span> spans = getSpansByTraceId(traceId);
// поиск узких мест
Span slowestSpan = spans.stream()
.max(Comparator.comparing(Span::getDuration))
.orElse(null);
// анализ ошибок
List<Span> errorSpans = spans.stream()
.filter(span -> span.getStatus().getStatusCode() == StatusCode.ERROR)
.collect(Collectors.toList());
}
}
```
---
## Observability
### Три столпа Observability
#### 1. Metrics (Метрики)
- Количественные измерения
- Агрегированные данные
- Хорошо для мониторинга трендов
#### 2. Logs (Логи)
- События с временными метками
- Детальная информация
- Хорошо для отладки
#### 3. Traces (Трассировки)
- Путь запроса через систему
- Временные зависимости
- Хорошо для анализа производительности
### Паттерны наблюдаемости
#### 1. Health Checks
```java
@Component
public class HealthIndicator implements HealthIndicator {
@Override
public Health health() {
try {
// проверка зависимостей
boolean dbHealthy = checkDatabase();
boolean cacheHealthy = checkCache();
if (dbHealthy && cacheHealthy) {
return Health.up()
.withDetail("database", "UP")
.withDetail("cache", "UP")
.build();
} else {
return Health.down()
.withDetail("database", dbHealthy ? "UP" : "DOWN")
.withDetail("cache", cacheHealthy ? "UP" : "DOWN")
.build();
}
} catch (Exception e) {
return Health.down(e).build();
}
}
}
```
#### 2. Custom Metrics
```java
@Component
public class BusinessMetrics {
private final Counter ordersCreated;
private final Timer orderProcessingTime;
private final Gauge activeUsers;
public BusinessMetrics(MeterRegistry meterRegistry) {
this.ordersCreated = Counter.builder("orders.created")
.description("Total orders created")
.register(meterRegistry);
this.orderProcessingTime = Timer.builder("orders.processing.time")
.description("Order processing time")
.register(meterRegistry);
this.activeUsers = Gauge.builder("users.active")
.description("Number of active users")
.register(meterRegistry, this, obj -> obj.getActiveUsersCount());
}
public void recordOrderCreated() {
ordersCreated.increment();
}
public void recordOrderProcessingTime(Duration duration) {
orderProcessingTime.record(duration);
}
private int getActiveUsersCount() {
// логика подсчета активных пользователей
return 0;
}
}
```
---
## Alerting
### Принципы алертинга
#### 1. Alert Fatigue Prevention
- Только важные алерты
- Группировка похожих алертов
- Эскалация по времени
#### 2. Alert Classification
- **Critical** — система недоступна
- **Warning** — деградация производительности
- **Info** — информационные уведомления
### Правила оповещений Prometheus
```yaml
# alerts.yml
groups:
- name: application
rules:
- alert: HighErrorRate
expr: rate(http_requests_total{status=~"5.."}[5m]) / rate(http_requests_total[5m]) > 0.05
for: 2m
labels:
severity: critical
annotations:
summary: "High error rate detected"
description: "Error rate is {{ $value }} for {{ $labels.instance }}"
- alert: HighResponseTime
expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 1
for: 5m
labels:
severity: warning
annotations:
summary: "High response time"
description: "95th percentile response time is {{ $value }}s"
```
### Конфигурация Alertmanager
```yaml
# alertmanager.yml
global:
smtp_smarthost: 'localhost:587'
smtp_from: 'alerts@example.com'
route:
group_by: ['alertname']
group_wait: 10s
group_interval: 10s
repeat_interval: 1h
receiver: 'web.hook'
receivers:
- name: 'web.hook'
webhook_configs:
- url: 'http://alert-webhook:5001/alerts'
- name: 'email'
email_configs:
- to: 'admin@example.com'
subject: 'Alert: {{ .GroupLabels.alertname }}'
body: |
{{ range .Alerts }}
Alert: {{ .Annotations.summary }}
Description: {{ .Annotations.description }}
{{ end }}
```
---
## SLI/SLO/SLA
### SLI (Индикатор уровня сервиса)
**SLI** — метрика, которая измеряет качество сервиса.
#### Примеры SLI:
- **Availability**: `successful_requests / total_requests`
- **Latency**: `requests_completed_within_threshold / total_requests`
- **Throughput**: `successful_requests_per_second`
```java
@Component
public class SLICalculator {
public double calculateAvailability(String service, Duration window) {
Counter successfulRequests = Counter.builder("requests.successful")
.tag("service", service)
.register(meterRegistry);
Counter totalRequests = Counter.builder("requests.total")
.tag("service", service)
.register(meterRegistry);
double successful = successfulRequests.count();
double total = totalRequests.count();
return total > 0 ? successful / total : 0.0;
}
public double calculateLatency(String service, Duration threshold) {
Timer requestTimer = Timer.builder("request.duration")
.tag("service", service)
.register(meterRegistry);
// логика расчета латентности
return 0.95; // пример
}
}
```
### SLO (Цель уровня сервиса)
**SLO** — целевое значение для SLI.
#### Примеры SLO:
- **Availability**: 99.9% uptime
- **Latency**: 95% of requests < 200ms
- **Error Rate**: < 0.1% errors
```java
@Component
public class SLOMonitor {
private final Map<String, SLO> slos = Map.of(
"availability", new SLO("availability", 0.999, "99.9% uptime"),
"latency", new SLO("latency", 0.95, "95% < 200ms"),
"error_rate", new SLO("error_rate", 0.001, "< 0.1% errors")
);
public boolean checkSLO(String sloName, double currentValue) {
SLO slo = slos.get(sloName);
return currentValue >= slo.getTarget();
}
}
```
### SLA (Соглашение об уровне сервиса)
**SLA** — соглашение с клиентами о качестве сервиса.
#### Компоненты SLA:
- **Service Description** — описание сервиса
- **Performance Metrics** — метрики производительности
- **Availability Targets** — цели доступности
- **Remediation** — меры по устранению нарушений
---
## Практические задания
### Задание 1: Prometheus + Grafana
1. Настройте Prometheus для сбора метрик
2. Создайте дашборд в Grafana
3. Настройте алерты
### Задание 2: Distributed Tracing
1. Интегрируйте OpenTelemetry в приложение
2. Настройте Jaeger для анализа трассировок
3. Создайте анализ производительности
### Задание 3: ELK Stack
1. Настройте централизованное логирование
2. Создайте индексы в Elasticsearch
3. Настройте дашборды в Kibana
---
## Дополнительные ресурсы
### Инструменты
- **Metrics**: Prometheus, InfluxDB, TimescaleDB
- **Logging**: ELK Stack, Fluentd, Splunk
- **Tracing**: Jaeger, Zipkin, OpenTelemetry
- **Dashboards**: Grafana, Kibana, DataDog
### Книги
- "Observability Engineering" by Charity Majors
- "Site Reliability Engineering" by Google
- "Distributed Systems Observability" by Cindy Sridharan
---
## Контрольные вопросы
1. Что такое мониторинг и зачем он нужен?
2. Какие типы метрик вы знаете?
3. Как работает Prometheus и PromQL?
4. Что такое structured logging?
5. Как работает distributed tracing?
6. Что такое три столпа observability?
7. Как настроить алертинг?
8. Что такое SLI/SLO/SLA?