Introducción
El post de hoy será un poco largo y tal vez un poco complicado, pero es un tema muy interesante e importante para las aplicaciones que hoy corren en la nube.
Hoy veremos como configurar toda una suite de APM (Application performance monitoring) utilizando software open source y libre de usar con imágenes de Docker y OpenTelemetry.
Crearemos una aplicación Web API para consultar el clima (clima real, de un api pública gratuita llamada Free Open-Source Weather API | Open-Meteo.com) y también crearemos dashboards chulos en Grafana que podrán monitorear cualquier aplicación y cualquier instancia (si está escalada) de la aplicación.
Existe mucha documentación, pero aquí trataré de consolidad todo para una aplicación WebAPI utilizando Grafana, Prometheus, Loki, Zipkin y el OTEL Collector de OpenTelemetry.
Que esto me lleva a lo más importante: OpenTelemetry.
Este ya es un estándar de telemetría y aunque aún es nuevo, muchos servicios lo están adoptando. Ya que hay servicios como Application Insights de Azure o Datadog que ya te permiten recibir telemetría de aplicaciones por medio del protocolo que OpenTelemetry creó, el OTLP.
Nota 💡: Para seguir este post es necesario tener docker instalado y actualizado
Nota 2 💡: El código fuente lo encuentras siempre en mi repositorio de github
APM (Application Performance Monitoring)
Ya hemos hablado en este blog sobre monitoreo de aplicaciones utilizando application insights, lo cual es cierto que ya es una solución completa y es la que yo uso todos los días para monitorear los proyectos en los que participo. Pero no todos usan Azure, así que hoy veremos otras opciones para poder implementar un APM.
Las ventajas de tener un monitoreo eficiente, es poder detectar anomalías y saber responder a ellas. La idea siempre será mejorar el software para que la experiencia de nuestros usuarios sea la mejor.
Tener la mayor visibilidad de cómo opera nuestro sistema nos da una gran ventaja y es algo que todos deberíamos de tener en nuestros proyectos y hoy veremos cómo hacerlo con puro Open Source (todos proyectos de la Cloud Native Foundation).
Lo que debe de tener un APM es lo siguiente:
Logs
Siempre nuestras aplicaciones deben de generar logs, ya sean de error o informativos. Estos siempre nos ayudarán a entender en donde falla nuestra aplicación y será más fácil diagnosticar los problemas.
En desarrollo por lo general todos los logs están activados, pero en producción lo más normal es solo mostrar los Warnings/Criticals:
Ejemplo de logs con Loki y Grafana
Poder consultar los logs sí o sí nos ayudarán a darnos cuenta de muchos problemas que nuestra aplicación pudiera tener y los desarrolladores podrán solucionar los bugs sin que batallen tanto. Porque por ejemplo, los StackTrace
y Exceptions
que ocurren, siempre se loggean y sin problema los podemos consultar para saber dónde falló la aplicación.
Tracing
Un trace es un grupo de operaciones o transacciones con spans que fueron originados por un HTTP Request.
Es muy común que las aplicaciones web se comuniquen con otros servicios o bases de datos, por lo que, un trace completo nos ayudaría a seguir toda esa línea de ejecución sin importar que esta se encuentre distribuida en distintos servicios y fácilmente poder saber dónde hay problemas.
Un ejemplo:
Ejemplo de Tracing de APM de Elastic Observability
Aquí vemos que el request inicial fue GET /dashboard
y cada servicio generó sus propios spans, pero de un mismo trace original. Aquí fácilmente podemos localizar en donde falló un request o que es lo que está lento cuando hay algo mal en algún servicio.
Metrics
Las métricas también son parte muy importante, estas nos ayudarán conocer muchas cosas sobre nuestra aplicación, como: Uso de CPU, memoria disponible, solicitudes fallidas, tiempo de respuesta por solicitud, etc.
Ejemplo de Metrics de APM de Elastic Observability
Infraestructura
Las tecnologías que utilizaremos son las siguientes:
-
Prometheus
- Este será nuestra base de datos y el que recolectará toda la información para las métricas
-
Loki
- Es una base de datos dedicado a los logs, este recibirá y guardará los logs de una manera que se puedan obtener después
- Otro servicio de logs muy bueno es Seq de datalust.co, pero por ahora no tiene soporte para OpenTelemetry
- Es una base de datos dedicado a los logs, este recibirá y guardará los logs de una manera que se puedan obtener después
-
Zipkin
- Nos permitirá guardar todos los
traces
yspans
que la aplicación (o aplicaciones) generen- Otro servicio open source para tracing es Jaeger que sí soporta OpenTelemetry
- Nos permitirá guardar todos los
-
Grafana
- Grafana nos ayudará a mostrar toda la información recolectada por los anteriores, aquí podemos crear dashboards y explorar toda la información (aunque zipkin tiene su propia UI y Prometheus también).
- Los dashboards van a incluir gráficas similares a las capturas mostradas anteriormente y es totalmente libre de configurar a las necesidades de cada uno.
-
OpenTelemetry Collector:
- Este es opcional, pero es recomendado. El OTEL Collector es un intermediario entre mi aplicación y todos los servicios anteriores que hemos mencionado, nuestra aplicación no sabrá a donde va toda la información, solo le importa que se va a comunicar con un protocolo especial de OpenTelemetry y el OTEL Collector se encargará de mandarlo a cada servicio según se requiera.
- Esto es muy útil cuando tenemos muchos servicios, solo configuramos el OTEL (autenticación, certificados, cosas tediosas) pero las aplicaciones de igual forma solo mandan su información al collector sin preocuparse de los detalles.
- También es útil cuando quisiéramos cambiar de proveedor de APM, nuestra aplicación no se enterará de nada, solo actualizamos el Collector y ya.
- Muchos APMs ya soportan OpenTelemetry por que ya es un estándar, por lo que es una muy buena ventaja sí utilizar este collector.
Lo que haremos a continuación es lo siguiente:
- Tendremos una Web API que se comunicará con una API Pública del Clima.
- La Web API mandará los Logs, Traces y Metrics por medio del protocolo OTLP de OpenTelemetry y se los mandará al OTEL Collector.
- El OTEL Collector los procesará y los mandará a sus exporters (Loki, Prometheus y Zipkin).
- Configuraremos Grafana para visualizar dashboards con las métricas y logs
- Usaremos Zipkin para ver todos los traces
Comencemos.
Docker Compose
Lo primero que necesitamos es configurar la infraestructura, no veremos tanto los detalles de la aplicación Web API pero siempre puedes ver el código fuente para basarte en ello.
version: "3"
services:
loki:
image: grafana/loki:latest
ports:
- "3100:3100"
command: -config.file=/etc/loki/local-config.yaml
networks:
- opentelemetry
prometheus:
image: prom/prometheus:latest
ports:
- "9090:9090"
command: --config.file=/etc/prometheus/prometheus.yaml --storage.tsdb.path=/prometheus
volumes:
- ./prometheus.yaml:/etc/prometheus/prometheus.yaml
- ./tmp/prometheus:/prometheus
networks:
- opentelemetry
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
environment:
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
- GF_AUTH_DISABLE_LOGIN_FORM=true
volumes:
- ./tmp/grafana/:/var/lib/grafana/
- ./grafana-datasource.yaml:/etc/grafana/provisioning/datasources/ds.yaml
depends_on:
- prometheus
- loki
networks:
- opentelemetry
otel-collector:
image: otel/opentelemetry-collector-contrib:latest
ports:
- "4317:4317"
- "4318:4318"
command: --config=/etc/otel-collector-config.yaml
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
networks:
- opentelemetry
zipkin:
image: openzipkin/zipkin:latest
ports:
- "9411:9411"
networks:
- opentelemetry
networks:
opentelemetry
name: opentelemetry-network
¿Qué significa todo eso?
Si nunca has usado docker y archivos compose tal vez esto te resultará complicado de procesar, pero la esencia de todo esto es configurar cada servicio según sus imagenes docker que ya existen publicadas por sus desarrolladores.
Estamos indicando que configuración usar y que puertos usar (por lo general, los defaults) y todos se encuentran en una misma red (por lo cual, sus nombres de host serán los que están indicados aquí, ejemplo: http://loki:3100.
grafana-datasource.yaml
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
orgId: 1
url: http://prometheus:9090
basicAuth: false
isDefault: true
version: 1
editable: false
- name: Loki
type: loki
access: proxy
orgId: 1
url: http://loki:3100
basicAuth: false
isDefault: false
version: 1
editable: false
- name: Zipkin
type: zipkin
access: proxy
orgId: 1
url: http://zipkin:9411
basicAuth: false
isDefault: false
version: 1
editable: false
Aquí estamos diciendo que Grafana tendrá tres Data Sources (Prometheus, Loki y Zipkin) indicando también el host de donde se obtendrá su información.
Aquí todo es super básico, nada de autenticación y cosas por el estilo, pero todo eso puede configurar si profundizamos en la documentación de cada proveedor.
loki-config.yaml
auth_enabled: false
server:
http_listen_address: 0.0.0.0
grpc_listen_address: 0.0.0.0
http_listen_port: 3100
schema_config:
configs:
- from: 2020-04-15
store: boltdb
object_store: filesystem
schema: v11
index:
prefix: index_
period: 168h
Aquí nuevamente nos vamos por la configuración más básica, Loki puede guardar los logs en muchos lugares, como Storage Blobs de Azure o en Amazon S3.
En este caso, solo los guardamos como archivos por simplicidad.
prometheus.yaml
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'otelcollector'
static_configs:
- targets: [ 'otel-collector:8889' ]
La forma en la que funciona prometheus es haciendo polling de las métricas, por lo que aquí le estamos diciendo de donde conseguirá las métricas y cada cuando debe preguntar por ellas.
Aquí preguntará las métricas al OTEL Collector y lo hará cada 15 segundos. Si no usáramos el collector, aquí irían nuestros proyectos que queremos monitorear, pero aquí la ventaja es que no importa si tenemos 1 o 100 proyectos, siempre se le preguntarán al collector y el nos dará el de todos.
otel-collector-config.yaml
receivers:
otlp:
protocols:
http:
grpc:
processors:
attributes:
actions:
- action: insert
key: loki.attribute.labels
value: event.domain
resource:
attributes:
- action: insert
key: loki.resource.labels
value: service.name
batch:
timeout: 1s
send_batch_size: 1024
exporters:
prometheus:
endpoint: "0.0.0.0:8889"
send_timestamps: true
resource_to_telemetry_conversion:
enabled: true
loki:
endpoint: http://loki:3100/loki/api/v1/push
tls:
insecure: true
zipkin:
endpoint: http://zipkin:9411/api/v2/spans
service:
pipelines:
metrics:
receivers: [otlp]
processors: [batch]
exporters: [prometheus]
traces:
receivers: [otlp]
processors: [batch]
exporters: [zipkin]
logs:
receivers: [otlp]
processors: [batch, resource]
exporters: [loki]
Por último, queda la configuración del collector, aquí se definen 4 cosas:
- Los Receivers (los que mandan la información)
- Nuestra aplicación mandará los datos por medio de OTLP
- Los Processors (de qué forma se procesará la información)
- Se procesará la info por lotes de 1024 registros (configuración default que Copilot me sugirió 🤭)
- Los Exporters (A donde irá la información procesada)
- Se expondrán endpoints para el scrapping de prometheus
- Se mandarán los logs a Loki
- Se mandarán los Traces a zipkin
- Los pipelines (juntar los 3 anteriores)
Técnicamente a este punto ya podemos correr el docker compose y ver si tenemos algún problema con la configuración:
docker compose up
Y en docker desktop deberíamos de ver todos los servicios corriendo:
![[Pasted image 20230603081926.png]]
Nota 💡: Siempre puedes descargar el proyecto completo aquí para no batallar tanto si lo quisiste escribir a mano
Aplicación ASP.NET
Ahora haremos la aplicación API que queremos monitorear, es sencilla ya que solo haremos un endpoint para consultar el clima según coordenadas geográficas utilizando Open Meteo, una API gratuita del clima.
NuGets a usar
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.5" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.5.0-rc.1" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.5.0-rc.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.0.0-rc9.14" />
<PackageReference Include="OpenTelemetry.Instrumentation.EventCounters" Version="1.0.0-alpha.2" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.1.0-rc.2" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.0.0-rc9.14" />
<PackageReference Include="OpenTelemetry.Instrumentation.Process" Version="0.5.0-beta.2" />
<PackageReference Include="Serilog.AspNetCore" Version="7.0.0" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
<PackageReference Include="Serilog.Enrichers.Environment" Version="2.2.0" />
<PackageReference Include="Serilog.Sinks.OpenTelemetry" Version="1.0.0-dev-00208" />
Lo importante:
- Toda la instrumentación de OpenTelemetry la vemos en estos paquetes
- OpenTelemetry SDK no soporta por ahora los logs, pero lo hacemos por medio de Serilog y su Sink de OpenTelemetry, es por eso que optamos por Serilog para mandar los logs al Collector
Instrumentor
Esta parte es opcional, pero es muy útil si queremos agregar métricas personalizadas, y puede ser de lo que sea, de lo que necesites. Lo hacemos creando un Instrumentor
, que posteriormente registraremos y automáticamente se mandará al collector para ser consultado y hasta poder hacer gráficas.
using System.Diagnostics;
using System.Diagnostics.Metrics;
namespace WebApi;
public sealed class Instrumentor : IDisposable
{
public const string ServiceName = "WebApi";
public ActivitySource Tracer { get; }
public Meter Recorder { get; }
public Counter<long> IncomingRequestCounter { get; }
public Instrumentor()
{
var version = typeof(Instrumentor).Assembly.GetName().Version?.ToString();
Tracer = new ActivitySource(ServiceName, version);
Recorder = new Meter(ServiceName, version);
IncomingRequestCounter = Recorder.CreateCounter<long>("app.incoming.requests",
description: "The number of incoming requests to the backend API");
}
public void Dispose()
{
Tracer.Dispose();
Recorder.Dispose();
}
}
Al counter (o métrica) le estamos dando el nombre de app.incoming.requests y en prometheus estará disponible bajo el nombre app_incoming_requests_total
.
Configuración de Logs
Como lo mencioné anteriormente, aquí haremos uso de Serilog y el Sink que tiene para OpenTelemetry para mandar todos los logs recoletados por ILogger
, aquí podemos configurar el nivel del log que queremos que se mande o si queremos que se mande solo en producción y cosas así.
const string outputTemplate =
"[{Level:w}]: {Timestamp:dd-MM-yyyy:HH:mm:ss} {MachineName} {EnvironmentName} {SourceContext} {Message}{NewLine}{Exception}";
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.Enrich.FromLogContext()
.Enrich.WithThreadId()
.Enrich.WithEnvironmentName()
.Enrich.WithMachineName()
.WriteTo.Console(outputTemplate: outputTemplate)
.WriteTo.OpenTelemetry(opts =>
{
opts.ResourceAttributes = new Dictionary<string, object>
{
["app"] = "webapi",
["runtime"] = "dotnet",
["service.name"] = "WebApi"
};
})
.CreateLogger();
builder.Host.UseSerilog();
No hay que olvidar poner app.UseSerilogRequestLogging();
en los middlewares porque al usar Serilog se elimina cualquier Log que .NET te configura por default, queremos loggear todo lo que sucede en la aplicación, al menos en modo desarrollo.
Configuración de Tracing y Métricas
builder.Services.AddSingleton<Instrumentor>();
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing
.AddSource(Instrumentor.ServiceName)
.ConfigureResource(resource => resource
.AddService(Instrumentor.ServiceName))
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddOtlpExporter())
.WithMetrics(metrics => metrics
.AddMeter(Instrumentor.ServiceName)
.ConfigureResource(resource => resource
.AddService(Instrumentor.ServiceName))
.AddRuntimeInstrumentation()
.AddAspNetCoreInstrumentation()
.AddProcessInstrumentation()
.AddHttpClientInstrumentation()
.AddEventCountersInstrumentation(c =>
{
// https://learn.microsoft.com/en-us/dotnet/core/diagnostics/available-counters
c.AddEventSources(
"Microsoft.AspNetCore.Hosting",
"Microsoft-AspNetCore-Server-Kestrel",
"System.Net.Http",
"System.Net.Sockets");
})
.AddOtlpExporter());
Aquí ya terminamos de configurar OpenTelemetry indicando que queremos incluir en el tracing y metrics. Todo esto está disponible gracias a todos los paquetes de instrumentación que agregamos y todo se hace ya en automático.
Me puse a jugar con los EventSources y ver que incluye cada uno, pero igual los counters disponibles se encuentran en el link que puse ahí como comentario.
Aunque creo, que estos que incluimos aquí, son más que suficientes.
/api/weather-forecast endpoint
Aquí por fin el endpoint que vamos a monitorear, en esta ocasión consumiremos una API pública del clima para consultar su información según las coordenadas que queramos
app.MapGet("/api/weather-forecast", async ([AsParameters] WeatherForeCastParams request) =>
{
request.Instrumentor.IncomingRequestCounter.Add(1,
new KeyValuePair<string, object?>("operation", "GetWeatherForecast"),
new KeyValuePair<string, object?>("minimal-api-route", "/api/weather-forecast"));
var url = $"https://api.open-meteo.com/v1/forecast?latitude={request.Latitude}&longitude={request.Longitude}&hourly=temperature_2m";
var response = await request.HttpClient.GetAsync(url);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<WeatherForecast>();
});
Struct con los parámetros del endpoint:
public struct WeatherForeCastParams
{
public double Latitude { get; set; }
public double Longitude { get; set; }
public HttpClient HttpClient { get; set; }
public Instrumentor Instrumentor { get; set; }
}
Y el DTO que maneja la API del clima:
namespace WebApi.Models;
public class WeatherForecast
{
public double latitude { get; set; }
public double longitude { get; set; }
public double generationtime_ms { get; set; }
public int utc_offset_seconds { get; set; }
public string timezone { get; set; }
public string timezone_abbreviation { get; set; }
public double elevation { get; set; }
public HourlyUnits hourly_units { get; set; }
public Hourly hourly { get; set; }
}
public class Hourly
{
public List<string> time { get; set; }
public List<double> temperature_2m { get; set; }
}
public class HourlyUnits
{
public string time { get; set; }
public string temperature_2m { get; set; }
}
Hasta este punto ya podemos correr la aplicación y deberíamos de poder ya generar métricas (por que en teoria, el collector y toda la infraestructura ya está corriendo).
Dashboards en Grafana
Preparé dos dashboards basados obviamente en otros que son públicos y libres de usar, pero estos no usaban el collector, por lo que el nombre de las métricas cambia un poco.
Antes de empezar con los dashboards veamos los datos que ya se empezaron a generar
Prometheus
Entrando a http://localhost:9090/ podemos explorar todas las métricas que se están exportando:
Zipkin
Entrando a http://localhost:9411/ podemos ya ver los Traces generados
Y el detalle de cada uno
Aquí si ocurriera algún error, lo veríamos sin problema:
Loki
Loki no tiene interfaz tal cual, pero podemos explorar los logs si ya entramos a Grafana, porque este ya tiene todos los datasources configurados.
Entramos a http://localhost:3000/explore y en data source elegimos Loki
Aquí podemos hacer muchas cosas, como transformar el JSON a un texto más legible
Importar Dashboards
Los dashboards se encuentran dentro de la carpeta resources
del repositorio (igual aquí está el 01 y 02).
Para importarlos solo hay que irnos al menú de Grafana y en dashboards podremos ver la opción Import y pegamos el contenido del JSON
Si todo sale bien, podremos ver los dashboards con toda la información de WebApi que ya ha estado generanto (por que la dejamos prendida y yo hice varios requests)
Conclusión
Esto puede resultar abrumador, pero como siempre cuando entras en una tecnología nueva tienes que ser paciente, leer mucho e investigar siempre que tengas dudas.
Aquí podemos ver un walkthrough de como configurar todo de inicio a fin, tener códigos de ejemplo siempre me ayudan a saber como configurar todo.
Por otra parte, aprender a usar el collector resultará útil para todas nuestras aplicaciones ya que omitiremos detalles de los servicios que usaremos para telemetría, estos servicios deberán de contar con autenticación y configuración adicional que con el tiempo puede variar y tener que actualizarlo solo en el collector y no en todos nuestros servicios es la super ventaja que ofrece el OTEL Collector.
Espero te haya servido este post, ya que yo aprendí mucho sobre estas tecnologías y espero que tú también.
Referencias
- Guide to OpenTelemetry (logz.io)
- Instrumenting C# .Net Apps With OpenTelemetry | Logz.io
- bradygaster/dotnet-cloud-native-build-2023 (github.com)
- cecilphillip/grafana-otel-dotnet: Sample setup showing ASP.NET Core observability with Prometheus, Loki, Grafana, Opentelemetry Collector (github.com)
- open-telemetry/opentelemetry-dotnet: The OpenTelemetry .NET Client (github.com)
- .NET | OpenTelemetry