Наблюдаемость (observability) – это ключевое свойство современной системы, позволяющее понимать её внутреннее состояние на основе внешних данных. Если мониторинг отвечает на вопрос "что случилось?", то наблюдаемость идет дальше, помогая понять "почему это случилось?".
В .NET существует несколько подходов к решению этой проблемы, но особенно эффективной оказалась связка OpenTelemetry, Prometheus и Grafana. По результатам опроса Cloud Native Computing Foundation за 2023 год, более 62% компаний используют OpenTelemetry в своих проектах, а Prometheus стал стандартом де-факто для сбора метрик в облачных средах.
ASP.NET Core отлично интегрируется со стеком наблюдаемости через OpenTelemetry. Это дает возможность получать данные трех типов:- Метрики – числовые измерения состояния системы за период времени,
- Трассировки – запись последовательности вызовов между компонентами,
- Логи – текстовые записи о событиях в системе.
Реализация наблюдаемости в веб-приложениях на ASP.NET Core не требует кардинальной перестройки кода, но дает огромные преимущества при отладке, профилировании и диагностике проблем. Особенно ценно это для микросервисных архитектур, где сложность взаимодействия компонентов растет экспоненциально с увеличением их количества.
Основы мониторинга: OpenTelemetry как стандарт трейсинга
OpenTelemetry без преувеличения совершил революцию в мире наблюдаемости. Я помню времена, когда каждый вендор продвигал свое проприетарное решение, а разработчикам приходилось выбирать между несовместимыми инструментами. Бывало, внедришь одну систему мониторинга, а через год руководство решает перейти на другую – и тут начинается головная боль с переписыванием всей инструментации кода. В 2019 году два проекта – OpenCensus от Google и OpenTracing от CNCF – объединились, чтобы создать OpenTelemetry. Этот шаг оказался важным для индустрии. Теперь у нас есть открытый стандарт со спецификациями для сбора и передачи телеметрических данных, который не привязан к конкретному вендору.
Архитектура и принципы работы
Архитектура OpenTelemetry состоит из нескольких ключевых компонентов:
1. API – интерфейсы для создания телеметрических данных в коде,
2. SDK – реализация API с настройками сбора и обработки данных,
3. Экспортеры – компоненты для отправки данных в системы мониторинга,
4. Коллектор – необязательный компонент для централизованной обработки и маршрутизации данных.
Основной принцип работы можно описать так: ваше приложение генерирует телеметрические данные через API, SDK их обрабатывает, а экспортеры отправляют в нужные системы хранения или визуализации. Важная концепция в OpenTelemetry – это контекст. Он позволяет связывать информацию между разными компонентами системы, даже если они разделены сетью или запущены на разных машинах. Контекст распространяется через HTTP-заголовки, брокеры сообщений и другие средства коммуникации, обеспечивая целосную картину происходящего.
Когда я впервые столкнулся с этой технологией, меня поразило, насколько гибким может быть инструментарий. Можно настроить множество аспектов: как часто собирать метрики, какие данные сохранять, как агрегировать информацию. И при этом не привязываться к конкретному хранилищу или системе визуализации.
Сравнение с альтернативами
До недавнего времени в .NET экосистеме было несколько популярных альтернатив:
Application Insights – решение от Microsoft, интегрированное с Azure,
Jaeger – система трассировки для микросервисных архитектур,
Zipkin – еще одна система трейсинга, развиваемая сообществом,
New Relic и Datadog – коммерческие решения полного цикла,
Но OpenTelemetry имеет ряд преимуществ:
1. Стандартизация – единый подход к инструментации кода, который поддерживается большинством вендоров.
2. Переносимость – возможность сменить бэкенд без изменения кода инструментации.
3. Расширяемость – простота добавления новых типов данных и атрибутов.
4. Поддержка сообщества – активное развитие и обновления.
Я работал с Application Insights несколько лет и даже был его фанатом. Но когда попробовал OpenTelemetry, понял, что это будущее. Особенно ценно, что Microsoft теперь рекомендует его как предпочтительный способ инструментации даже для интеграции с их собственными сервисами.
Интеграция с ASP.NET Core
Подключить OpenTelemetry к ASP.NET Core на удивление просто. Сначала нужно добавить необходимые пакеты:
| C# | 1
2
3
| dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.Exporter.Console |
|
Затем настроить сервисы в Program.cs или Startup.cs:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| var builder = WebApplication.CreateBuilder(args);
// Добавляем OpenTelemetry
var openTelemetryBuilder = builder.Services.AddOpenTelemetry();
// Настраиваем ресурс (информацию о сервисе)
openTelemetryBuilder.ConfigureResource(resource => resource
.AddService(builder.Environment.ApplicationName));
// Настраиваем метрики
openTelemetryBuilder.WithMetrics(metrics => metrics
.AddAspNetCoreInstrumentation()
.AddConsoleExporter());
var app = builder.Build();
// ...остальная конфигурация |
|
Этого достаточно, чтобы начать собирать базовые метрики вроде длительности запросов, количества ошибок и т.д. Но реальная мощь OpenTelemetry раскрывается, когда вы начинаете добавлять собственные метрики и трейсы.
Если вы переводите существующий проект на OpenTelemetry, рекомендую начинать постепенно. Сначала настройте базовую инструментацию, затем определите наиболее критичные компоненты системы и инструментируйте их в первую очередь. Такой подход позволит быстрее получить пользу без необходимости сразу менять весь код. В одном из моих проектов мы даже создали специальные атрибуты, которые позволяли декларативно добавлять трейсинг к методам:
| C# | 1
2
3
4
5
| [Trace("ImportantBusinessOperation")]
public async Task<Result> ProcessOrder(Order order)
{
// логика обработки заказа
} |
|
Реализация такого атрибута через интерцепторы позволила значительно упростить инструментацию и сделать её более унифицированной.
Настройка кастомных трейсов в бизнес-логике
Настройка трейсов в бизнес-логике требует больше ручной работы, но даёт полный контроль над собираемыми данными. Типичный сценарий выглядит так:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // Создаем трейсер
using var activity = _activitySource.StartActivity("ProcessPayment");
try
{
// Добавляем атрибуты к трейсу
activity?.SetTag("payment.amount", payment.Amount);
activity?.SetTag("payment.method", payment.Method);
// Выполняем бизнес-операцию
var result = await _paymentGateway.ProcessAsync(payment);
// Отмечаем успешное завершение
activity?.SetStatus(ActivityStatusCode.Ok);
return result;
}
catch (Exception ex)
{
// Фиксируем ошибку в трейсе
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
throw;
} |
|
Такой подход особенно полезен для отслеживания важных бизнес-операций. Однажды мне пришлось разбираться, почему процесс обработки платежей иногда затягивался. Благодаря детальным трейсам мы обнаружили, что в определённых случаях шлюз оплаты делал повторные запросы к своему бэкенду, что значительно увеличивало время ответа.
Интересно, что OpenTelemetry напрямую не зависит от конкретного logger-а, что даёт свободу использовать привычные инструменты логирования (Serilog, NLog и другие) вместе с телеметрией. Связывание логов с трейсами происходит через контекст активности. Для реализации распределенных систем, таких как микросервисные архитектуры, OpenTelemetry предлагает механизм распространения контекста. Это позволяет отслеживать путь запроса через несколько сервисов, сохраняя причинно-следственные связи.
Я пришол к выводу, что без хорошей наблюдаемости современные распределенные системы становятся практически неуправляемыми. Неочевидные зависимости между сервисами, асинхронные потоки данных, ретраи и таймауты – всё это создаёт сложную картину, в которой без трейсинга разобраться почти невозможно.
Производительность OpenTelemetry: overhead и влияние на latency приложений
Когда речь заходит о внедрении дополнительных инструментов в рабочую систему, первый вопрос, который у меня возникает – какова цена? И я имею в виду не деньги, а ресурсы и производительность. Внедрение любой телеметрии неизбежно создает overhead, и OpenTelemetry не исключение.
В своей практике я провел несколько тестов на реальных проектах. В среднем, базовая реализация OpenTelemetry добавляет около 3-5% к времени обработки запроса. Эта цифра может увеличиваться до 10-15% при интенсивном использовании кастомных метрик и трейсов. Но это вполне приемлемая плата за видимость происходящего внутри приложения. Один из способов снизить накладные расходы – использовать семплирование. Вместо того чтобы собирать все трейсы, можно настроить сбор только части из них:
| C# | 1
2
3
4
| services.AddOpenTelemetry().WithTracing(builder => builder
.SetSampler(new TraceIdRatioBasedSampler(0.1)) // собираем только 10% трейсов
.AddAspNetCoreInstrumentation()
.AddOtlpExporter()); |
|
В высоконагруженных системах семплирование – это необходимость. На одном из проектов мы настроили адаптивное семплирование: при низкой нагрузке собирали до 50% трейсов, а при пиковой – снижали до 1-5%. Это позволило сохранить баланс между информативностью и производительностью. Еще один интересный подход – приоритизация трейсов. Например, можно настроить сбор 100% трейсов для критичных операций (платежи, регистрация) и минимальное семплирование для рутинных запросов:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| public class PrioritySampler : Sampler
{
public override SamplingResult ShouldSample(in SamplingParameters parameters)
{
// Анализируем путь запроса или другие атрибуты
if (parameters.Name.Contains("Payment") || parameters.Name.Contains("Register"))
{
return new SamplingResult(SamplingDecision.RecordAndSample);
}
// Для остальных запросов используем выборочное семплирование
return new Random().NextDouble() < 0.05
? new SamplingResult(SamplingDecision.RecordAndSample)
: new SamplingResult(SamplingDecision.Drop);
}
} |
|
Я обнаружил, что при правильной настройке семплирования и фильтрации собираемых данных, overhead от OpenTelemetry становится практически незаметным для конечных пользователей, но значительно улучшает возможности команды по отладке и мониторингу. Важно помнить, что метрики и трейсы – это разные типы данных с разным воздействием на производительность. Метрики обычно агрегируются в памяти и отправляются периодически, поэтому их влияние на latency минимально. Трейсы же создаются для каждого запроса и содержат более детальную информацию, что делает их "дороже" с точки зрения ресурсов.
Трассировка распределённых транзакций между микросервисами
Одна из самых сложных задач в микросервисной архитектуре – отслеживание запроса, проходящего через множество сервисов. Без правильно настроенного трейсинга расследование проблем превращается в настоящий детектив с неочевидными уликами и противоречивыми показаниями (логами). OpenTelemetry решает эту проблему через концепцию распространения контекста (context propagation). Идея проста: каждый запрос получает уникальный идентификатор (trace ID), который передается между сервисами через заголовки HTTP, сообщения в очередях или другие механизмы коммуникации.
Для HTTP-запросов это происходит автоматически благодаря встроенным инструментам:
| C# | 1
2
3
4
| services.AddOpenTelemetry().WithTracing(builder => builder
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddOtlpExporter()); |
|
Но для других типов коммуникации (например, RabbitMQ, Kafka) может потребоваться ручное распространение контекста:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| // При отправке сообщения
var currentActivity = Activity.Current;
if (currentActivity != null)
{
// Добавляем контекст в метаданные сообщения
message.Properties.Add("TraceId", currentActivity.TraceId.ToString());
message.Properties.Add("SpanId", currentActivity.SpanId.ToString());
}
// При получении сообщения
var traceIdStr = message.Properties["TraceId"] as string;
var parentSpanIdStr = message.Properties["SpanId"] as string;
if (traceIdStr != null && parentSpanIdStr != null)
{
var traceId = ActivityTraceId.CreateFromString(traceIdStr);
var parentSpanId = ActivitySpanId.CreateFromString(parentSpanIdStr);
// Создаем новую активность с родительским контекстом
using var activity = _activitySource.StartActivity(
"ProcessMessage",
ActivityKind.Consumer,
new ActivityContext(traceId, parentSpanId, ActivityTraceFlags.Recorded));
// Дальнейшая обработка сообщения
} |
|
В своей практике я столкнулся с ситуацией, когда один из микросервисов периодически зависал. Логи не давали ясной картины, так как проблема была на стыке нескольких сервисов. После настройки распределенного трейсинга мы обнаружили, что при определенных условиях формировалась циклическая зависимость: сервис A вызывал B, тот обращался к C, который снова вызывал A с немного измененными параметрами. Этот цикл не был очевиден без end-to-end трейсинга.
Для сложных микросервисных архитектур я рекомендую использовать OpenTelemetry Collector. Это компонент, который собирает телеметрию со всех сервисов, обрабатывает её и перенаправляет в целевые системы. Он позволяет централизовать обработку данных, снизить нагрузку на сервисы и упростить конфигурацию. Типичная архитектура с Collector выглядит так:
1. Микросервисы отправляют телеметрию в локальный или центральный Collector,
2. Collector обрабатывает данные: фильтрует, агрегирует, обогащает,
3. Обработанные данные направляются в системы хранения и визуализации,
Настройка OpenTelemetry Collector в Kubernetes выглядит примерно так:
| YAML | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| apiVersion: v1
kind: ConfigMap
metadata:
name: otel-collector-conf
data:
config.yaml: |
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch:
timeout: 1s
send_batch_size: 1024
resourcedetection:
detectors: [env, k8s]
exporters:
prometheus:
endpoint: 0.0.0.0:8889
jaeger:
endpoint: jaeger-collector:14250
tls:
insecure: true
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch, resourcedetection]
exporters: [jaeger]
metrics:
receivers: [otlp]
processors: [batch, resourcedetection]
exporters: [prometheus] |
|
Подключить prometheus к celery-flower, запущенных в разных docker.compose.yml Хочу подключит prometheus к celery-flower. Делаю всё по инструкции.
Есть 2 docker-compose.yml.
... Разница между ASP.NET Core 2, ASP.NET Core MVC, ASP.NET MVC 5 и ASP.NET WEBAPI 2 Здравствуйте. Я в бекенд разработке полный ноль. В чем разница между вышеперечисленными... ASP.NET Core. Старт - что нужно знать, чтобы стать ASP.NET Core разработчиком? Попалось хор краткое обзорное видео 2016 года с таким названием - Что нужно знать, чтобы стать... Какая разница между ASP .Net Core и ASP .Net Core MVC? Какая разница между ASP .Net Core и ASP .Net Core MVC? Или я может что-то не так понял? И...
Prometheus: сбор и хранение метрик
После настройки OpenTelemetry следующим важным звеном в цепочке наблюдаемости становится система сбора и хранения метрик. Тут выходит Prometheus — проект с открытым исходным кодом, созданный в SoundCloud и ставший частью Cloud Native Computing Foundation. За годы работы с разными решениями для мониторинга я пришол к выводу, что Prometheus выделяется своей простотой, масштабируемостью и надежностью.
В отличие от многих других систем, Prometheus использует модель pull (вытягивание данных), а не push (отправка данных). Это означает, что Prometheus сам периодически опрашивает (scrape) ваши приложения, чтобы получить текущие метрики. Такой подход имеет ряд преимуществ: централизованная конфигурация, возможность обнаружения проблем с доступностью сервисов и снижение нагрузки на само приложение.
Настройка endpoints для метрик
Первый шаг — настроить ASP.NET Core приложение для предоставления метрик в формате, понятном Prometheus. OpenTelemetry упрощает эту задачу с помощью экспортера. Для начала нужно установить соответствующий пакет:
| C# | 1
| dotnet add package OpenTelemetry.Exporter.Prometheus.AspNetCore |
|
Затем добавить экспортер в конфигурацию OpenTelemetry:
| C# | 1
2
3
4
5
6
| openTelemetryBuilder.WithMetrics(metrics => metrics
.AddAspNetCoreInstrumentation()
.AddPrometheusExporter());
// После вызова app.Build()
app.MapPrometheusScrapingEndpoint(); |
|
Этот код создаст эндпоинт /metrics, который будет возвращать текущие метрики в формате, который Prometheus может прочитать и обработать. Если зайти на этот эндпоинт в браузере, то увидите что-то подобное:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| # HELP http_server_request_duration_seconds Duration of HTTP request.
# TYPE http_server_request_duration_seconds histogram
http_server_request_duration_seconds_bucket{http_route="api/Products",http_response_status_code="200",le="0.005"} 0
http_server_request_duration_seconds_bucket{http_route="api/Products",http_response_status_code="200",le="0.01"} 0
http_server_request_duration_seconds_bucket{http_route="api/Products",http_response_status_code="200",le="0.025"} 0
http_server_request_duration_seconds_bucket{http_route="api/Products",http_response_status_code="200",le="0.05"} 0
http_server_request_duration_seconds_bucket{http_route="api/Products",http_response_status_code="200",le="0.075"} 0
http_server_request_duration_seconds_bucket{http_route="api/Products",http_response_status_code="200",le="0.1"} 0
http_server_request_duration_seconds_bucket{http_route="api/Products",http_response_status_code="200",le="0.25"} 0
http_server_request_duration_seconds_bucket{http_route="api/Products",http_response_status_code="200",le="0.5"} 0
http_server_request_duration_seconds_bucket{http_route="api/Products",http_response_status_code="200",le="0.75"} 0
http_server_request_duration_seconds_bucket{http_route="api/Products",http_response_status_code="200",le="1"} 2
http_server_request_duration_seconds_bucket{http_route="api/Products",http_response_status_code="200",le="2.5"} 3
http_server_request_duration_seconds_bucket{http_route="api/Products",http_response_status_code="200",le="5"} 3
http_server_request_duration_seconds_bucket{http_route="api/Products",http_response_status_code="200",le="7.5"} 3
http_server_request_duration_seconds_bucket{http_route="api/Products",http_response_status_code="200",le="10"} 3
http_server_request_duration_seconds_bucket{http_route="api/Products",http_response_status_code="200",le="+Inf"} 3
http_server_request_duration_seconds_sum{http_route="api/Products",http_response_status_code="200"} 2.95
http_server_request_duration_seconds_count{http_route="api/Products",http_response_status_code="200"} 3 |
|
Когда я впервые увидел этот вывод, он показался мне довольно пугающим. Но на самом деле всё просто: метрика http_server_request_duration_seconds представляет собой гистограмму, разбитую на бакеты (диапазоны значений). Каждая строка с _bucket показывает, сколько запросов попадает в определенный диапазон времени. Например, строка с le="1" и значением 2 означает, что два запроса заняли меньше 1 секунды.
Конфигурация scraping
Теперь нужно настроить Prometheus для сбора этих метрик. Создайте файл prometheus.yml с примерно таким содержимым:
| YAML | 1
2
3
4
5
6
| scrape_interval: 5s
scrape_configs:
job_name: "api"
static_configs:
- targets: ["localhost:5068"] |
|
Здесь scrape_interval указывает, как часто Prometheus будет опрашивать цели, а targets определяет адреса, с которых будут собираться метрики. В реальной среде вы, конечно, замените localhost:5068 на фактические адреса ваших сервисов.
В кластерных средах, например Kubernetes, можно использовать сервис-дискавери для автоматического обнаружения подов:
| YAML | 1
2
3
4
5
6
7
8
9
10
11
12
| scrape_configs:
job_name: "kubernetes-pods"
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: true
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
action: replace
target_label: __metrics_path__
regex: (.+) |
|
Я помню случай, когда один из сервисов в нашем кластере стал недоступен, и мы узнали об этом именно благодаря Prometheus. Он не только мониторил метрики приложения, но и автоматически фиксировал, что цель недоступна для scraping.
Создание composite метрик и recording rules
PromQL (язык запросов Prometheus) позволяет создавать сложные метрики на основе простых. Например, можно вычислить средне время ответа API за последние 5 минут:
| Code | 1
2
| rate(http_server_request_duration_seconds_sum{job="api"}[5m]) /
rate(http_server_request_duration_seconds_count{job="api"}[5m]) |
|
Однако постоянное вычисление таких запросов может быть ресурсоемким. Для этого существуют recording rules – предварительно вычисленные метрики. Определяются они в файле конфигурации:
| YAML | 1
2
3
4
5
6
| rules:
- name: api_metrics
interval: 30s
rules:
- record: api:request_duration_seconds:avg5m
expr: rate(http_server_request_duration_seconds_sum{job="api"}[5m]) / rate(http_server_request_duration_seconds_count{job="api"}[5m]) |
|
Теперь можно использовать метрику api:request_duration_seconds:avg5m вместо длинного выражения, что особенно удобно для дашбордов.
В одном из проектов я создал несколько десятков таких правил для отслеживания различных аспектов работы системы. Интересно, что на начальном этапе мы не знали точно, какие метрики будут наиболее полезны, поэтому собирали почти всё, что можно. Со временем выкристаллизовался набор действительно важных показателей, и мы смогли оптимизировать наш мониторинг.
Мониторинг Custom Business Metrics и KPI через Prometheus
Технические метрики, такие как длительность запросов или количество ошибок, безусловно важны. Но для бизнеса часто нужны другие показатели: количество регистраций, суммы заказов, конверсия и т.д. OpenTelemetry позволяет создавать такие метрики и экспортировать их в Prometheus. Вот пример создания собственной метрики для отслеживания ошибок:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| public class ProductsMetrics : IProductsMetrics
{
private const string NAME = "Get.Products";
private readonly Counter<long> _getProductsErrorCounter;
public ProductsMetrics()
{
var getProdutcsErrorMeter = new Meter(NAME);
_getProductsErrorCounter = getProdutcsErrorMeter.CreateCounter<long>("get.products.error.count");
}
public static string Name => NAME;
public void RegisterError(GetProductError getProductError)
{
var tags = new KeyValuePair<string, object?>(nameof(getProductError), getProductError.ToString());
_getProductsErrorCounter.Add(1, tags);
}
}
public enum GetProductError
{
QueryProducts,
FillPrices,
FillAvailability,
} |
|
Теперь эту метрику можно использовать в коде:
| C# | 1
2
3
4
5
6
7
8
9
| try
{
// Бизнес-логика
}
catch (Exception)
{
_productsMetrics.RegisterError(GetProductError.FillPrices);
throw;
} |
|
И не забудьте зарегистрировать метрику в OpenTelemetry:
| C# | 1
2
3
4
5
6
| var customMetricsNames = new string[] { ProductsMetrics.Name };
openTelemetryBuilder.WithMetrics(metrics => metrics
.AddMeter(customMetricsNames)
.AddAspNetCoreInstrumentation()
.AddPrometheusExporter()); |
|
В Prometheus эта метрика будет доступна как get_products_error_count_total, и можно будет видеть, какие типы ошибок случаются чаще всего.
В моей практике один из самых мощных инструментов Prometheus - это гистограммы и квантили. Они позволяют глубже понять распределение значений метрик, а не просто их среднее или сумму. Это особенно важно для мониторинга времени ответа API, ведь среднее значение может скрывать реальные проблемы.
Продвинутые метрики: гистограммы, квантили и их применение в веб-приложениях
Гистограмма в Prometheus делит диапазон значений на "бакеты" и подсчитывает,сколько измерений попадает в каждый бакет. Для времени ответа типичны бакеты от милисекунд до нескольких секунд. Очень информативными оказываются квантили - показатели, разделяющие выборку на части. Например, 95-й перцентиль (P95) показывает значение, ниже которого находятся 95% всех измерений. Для большинства приложений более показательно следить именно за P95 или P99, а не за средним временем ответа. В Prometheus для вычисления квантилей используется функция histogram_quantile:
| Code | 1
| histogram_quantile(0.95, sum by(le) (rate(http_server_request_duration_seconds_bucket{job="api"}[5m]))) |
|
Этот запрос покажет 95-й перцентиль времени ответа за последние 5 минут.
На одном из наших проектов мы настроили дашборд, который показывал одновременно P50, P90, P95 и P99 для всех критичных эндпоинтов. Это давало нам полную картину производительности. Когда P50 и P90 оставались стабильными, но P99 начинал расти, мы знали, что у небольшого процента пользователей появились проблемы, хотя большинство их не замечало.
| Code | 1
| histogram_quantile(0.50, sum by(le, http_route) (rate(http_server_request_duration_seconds_bucket{job="api"}[5m]))) |
|
Конфигурация retention политик и оптимизация хранения
Важный аспект работы с Prometheus - управление объемом данных. По умолчанию Prometheus хранит данные 15 дней, но этот параметр можно настроить через флаг --storage.tsdb.retention.time:
| Bash | 1
| ./prometheus --storage.tsdb.retention.time=30d |
|
Помимо времени хранения, можно ограничить объем данных:
| Bash | 1
| ./prometheus --storage.tsdb.retention.size=500GB |
|
В одном из проектов мы столкнулись с быстрым ростом объема данных из-за большого количества меток (labels). Оказалось, что мы случайно добавляли уникальный идентификатор пользователя как метку, что приводило к кардинальности в миллионы значений. Решение - тщательно выбирать метки и избегать высокой кардинальности.
Несколько практических советов по оптимизации хранения:
1. Используйте агрегацию через recording rules для часто запрашиваемых метрик.
2. Применяйте фильтрацию на уровне экспортера, чтобы не собирать ненужные метрики.
3. Настройте разные интервалы сбора для разных типов метрик.
В больших распределенных системах имеет смысл использовать федерацию Prometheus, когда несколько инстансов собирают локальные метрики, а центральный сервер агрегирует только нужные. Эта схема хорошо масштабируется и экономит ресурсы:
| YAML | 1
2
3
4
5
6
7
8
9
10
11
12
| scrape_configs:
- job_name: 'federate'
scrape_interval: 15s
honor_labels: true
metrics_path: '/federate'
params:
'match[]':
- '{job="api"}'
static_configs:
- targets:
- 'prometheus-shard1:9090'
- 'prometheus-shard2:9090' |
|
Grafana: визуализация данных мониторинга
Мы научились собирать телеметрию с помощью OpenTelemetry и хранить метрики в Prometheus. Но цифры в базе данных сами по себе не слишком информативны — нужен способ их визуализировать. И тут в игру вступает Grafana — открытая платформа для визуализации и анализа метрик. За последние пять лет я перепробовал много инструментов визуализации, но каждый раз возвращаюсь к Grafana. Почему? Потому что она сочетает гибкость, мощь и при этом относительную простоту использования. А ещё потому, что именно Grafana стала стандартом де-факто в индустрии — согласно исследованию CNCF Survey, более 80% компаний, использующих Kubernetes, применяют Grafana для визуализации метрик.
Создание дашбордов
Дашборд в Grafana — это набор связанных графиков, таблиц и других визуализаций, объединенных общей темой. Я обычно создаю отдельные дашборды для разных аспектов системы: общее состояние, производительность API, бизнес-метрики. После установки Grafana (что делается буквально в пару кликов с офицального сайта) первое, что нужно сделать — настроить источник данных. В нашем случае это Prometheus:
1. Перейдите в раздел Configuration > Data Sources.
2. Нажмите "Add data source" и выберите Prometheus.
3. Укажите URL вашего Prometheus (например, http://localhost:9090).
4. Нажмите "Save & Test".
Теперь можно создать первый дашборд:
1. Нажмите "+" в левом меню и выберите "Dashboard",
2. Добавьте новую панель,
3. В редакторе запросов выберите Prometheus и введите запрос PromQL,
Например, для отображения количества запросов в секунду по разным эндпоинтам можно использовать:
| Code | 1
| sum by (http_route) (rate(http_server_request_duration_seconds_count{job="api"}[5m])) |
|
Результатом будет график, показывающий rps (requests per second) для каждого маршрута вашего API. Помню, как впервые собрал такой дашборд и наконец-то увидел, что на самом деле происходит с нашим API в продакшне. Оказалось, что один эндпоинт, который мы считали малоиспользуемым, генерировал почти 40% всего трафика!
Для метрики времени ответа я рекомендую использовать запрос с перцентилями:
| Code | 1
| histogram_quantile(0.95, sum by(le, http_route) (rate(http_server_request_duration_seconds_bucket{job="api"}[5m]))) |
|
Этот запрос покажет 95-й перцентиль времени ответа для каждого маршрута за 5-минутный интервал.
Grafana предлагает множество типов визуализаций: линейные графики, гистограммы, тепловые карты, таблицы, счётчики и другие. Я часто использую комбинацию из нескольких типов на одном дашборде. Например, для мониторинга API уровня отказов отлично подходит панель "Stat", которая показывает текущее значение большим шрифтом и меняет цвет в зависимости от порогов, которые вы настроите.
Создание интерактивных дашбордов с drill-down возможностями
Одна из сильных сторон Grafana — возможность создавать интерактивные дашборды, где клик по элементу одного графика фильтрует данные на других или открывает новые дашборды с детализацией. Это называется drill-down и оказывается незаменимым при отладке проблем. Как-то раз мы обнаружили аномалию в работе одного из сервисов — резкие скачки времени ответа в определенные моменты. Благодаря drill-down механизму, я создал дашборд, где можно было кликнуть на пик нагрузки и провалиться в детальный анализ именно этого временного интервала, включая логи и трейсы соответствующих запросов. Мы быстро выяснили, что проблема возникала из-за неоптимизированного запроса к базе данных, который выполнялся только при определенных условиях. Для реализации drill-down есть несколько подходов:
1. Использование переменных дашборда — можно настроить переменные, которые изменяются при клике на элемент графика.
2. Настройка ссылок на другие дашборды — при клике пользователь переходит на другой дашборд с передачей контекста.
3. Применение ad-hoc фильтров — позволяет динамически добавлять фильтры к запросам.
Например, для настройки ссылки на другой дашборд:
1. В настройках панели выберите "Panel links".
2. Добавьте новую ссылку и укажите целевой дашборд.
3. В URL передайте переменные: /d/detailed?var-service=${__series.name}&from=${__from}&to=${__to}.
Теперь при клике на серию данных пользователь будет перенаправлен на детальный дашборд с сохранением контекста сервиса и временного интервала.
Настройка темплейтов и переменных в дашбордах
Переменные — ещё одна мощная функция Grafana, которая делает дашборды гибкими и переиспользуемыми. Вместо того чтобы создавать отдельный дашборд для каждого сервиса, можно создать один темплейт с переменными. Для добавления переменной:
1. В настройках дашборда выберите "Variables".
2. Нажмите "New" и задайте имя, например service.
3. Выберите тип "Query" и источник данных Prometheus.
4. В запросе укажите: label_values(http_server_request_duration_seconds_count, job).
Теперь в ваших запросах PromQL можно использовать эту переменную:
| Code | 1
| sum by (http_route) (rate(http_server_request_duration_seconds_count{job="$service"}[5m])) |
|
И пользователь сможет выбирать разные сервисы из выпадающего списка, видя соответствующие метрики без необходимости создавать отдельные дашборды. Переменные могут быть каскадными — когда значение одной переменной зависит от другой. Например:
1. Сначала пользователь выбирает сервис,
2. Затем список доступных эндпоинтов фильтруется в зависимости от выбранного сервиса.
Это реализуется через зависимые запросы:
| Code | 1
| label_values(http_server_request_duration_seconds_count{job="$service"}, http_route) |
|
В одном из проектов я создал систему дашбордов, где первый уровень позволял выбрать команду, второй — сервис этой команды, третий — конкретный инстанс сервиса, и наконец, четвертый — компонент внутри инстанса. Такая детализация оказалась неоценимой при отладке проблем в сложной микросервисной архитектуре.
Еще одна полезная функция — переменные с интервалом времени. Они позволяют динамически изменять период агрегации в запросах:
| Code | 1
| rate(http_server_request_duration_seconds_count{job="$service"}[$interval]) |
|
И пользователь может выбирать, смотреть ли метрики с агрегацией за 1 минуту, 5 минут или более длительный период.
Настройка алертов и уведомлений
Дашборды отлично работают, когда кто-то активно их просматривает. Но нам нужен способ узнавать о проблемах автоматически. Для этого в Grafana есть система алертов. Раньше алерты настраивались на уровне отдельных панелей, но в новых версиях Grafana появился Unified Alerting — централизованная система управления алертами. Она намного мощнее и гибче. Для создания алерта:
1. Перейдите в раздел Alerting.
2. Нажмите "New alert rule".
3. Укажите запрос и условие срабатывания.
Например, чтобы создать алерт на высокое время ответа API:
| Code | 1
| max by(http_route) (histogram_quantile(0.95, sum by(le, http_route) (rate(http_server_request_duration_seconds_bucket{job="api"}[5m])))) |
|
И условие: IS ABOVE 2 (срабатывает, если P95 превышает 2 секунды).
Дальше нужно настроить, что делать при срабатывании алерта. В Grafana есть понятие каналов уведомлений (notification channels). Это могут быть:
1. Email
2. Slack
3. Microsoft Teams
4. Webhook
5. PagerDuty, OpsGenie и другие системы управления инцидентами
В одном из проектов мы настроили каскадную систему эскалации: сначала уведомления шли в Slack канал команды, если проблема не решалась в течение 15 минут — email руководителю команды, а если и это не помогало — вызов через PagerDuty дежурному инженеру.
Grafana Unified Alerting: централизованное управление инцидентами
Grafana Unified Alerting появился в версии 8.0 и стал настоящим прорывом в области управления алертами. В отличие от старой модели, где алерты были привязаны к конкретным панелям, теперь они управляются централизованно. Я перешел на эту систему, как только она появилась, и могу сказать одно – возврата к прежнему подходу уже не будет.
Ключевое преимущество централизованного подхода – структурированная организация алертов. Теперь их можно группировать по папкам, назначать метки и создавать сложные правила маршрутизации. В одном из наших проектов у нас более 200 различных алертов, и без такой структуризации это превратилось бы в полный хаос.
| YAML | 1
2
3
4
5
6
7
8
9
10
11
12
| groups:
name: API Alerts
rules:
- alert: HighLatency
expr: histogram_quantile(0.95, sum by(le, service) (rate(http_server_request_duration_seconds_bucket{job="api"}[5m]))) > 2
for: 5m
labels:
severity: warning
team: backend
annotations:
summary: "High API latency"
description: "P95 latency is above 2 seconds for service {{ $labels.service }}" |
|
Важной концепцией в Unified Alerting стали политики уведомлений (notification policies). Они определяют, что происходит, когда срабатывает алерт. Например, можно настроить разные политики для разных команд или для разных уровней критичности.
Однажды мы столкнулись с "шумным" алертом, который постоянно активировался и деактивировался из-за пограничных значений. Это приводило к сотням уведомлений в Slack и эффекту "усталости от алертов" – люди просто перестали обращать на них внимание. Мы решили проблему, добавив гистерезис через настройку for (алерт активируется только если условие выполняется N минут) и настроив политику тишины (silence) на короткие повторные срабатывания.
Интеграция с внешними системами уведомлений
Grafana поддерживает широкий спектр каналов уведомлений. Я перепробовал большинство из них и обнаружил, что правильная комбинация каналов критически важна для эффективного реагирования на инциденты.
Для интеграции со Slack нужно создать входящий webhook в настройках Slack и добавить его в Grafana:
| JSON | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| {
"text": "{{ if eq .Status \"firing\" }}{{ else }}{{ end }} {{ .Status | title }}: {{ .CommonLabels.alertname }}\n{{ .CommonAnnotations.description }}",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "{{ if eq .Status \"firing\" }}*PROBLEM*{{ else }}*RESOLVED*{{ end }} {{ .CommonAnnotations.summary }}"
}
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": "*Service:*\n{{ .CommonLabels.service }}"
},
{
"type": "mrkdwn",
"text": "*Severity:*\n{{ .CommonLabels.severity }}"
}
]
}
]
} |
|
Для Teams процесс похож, но с использованием формата адаптивных карточек. Это даёт более богатые возможности визуализации уведомлений.
Наиболее полезной для нас оказалась интеграция с PagerDuty. Для критических систем недостаточно пассивных уведомлений – нужна система, которая может "разбудить" инженера среди ночи. PagerDuty предоставляет ротацию дежурств, эскалацию и различные способы оповещения (звонки, SMS, пуш-уведомления). В нашей системе мы внедрили трехуровневую стратегию: некритичные алерты идут только в Slack, предупреждения отправляются по email, а критические инциденты эскалируются в PagerDuty. Это позволяет не беспокоить дежурных инженеров по мелочам, но гарантирует, что серьезные проблемы не останутся без внимания.
Пошаговое развёртывание стека
Довольно теории! Давайте перейдем к конкретной реализации всего стека наблюдаемости. Я поделюсь своим подходом к развертыванию всей инфраструктуры с использованием Docker Compose, который неоднократно применял в реальных проектах. Для начала, создадим структуру директорий:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
| monitoring-stack/
├── docker-compose.yml
├── prometheus/
│ └── prometheus.yml
├── grafana/
│ ├── datasources/
│ │ └── prometheus.yml
│ └── dashboards/
│ ├── dashboard.yml
│ └── api-dashboard.json
└── application/
└── Dockerfile |
|
Теперь приступим к созданию файла docker-compose.yml:
| YAML | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
| version: '3.8'
services:
api:
build:
context: ./application
ports:
- "8080:80"
environment:
- ASPNETCORE_ENVIRONMENT=Production
restart: unless-stopped
prometheus:
image: prom/prometheus:latest
ports:
- "9090:9090"
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--storage.tsdb.retention.time=15d'
- '--web.console.libraries=/usr/share/prometheus/console_libraries'
- '--web.console.templates=/usr/share/prometheus/consoles'
restart: unless-stopped
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
volumes:
- ./grafana/datasources:/etc/grafana/provisioning/datasources
- ./grafana/dashboards:/etc/grafana/provisioning/dashboards
- grafana_data:/var/lib/grafana
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=secret-password
- GF_USERS_ALLOW_SIGN_UP=false
restart: unless-stopped
volumes:
prometheus_data:
grafana_data: |
|
Не забываем о конфигурации Prometheus (prometheus.yml):
| YAML | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
- job_name: 'api'
scrape_interval: 5s
static_configs:
- targets: ['api:80']
metrics_path: '/metrics' |
|
Для автоматической настройки источника данных в Grafana создадим grafana/datasources/prometheus.yml:
| YAML | 1
2
3
4
5
6
7
8
9
10
11
| apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
orgId: 1
url: [url]http://prometheus:9090[/url]
basicAuth: false
isDefault: true
editable: false |
|
И базовую конфигурацию для автозагрузки дашбордов grafana/dashboards/dashboard.yml:
| YAML | 1
2
3
4
5
6
7
8
9
10
11
| apiVersion: 1
providers:
- name: 'Default'
orgId: 1
folder: ''
type: file
disableDeletion: false
editable: true
options:
path: /etc/grafana/provisioning/dashboards |
|
Чтобы всё это работало с нашим .NET приложением, нужно правильно настроить Dockerfile. Вот пример для базового API:
| Windows Batch file | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
WORKDIR /app
EXPOSE 80
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["ShopSports.Api.csproj", "./"]
RUN dotnet restore "ShopSports.Api.csproj"
COPY . .
RUN dotnet build "ShopSports.Api.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "ShopSports.Api.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "ShopSports.Api.dll"] |
|
При настройке стека для production среды, безопасность становится критически важным аспектом. Вот несколько ключевых рекомендаций, которые я всегда применяю:
1. Аутентификация для эндпоинтов метрик: Используйте BasicAuth или токены для защиты /metrics.
2. Сеть: Разделите ваши контейнеры по разным сетям, предоставляя минимально необходимые доступы.
3. Шифрование: Настройте TLS для всех компонентов.
Например, для обеспечения базовой аутентификации в Prometheus:
| YAML | 1
2
| basic_auth_users:
admin: $2y$12$yG8Rb0QgqOgLZvP0hHFHxebVD.IpcXULhg8rPwR0PJRJkCQNjv3RK |
|
А в docker-compose добавьте:
| YAML | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| networks:
frontend:
backend:
services:
api:
networks:
- frontend
- backend
prometheus:
networks:
- backend
# Не открываем порт во внешний мир
ports: []
grafana:
networks:
- backend
- frontend
# Только Grafana доступна извне
ports:
- "3000:3000" |
|
В наших проектах я часто добавляю Nginx как реверс-прокси перед всеми сервисами, что позволяет централизованно управлять TLS и базовой аутентификацией.
Для более сложных сценариев мониторинга, особенно в микросервисной архитектуре, я рекомендую добавить OpenTelemetry Collector. Он действует как буфер между вашими сервисами и системами мониторинга, обеспечивая дополнительный уровень надежности:
| YAML | 1
2
3
4
5
6
7
8
| otel-collector:
image: otel/opentelemetry-collector:latest
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
command:
- "--config=/etc/otel-collector-config.yaml"
networks:
- backend |
|
Такая конфигурация позволяет быстро развернуть полный стек наблюдаемости, который можно адаптировать под конкретные нужды вашей системы.
Интеграция с системами логирования (ELK stack)
Наблюдаемость не ограничивается только метриками и трейсами. Третий столп - это логи, которые дают контекстную информацию о конкретных событиях. Когда метрики показывают "что-то не так", а трейсы показывают "где именно", именно логи отвечают на вопрос "почему это происходит". ELK-стек (Elasticsearch, Logstash, Kibana) стал стандартом для централизованного сбора и анализа логов. Я использую его почти во всех проектах, потому что он дает невероятную гибкость при работе с большими объёмами логов.
Интеграция ASP.NET Core с ELK достаточно проста. Сначала устанавливаем пакет Serilog:
| C# | 1
2
| dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.Elasticsearch |
|
Затем настраиваем его в Program.cs:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
| var builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog((context, configuration) =>
configuration
.ReadFrom.Configuration(context.Configuration)
.Enrich.FromLogContext()
.Enrich.WithProperty("ApplicationName", builder.Environment.ApplicationName)
.WriteTo.Elasticsearch(new ElasticsearchSinkOptions(new Uri("http://elasticsearch:9200"))
{
AutoRegisterTemplate = true,
IndexFormat = $"{builder.Environment.ApplicationName.ToLower()}-{DateTime.UtcNow:yyyy-MM}"
})); |
|
Самое ценное в интеграции ELK с OpenTelemetry - возможность связать логи с трейсами. Для этого используем обогатитель, который добавляет trace ID и span ID в каждую запись лога:
| C# | 1
| .Enrich.With<ActivityEnricher>() |
|
Реализация ActivityEnricher выглядит так:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| public class ActivityEnricher : ILogEventEnricher
{
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
var activity = Activity.Current;
if (activity != null)
{
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty(
"TraceId", activity.TraceId.ToString()));
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty(
"SpanId", activity.SpanId.ToString()));
}
}
} |
|
Теперь в Kibana можно искать логи по конкретному trace ID, увиденному в Grafana или Jaeger. Это нереально упрощает отладку проблем в продакшене.
В своей практике я часто создаю в Kibana дашборды, которые дополняют графики Grafana. Где Grafana показывает агрегированные метрики, Kibana детализирует конкретные события, что дает полную картину происходящего в системе.
Микросервисное приложение с complete observability stack
В завершение хочу поделиться с вами конкретным примером из моей практики. Недавно наша команда разрабатывала микросервисное приложение для обработки заказов в e-commerce системе. Структура включала 4 сервиса: API Gateway, Catalog Service, Order Service и Payment Service. Для полноценной наблюдаемости мы интегрировали все рассмотренные технологии. Вот ключевые компоненты решения:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| // Program.cs для каждого микросервиса
var builder = WebApplication.CreateBuilder(args);
// Настройка OpenTelemetry
var serviceName = builder.Environment.ApplicationName;
builder.Services.AddOpenTelemetry()
.ConfigureResource(r => r.AddService(serviceName))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddEntityFrameworkCoreInstrumentation()
.AddSource(serviceName)
.AddOtlpExporter(opts => opts.Endpoint = new Uri("http://otel-collector:4317")))
.WithMetrics(metrics => metrics
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation()
.AddOtlpExporter(opts => opts.Endpoint = new Uri("http://otel-collector:4317")));
// Настройка Serilog для интеграции с ELK
builder.Host.UseSerilog((ctx, cfg) => cfg
.ReadFrom.Configuration(ctx.Configuration)
.Enrich.WithProperty("Service", serviceName)
.Enrich.With<ActivityEnricher>()
.WriteTo.Elasticsearch(new ElasticsearchSinkOptions(new Uri("http://elasticsearch:9200")) {
IndexFormat = $"logs-{serviceName.ToLower()}-{DateTime.UtcNow:yyyy.MM}"
})); |
|
Для маршрутизации телеметрии мы настроили OpenTelemetry Collector как единую точку сбора всех типов данных:
| YAML | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| # otel-collector-config.yaml
receivers:
otlp:
protocols:
grpc:
http:
processors:
batch:
resourcedetection:
detectors: [env, k8s]
attributes:
actions:
- key: environment
value: production
action: upsert
exporters:
prometheus:
endpoint: 0.0.0.0:8889
elasticsearch:
endpoints: ["http://elasticsearch:9200"]
jaeger:
endpoint: jaeger:14250
tls:
insecure: true
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch, resourcedetection, attributes]
exporters: [jaeger, elasticsearch]
metrics:
receivers: [otlp]
processors: [batch, resourcedetection, attributes]
exporters: [prometheus]
logs:
receivers: [otlp]
processors: [batch, resourcedetection, attributes]
exporters: [elasticsearch] |
|
Такая архитектура дала нам единый стек наблюдаемости, позволяющий отслеживать весь путь запроса через разные сервисы. На практике это сократило время диагностики проблем с нескольких часов до минут. Особенно ценно, что разработчикам больше не нужно было переключаться между разными инструментами - вся информация собрана в одном месте.
ASP.NET MVC 4,ASP.NET MVC 4.5 и ASP.NET MVC 5 большая ли разница между ними? Начал во всю осваивать технологию,теперь хочу с книжкой посидеть и вдумчиво перебрать всё то что... ASP.NET Core: разный формат даты контроллера ASP.NET и AngularJS Собственно, проблему пока еще не разруливал, но уже погуглил. Разный формат даты который использует... ASP.NET MVC или ASP.NET Core Добрый вечер, подскажите что лучшие изучать ASP.NET MVC или ASP.NET Core ? Как я понимаю ASP.NET... Что выбрать ASP.NET или ASP.NET Core ? Добрый день форумчане, хотелось бы услышать ваше мнение, какой из перечисленных фреймворков лучше... ASP.NET Core или ASP.NET MVC Здравствуйте
После изучение основ c# я решил выбрать направление веб разработки. Подскажите какие... Стоит ли учить asp.net, если скоро станет asp.net core? Всем привет
Если я правильно понимаю, лучше учить Core ? ASP.NET или ASP.NET Core Добрый вечер, подскажите новичку в чем разница между asp.net и asp.net core, нужно ли знать оба... Почему скрипт из ASP.NET MVC 5 не работает в ASP.NET Core? В представлении в версии ASP.NET MVC 5 был скрипт:
@model RallyeAnmeldung.Cars
... Несколько приложений ASP.NET Core на одном сервере Linux Несколько приложений ASP.NET Core на одном сервере Linux... Asp.net core rc 2 и Entity Framework core Добрый день, кто-нибудь уже перешел на новую версию фреймверка?
Хотелось бы получить пример.
... ASP.NET Core + EF Core: ошибка при обновлении БД после создания миграции Всем привет!
Начал осваивать ASP.NET Core: создал проект "Веб-приложение" без Identity.
Сразу же... Пагинация. Как установить колличество позиций на странице? Razor Pages с EF Core в ASP.NET Core Изучаю учебник - Razor Pages с Entity Framework Core в ASP.NET Core // docs.microsoft.com/ru-ru/
...
|