C# e Net 6 in Kubernetes con Prometheus e Grafana

di Andrea Zani, in .NET,

Premetto che ho scoperto da poco questa cosa. Naturalmente sto parlando di Kubernetes che qui nel mio blog ha quasi il 100% dei post in quest'ultimo periodo. In questo caso farò la combinata Kubernetes con Net 6. La scoperta recente riguarda la possiblità di collegare direttamente i miei Pod, in cui girano container scritti in C#, con Prometheus che permette il monitoraggio delle risorse di un cluster, e di conseguenza con Grafana che permette di creare report con grafici dettagliati delle informazioni raccolte.

Inizio lanciando l'esecuzione di questi due software all'interno del mio cluster. Utilizzerò il servizio di Kubernetes all'interno dell'installazione standard di Docker (anche se ho fatto prove anche con Minikube e su servizi online reali). Il metodo più veloce per l'installazione di questi due pacchetti è grazie a Helm. Helm è un gestore di pacchetti per Kubernetes che permette l'installazione di applicazioni anche molto complesse con un solo comando.

In questo post scavalco completamente l'installazione di Helm visto che in rete si trovano moltissimi esempi in merito e risulta semplice su tutte le piattaforme (su Windows 10 è sufficiente scaricare lo zip da questa pagina e estrarre l'eseguibile).

Storicamente per l'installazione di Prometheus si usava il repository stable, ma da più di un anno lui e molti altri risultano Deprecated. Il motivo principale si trova qui. Da questo motore di ricerca per Helm si possono cercare alternative:

https://artifacthub.io/

E nel caso si fa una ricerca per Prometheus si trova l'ufficiale:

https://artifacthub.io/packages/helm/prometheus-community/prometheus

Sinceramente in passato l'ho usato senza problemi perché molto comodo: con un solo comando installa Prometheus, Grafana e tutto ciò che serve - Pod, service... - ma sono mesi che l'utilizzo di questo package mi dà problemi che necessitano di workaround per fare funzionare il tutto. Ora preferisco utilizzare altri pacchetti per Prometheus, come quello di Bitnami:

https://bitnami.com/stack/prometheus-operator/helm

Per Grafana:

https://bitnami.com/stack/grafana/helm

E' ora di fare sul serio installando tutto. Avendo Helm installato e un cluster di Kubernetes, inizio creando il Namespace dove inserirò tutto ciò che riguarda questi due software:

kubectl create namespace monitors

Quindi aggiungo il repository a Helm:

helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update

... e installo Prometheus:

>helm install my-prometheus --namespace monitors bitnami/kube-prometheus
W0109 10:04:14.516714   16608 warnings.go:70] policy/v1beta1 PodSecurityPolicy is deprecated in v1.21+, unavailable in v1.25+
W0109 10:04:14.521874   16608 warnings.go:70] policy/v1beta1 PodSecurityPolicy is deprecated in v1.21+, unavailable in v1.25+
W0109 10:04:14.526425   16608 warnings.go:70] policy/v1beta1 PodSecurityPolicy is deprecated in v1.21+, unavailable in v1.25+
W0109 10:04:14.530130   16608 warnings.go:70] policy/v1beta1 PodSecurityPolicy is deprecated in v1.21+, unavailable in v1.25+
W0109 10:04:14.532808   16608 warnings.go:70] policy/v1beta1 PodSecurityPolicy is deprecated in v1.21+, unavailable in v1.25+
W0109 10:04:15.369263   16608 warnings.go:70] policy/v1beta1 PodSecurityPolicy is deprecated in v1.21+, unavailable in v1.25+
W0109 10:04:15.376872   16608 warnings.go:70] policy/v1beta1 PodSecurityPolicy is deprecated in v1.21+, unavailable in v1.25+
W0109 10:04:15.376872   16608 warnings.go:70] policy/v1beta1 PodSecurityPolicy is deprecated in v1.21+, unavailable in v1.25+
W0109 10:04:15.378700   16608 warnings.go:70] policy/v1beta1 PodSecurityPolicy is deprecated in v1.21+, unavailable in v1.25+
W0109 10:04:15.380284   16608 warnings.go:70] policy/v1beta1 PodSecurityPolicy is deprecated in v1.21+, unavailable in v1.25+
NAME: my-prometheus
LAST DEPLOYED: Sun Jan  9 10:04:13 2022
NAMESPACE: monitors
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
CHART NAME: kube-prometheus
CHART VERSION: 6.6.0
APP VERSION: 0.53.1

** Please be patient while the chart is being deployed **

Watch the Prometheus Operator Deployment status using the command:

    kubectl get deploy -w --namespace monitors -l app.kubernetes.io/name=kube-prometheus-operator,app.kubernetes.io/instance=my-prometheus

Watch the Prometheus StatefulSet status using the command:

    kubectl get sts -w --namespace monitors -l app.kubernetes.io/name=kube-prometheus-prometheus,app.kubernetes.io/instance=my-prometheus

Prometheus can be accessed via port "9090" on the following DNS name from within your cluster:

    my-prometheus-kube-prometh-prometheus.monitors.svc.cluster.local

To access Prometheus from outside the cluster execute the following commands:

    echo "Prometheus URL: http://127.0.0.1:9090/"
    kubectl port-forward --namespace monitors svc/my-prometheus-kube-prometh-prometheus 9090:9090

Watch the Alertmanager StatefulSet status using the command:

    kubectl get sts -w --namespace monitors -l app.kubernetes.io/name=kube-prometheus-alertmanager,app.kubernetes.io/instance=my-prometheus

Alertmanager can be accessed via port "9093" on the following DNS name from within your cluster:

    my-prometheus-kube-prometh-alertmanager.monitors.svc.cluster.local

To access Alertmanager from outside the cluster execute the following commands:

    echo "Alertmanager URL: http://127.0.0.1:9093/"
    kubectl port-forward --namespace monitors svc/my-prometheus-kube-prometh-alertmanager 9093:9093

Vengono visualizzate informazioni utili che potrei usare più tardi. Ora tocca a Grafana:

>helm install my-grafana --namespace monitors --set admin.password=password bitnami/grafana
NAME: my-grafana
LAST DEPLOYED: Sun Jan  9 10:07:17 2022
NAMESPACE: monitors
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
CHART NAME: grafana
CHART VERSION: 7.6.0
APP VERSION: 8.3.3

** Please be patient while the chart is being deployed **

1. Get the application URL by running these commands:
    echo "Browse to http://127.0.0.1:8080"
    kubectl port-forward svc/my-grafana 8080:3000 &

2. Get the admin credentials:

    echo "User: admin"
    echo "Password: $(kubectl get secret my-grafana-admin --namespace monitors -o jsonpath="{.data.GF_SECURITY_ADMIN_PASSWORD}" | base64 --decode)"

Nei due link sopra si possono trovare anche tutti i parametri che si possono aggiungere ai due pacchetti durante l'installazione. Nel mio caso mi sono limitato a specificare in quale Namespace installare il tutto e la password per l'utente principale in Grafana (altrimenti sarà creata una casuale).

Innanzitutto verifico che Prometheus sia avviato correttamente. Cerco il Service a lui collegato:

>kubectl -n monitors get svc
NAME                                      TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)                      AGE
alertmanager-operated                     ClusterIP   None            <none>        9093/TCP,9094/TCP,9094/UDP   4m20s
my-grafana                                ClusterIP   10.111.50.218   <none>        3000/TCP                     80s
my-prometheus-kube-prometh-alertmanager   ClusterIP   10.103.68.153   <none>        9093/TCP                     4m23s
my-prometheus-kube-prometh-operator       ClusterIP   10.98.63.55     <none>        8080/TCP                     4m23s
my-prometheus-kube-prometh-prometheus     ClusterIP   10.99.91.123    <none>        9090/TCP                     4m23s
my-prometheus-kube-state-metrics          ClusterIP   10.107.81.39    <none>        8080/TCP                     4m23s
my-prometheus-node-exporter               ClusterIP   10.106.217.82   <none>        9100/TCP                     4m23s
prometheus-operated                       ClusterIP   None            <none>        9090/TCP                     4m20s

Eccolo è my-prometheus-kube-prometh-prometheus. Ma è di tipo cluster, quindi accessibile solo internamente. Per poterlo richiamare anche dall'esterno (da browser) uso il comando port-forward come è stato mostrato anche nella risposta di Helm:

kubectl port-forward --namespace monitors svc/my-release1-kube-prometheu-prometheus 9090:9090

Ora posso utilizzare il browser e richiamare la pagina http://localhost:9090 (anche se il cluster di Kubernetes fosse avviato in un servizio in cloud):

Funziona. Per ora non mi interessa altro. E' il momento di Grafana:

kubectl port-forward svc/my-grafana 8080:3000

Ora aprendo il browser alla pagina http://localhost:8080 saranno richieste le credenziali: admin e password:

Il passaggio successivo è collegare Grafana con Prometheus. Vado in Configuration nella barra a sinistra, Data sources. Quindi click su Add Data Source. Ora aggiungo come fonte Prometheus e nella pagina successiva, nella Textbox dell'url, inserisco il nome del suo service visto prima (le informazioni riportate da Helm non sono complete):

http://my-prometheus-kube-prometh-prometheus.monitors.svc.cluster.local:9090

Quindi, sul fondo della pagina, clicco su Save and Test:

Se è tutto verde e funziona, posso inserire una Dashboard. L'installazione dai pacchetti di Bitnami non installa nessuna Dashboard di default, cosa che invece fa la versione Community. Non è un problema. Nella barra a destra click su + (create), quindi Import. Nella Textbox "Import via grafana.com" inserisco il valore numerico 10000 e come Data source seleziono Prometheus dal dropdownlist. Ed ecco una Dashboard con le informazioni di base del cluster:

Se si vogliono avere dati sui Pod, consiglio questa Dashboard: 15398:

Oppure per la visione dei Pod: 15336, 8860 e 10518. A parte i gusti personali, è presente un ricco database con molte dashboard già pronte: https://grafana.com/grafana/dashboards/

Grafana, come già detto, lavora in copia con Prometheus visto che richiede i dati a quest'ultimo per popolare i suoi grafici. Personalmente preferisco partire proprio da lui per eseguire i miei test prima di portarli in Grafana. Nella home page di Prometheus è possibile inserire le query nel suo formato interno e vedere subito il risultato in formato testuale o chart. L'intellisense della Textbox permette di vedere subito i datapoint disponibili, anche quelli custom che definirò dopo:

I datapoint principali e più utilizzati in Prometheus sono due:

  • counter: è un valore numerico a incremento, come potrebbe essere il numero di richieste ad un container.
  • gauge: è un valore numerico assoluto, come il consumo di memoria di un container.

In Prometheus il più semplice è il gauge, per esempio, per vedere il consumo di memoria dei container che girano nel cluster:

In un cluster con pochi Pod i risultati sono gestibili, ma è sempre meglio poterli filtrare. Avendo come output una riga come la seguente:

container_memory_usage_bytes{container="run", endpoint="https-metrics", ... namespace="testmetrics", node="docker-desktop", ...}

Posso filtrare per il Namespace interessato:

E qui il risultato filtrato con solo tre linee che sono i container che girano in quel container. Se volessi aggregare questi dati:

sum(container_memory_usage_bytes{namespace="testmetrics"})

Se volessi filtrare anche per Node:

sum(container_memory_usage_bytes{namespace="testmetrics",node="docker-desktop"})

Per il tipo counter la visualizzazione in chart sarebbe poco interessante perché, essendo un valore a solo incremento, si avrebbe una linea che sale in modo più o meno accentuato. Prometheus permette di aggregare questi dati in periodi di tempo, rendendo la visualizzazione di questi dati molto più utile. Prendo il counter container_cpu_user_seconds_total che mostra l'utilizzo di cpu per ogni container per secondo. Ora aggregando i dati a 5 minuti:

rate(container_cpu_user_seconds_total [5m])

Nella documentazione di Prometheus è possibile trovare funzioni e altre info utili per la creazione di query - io qui ho solo mostrato le basi e non mi posso definire di certo un maestro nel suo uso, quindi mi fermo qui. E' giunto il momento di Net 6. Anche un Pod in cui gira un mio codice in Net Core visualizzerebbe le informazioni come sopra - un Pod è sempre un Pod. Ma se volessi inserire informazioni mirate ci si scontra con le limitazioni di informazioni disponibili - se volessi sapere quante richieste http ha avuto una determinata pagina, come potrei avere questa informazione?

La principale domanda è un'altra: come fa Prometheus a prendere queste informazioni dal cluster di Kubernetes? Dall'interfaccia di Prometheus visualizzo la pagina Service Discovery:

http://localhost:9090/service-discovery

Questi service sono letti dalla lista dei ServiceMonitor che è una CustomResource creata da Prometheus:

>kubectl get servicemonitor -A
NAMESPACE   NAME                                                 AGE
monitors    my-prometheus-kube-prometh-alertmanager              61m
monitors    my-prometheus-kube-prometh-apiserver                 61m
monitors    my-prometheus-kube-prometh-coredns                   61m
monitors    my-prometheus-kube-prometh-kube-controller-manager   61m
monitors    my-prometheus-kube-prometh-kube-proxy                61m
monitors    my-prometheus-kube-prometh-kube-scheduler            61m
monitors    my-prometheus-kube-prometh-kubelet                   61m
monitors    my-prometheus-kube-prometh-operator                  61m
monitors    my-prometheus-kube-prometh-prometheus                61m
monitors    my-prometheus-kube-state-metrics                     61m
monitors    my-prometheus-node-exporter                          61m

Ergo, per permettere la lettura da Prometheus dei miei dati custom devo creare un ServiceMonitor. Ma è solo il primo passo, perché queste Resource servono solo a Prometheus a indicare dove prendere le informazioni che io devo esporre in qualche modo. Ultimo tassello è creare un Exporter che, nel mio caso, è una pagina nella mia webapplication che mette a disposizione queste informazioni in un formato ben preciso. La community ha creato numerosi exporter per i software più utilizzati, qui la lista:

https://prometheus.io/docs/instrumenting/exporters/

Per esempio, se nel mio cluster avessi il database MySql potrei collegare l'exporter:

https://github.com/prometheus/mysqld_exporter

E avrei dettagli completi del Pod e del database (sono presenti anche le immagini Docker già pronte per essere usate in Kubernetes).

Per la mia semplice web application in Net 6 l'exporter lo creerò direttamente nell'applicazione. In nuget sono presenti alcuni Package che semplificano il tutto, nel mio caso ho usato questo:

https://www.nuget.org/packages/prometheus-net.AspNetCore/

Ed ecco la mia minimal app in C# e Net 6, il file program.cs:

using System.Net;
using Prometheus;
using WebApiMetrics;

string pathMetrics = Environment.GetEnvironmentVariable("METRICS_PATH") ?? "/metrics";
using MetricsApp metrics = new(pathMetrics);

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseRouting();
app.UseHttpMetrics();
app.MapMetrics(pattern: pathMetrics);

app.Use(metrics.Use);
app.MapGet("/", () => "Hello, World! v1.0");
app.MapGet("/host", () => $"Host: {Dns.GetHostName()}");
app.MapGet("/health", () => "health check");
app.Run();

Per la creazione automatica dell'exporter bastano queste due righe:

app.UseHttpMetrics();
app.MapMetrics(pattern: pathMetrics);

In pattern ho inserito come Path /metrics. Questo in una pagina pubblica potrebbe essere facilmente scoperto da chicchessia, ma in questo caso è compito nostro bloccare tali richieste oppure crearne una difficile da scoprire a caso (sfido a trovare a caso il Path /metrics_fjoewfu23jfewojeczcewmjhf_fwef8fetreoiuoiw).

Nella classe MetricsApp ho inserito la configurazione custom per aggiungere mie informazioni:

using System.Diagnostics;
using Prometheus;

namespace WebApiMetrics;

public sealed class MetricsApp : IDisposable
{
    private readonly Counter _counter1, _counter2, _counter3, _counter4;
    private readonly string _pathMetrics;
    private readonly Process _proc;

    public MetricsApp(string pathMetrics)
    {
        _pathMetrics = pathMetrics ?? throw new ArgumentException(nameof(pathMetrics));
        _proc = Process.GetCurrentProcess();
        _counter1 = Metrics.CreateCounter(
            "webapimethods_path_counter",
            "requests to webapimethods",
            new CounterConfiguration
            {
                LabelNames = new[] { "method", "endpoint" }
            });

        _counter2 = Metrics.CreateCounter(
            "private_memory_size_64",
            "PrivateMemorySize64");

        _counter3 = Metrics.CreateCounter(
            "gc_get_total_memory",
            "GC.GetTotalMemory");

        _counter4 = Metrics.CreateCounter(
            "system_environment_workingset",
            "System.Environment.WorkingSet");
    }

    public void Dispose()
    {
        _proc.Dispose();
    }

    public Task Use(HttpContext context, Func<Task> next)
    {
        var path = context.Request.Path.Value;
        if (path is not null
            && path != _pathMetrics
            && path != "/health"
            && path != "/favicon.ico")
        {
            _counter1.WithLabels(context.Request.Method, path).Inc();
            _counter2.IncTo(_proc.PrivateMemorySize64);
            _counter3.IncTo(GC.GetTotalMemory(true));
            _counter4.IncTo(Environment.WorkingSet);
        }

        return next.Invoke();
    }
}

Nel costruttore della classe mi sono creato un custom counter che inserirà questi due dati: Method e Endpoint, quindi altri tre Counter che aggiungeranno altre informazioni sul consumo di memoria della mia applicazione. Quindi il mio middleware (metodo Use) leggerà ogni Path e inserirà nel Counter le informazioni.

In questo esempio ho usato entrambi i tipo di Counter di cui ho scritto prima. Il primo di tipo counter per incrementare il numero di pagine visitate:

_counter1 = Metrics.CreateCounter(
"webapimethods_path_counter",
"requests to webapimethods",
new CounterConfiguration
{
    LabelNames = new[] { "method", "endpoint" }
});

Alla creazione del Counter ho inserito il nome, il testo descrittivo che sarà usato per l'help, e due Label che differenzieranno il record salvato con il path e il method (GET, POST...) usato per la richiesta. E' possibile usare più Label per specificare nel dettaglio altre informazioni e per ottenere report più dettagliati. Essendo di tipo Counter, per aumentarne il contatore, scriverò poi:

_counter1.WithLabels(context.Request.Method, path).Inc();

Gli altri Counter sono di tipo gauge (con valore assoluto):

_counter2 = Metrics.CreateCounter(
    "private_memory_size_64",
    "PrivateMemorySize64");
...
_counter2.IncTo(_proc.PrivateMemorySize64);

Senza parametri aggiuntivi, definito il Counter, uso il metodo IncTo per inserire il valoro assoluto.

Una volta lanciato ho il risultato voluto richiamando nell'url la pagina metrics:

# HELP process_start_time_seconds Start time of the process since unix epoch in seconds.
# TYPE process_start_time_seconds gauge
process_start_time_seconds 1641932683.7917504
# HELP process_private_memory_bytes Process private memory size
# TYPE process_private_memory_bytes gauge
process_private_memory_bytes 38047744
# HELP process_working_set_bytes Process working set
# TYPE process_working_set_bytes gauge
process_working_set_bytes 48214016
# HELP process_virtual_memory_bytes Virtual memory size in bytes.
# TYPE process_virtual_memory_bytes gauge
process_virtual_memory_bytes 2213288710144
# HELP process_cpu_seconds_total Total user and system CPU time spent in seconds.
# TYPE process_cpu_seconds_total counter
process_cpu_seconds_total 1.234375
# HELP process_num_threads Total number of threads
# TYPE process_num_threads gauge
process_num_threads 23
# HELP process_open_handles Number of open handles
# TYPE process_open_handles gauge
process_open_handles 432
# HELP system_environment_workingset System.Environment.WorkingSet
# TYPE system_environment_workingset counter
system_environment_workingset 48316416
# HELP http_request_duration_seconds The duration of HTTP requests processed by an ASP.NET Core application.
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_sum{code="200",method="GET",controller="",action=""} 0.09010299999999999
http_request_duration_seconds_count{code="200",method="GET",controller="",action=""} 12
http_request_duration_seconds_bucket{code="200",method="GET",controller="",action="",le="0.001"} 3
http_request_duration_seconds_bucket{code="200",method="GET",controller="",action="",le="0.002"} 8
http_request_duration_seconds_bucket{code="200",method="GET",controller="",action="",le="0.004"} 10
http_request_duration_seconds_bucket{code="200",method="GET",controller="",action="",le="0.008"} 10
http_request_duration_seconds_bucket{code="200",method="GET",controller="",action="",le="0.016"} 11
http_request_duration_seconds_bucket{code="200",method="GET",controller="",action="",le="0.032"} 11
http_request_duration_seconds_bucket{code="200",method="GET",controller="",action="",le="0.064"} 12
http_request_duration_seconds_bucket{code="200",method="GET",controller="",action="",le="0.128"} 12
http_request_duration_seconds_bucket{code="200",method="GET",controller="",action="",le="0.256"} 12
http_request_duration_seconds_bucket{code="200",method="GET",controller="",action="",le="0.512"} 12
http_request_duration_seconds_bucket{code="200",method="GET",controller="",action="",le="1.024"} 12
http_request_duration_seconds_bucket{code="200",method="GET",controller="",action="",le="2.048"} 12
http_request_duration_seconds_bucket{code="200",method="GET",controller="",action="",le="4.096"} 12
http_request_duration_seconds_bucket{code="200",method="GET",controller="",action="",le="8.192"} 12
http_request_duration_seconds_bucket{code="200",method="GET",controller="",action="",le="16.384"} 12
http_request_duration_seconds_bucket{code="200",method="GET",controller="",action="",le="32.768"} 12
http_request_duration_seconds_bucket{code="200",method="GET",controller="",action="",le="+Inf"} 12
http_request_duration_seconds_sum{code="404",method="GET",controller="",action=""} 0.0005065
http_request_duration_seconds_count{code="404",method="GET",controller="",action=""} 1
http_request_duration_seconds_bucket{code="404",method="GET",controller="",action="",le="0.001"} 1
http_request_duration_seconds_bucket{code="404",method="GET",controller="",action="",le="0.002"} 1
http_request_duration_seconds_bucket{code="404",method="GET",controller="",action="",le="0.004"} 1
http_request_duration_seconds_bucket{code="404",method="GET",controller="",action="",le="0.008"} 1
http_request_duration_seconds_bucket{code="404",method="GET",controller="",action="",le="0.016"} 1
http_request_duration_seconds_bucket{code="404",method="GET",controller="",action="",le="0.032"} 1
http_request_duration_seconds_bucket{code="404",method="GET",controller="",action="",le="0.064"} 1
http_request_duration_seconds_bucket{code="404",method="GET",controller="",action="",le="0.128"} 1
http_request_duration_seconds_bucket{code="404",method="GET",controller="",action="",le="0.256"} 1
http_request_duration_seconds_bucket{code="404",method="GET",controller="",action="",le="0.512"} 1
http_request_duration_seconds_bucket{code="404",method="GET",controller="",action="",le="1.024"} 1
http_request_duration_seconds_bucket{code="404",method="GET",controller="",action="",le="2.048"} 1
http_request_duration_seconds_bucket{code="404",method="GET",controller="",action="",le="4.096"} 1
http_request_duration_seconds_bucket{code="404",method="GET",controller="",action="",le="8.192"} 1
http_request_duration_seconds_bucket{code="404",method="GET",controller="",action="",le="16.384"} 1
http_request_duration_seconds_bucket{code="404",method="GET",controller="",action="",le="32.768"} 1
http_request_duration_seconds_bucket{code="404",method="GET",controller="",action="",le="+Inf"} 1
# HELP dotnet_collection_count_total GC collection count
# TYPE dotnet_collection_count_total counter
dotnet_collection_count_total{generation="1"} 21
dotnet_collection_count_total{generation="0"} 21
dotnet_collection_count_total{generation="2"} 21
# HELP dotnet_total_memory_bytes Total known allocated memory
# TYPE dotnet_total_memory_bytes gauge
dotnet_total_memory_bytes 1600320
# HELP http_requests_received_total Provides the count of HTTP requests that have been processed by the ASP.NET Core pipeline.
# TYPE http_requests_received_total counter
http_requests_received_total{code="200",method="GET",controller="",action=""} 12
http_requests_received_total{code="404",method="GET",controller="",action=""} 1
# HELP http_requests_in_progress The number of requests currently in progress in the ASP.NET Core pipeline. One series without controller/action label values counts all in-progress requests, with separate series existing for each controller-action pair.
# TYPE http_requests_in_progress gauge
http_requests_in_progress{method="GET",controller="",action=""} 0
# HELP webapimethods_path_counter requests to webapimethods
# TYPE webapimethods_path_counter counter
webapimethods_path_counter{method="GET",endpoint="/host"} 2
webapimethods_path_counter{method="GET",endpoint="/"} 10
# HELP gc_get_total_memory GC.GetTotalMemory
# TYPE gc_get_total_memory counter
gc_get_total_memory 1477464
# HELP private_memory_size_64 PrivateMemorySize64
# TYPE private_memory_size_64 counter
private_memory_size_64 36917248

E infatti sono presenti le due voci per le due pagine inserite, la home e la host:

webapimethods_path_counter{method="GET",endpoint="/host"} 1
webapimethods_path_counter{method="GET",endpoint="/"} 3

Inoltre sono presenti i tre Counter custom del consumo di memoria con tanto di help:

# HELP system_environment_workingset System.Environment.WorkingSet
# TYPE system_environment_workingset counter
system_environment_workingset 48316416
...
# HELP gc_get_total_memory GC.GetTotalMemory
# TYPE gc_get_total_memory counter
gc_get_total_memory 1477464
# HELP private_memory_size_64 PrivateMemorySize64
# TYPE private_memory_size_64 counter
private_memory_size_64 36917248

Ma se si osserva l'output sono presenti molte altre voci, alcune molto utili, come il numero Thread e Handle in esecuzione... E tutte queste informazioni saranno esposte e visibili Prometheus con le query viste prima.

E' giunto il momento di creare un ServiceMonitor dedicato alla mia applicazione. Innanzitutto è necessario creare una immagine Docker e caricarla su un public registry (sempre se non si ha uno privato anche installato in locale). Nel codice, il cui link inserirò alla fine di questo post, ho inserito il Dockerfile per la sua creazione nel formato compatto, e anche il file di Deploy per Kubernetes che qui inserirò spezzato in due parti. La prima parte è per il Deploy:

apiVersion: v1
kind: Namespace
metadata:
  name: testmetrics
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: minimalapiwebservice
  name: minimalapiwebservice
  namespace: testmetrics
spec:
  selector:
    app: minimalapiweb
  ports:
  - name: metrics
    protocol: TCP
    port: 8081
    targetPort: metrics
  type: LoadBalancer
#  externalIPs: # <- per minikube
#  - 192.168.49.2 # <- per minikune. Ip = minikube ip dal terminale
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: minimalapiweb
  labels:
    app: minimalapiweb
  namespace: testmetrics
spec:
  replicas: 1
  selector:
    matchLabels:
      app: minimalapiweb
  template:
    metadata:
      labels:
        app: minimalapiweb
    spec:
      containers:
      - name: run
        image: sbraer/webapimetrics:v1.0
        imagePullPolicy: IfNotPresent
        ports:
        - name: metrics
          containerPort: 3000
        securityContext:
          runAsUser: 1000
          runAsGroup: 100
        #command: ["./app/WebApiMetrics"]
        readinessProbe:
          httpGet:
            path: /health
            port: 3000
          periodSeconds: 10
          initialDelaySeconds: 3
        livenessProbe:
          httpGet:
            path: /health
            port: 3000
          periodSeconds: 10
          initialDelaySeconds: 10

Innanzitutto creo un Namespace dedicato - testmetrics - quindi un Service e un Deploy sempre in questo Namespace. Ho inserito la definizione delle porte sotto il nome metrics, perché sembra che sia necessario inserirne il nome perché possa essere viste dal ServiceMonitor, così come ho inserito la label nel service definito in questo modo:

labels:
  app: minimalapiwebservice

Tutto è necessario per il prossimo ServiceMonitor:

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: minimalapiweb-sm
spec:
  endpoints:
  - interval: 5s
    port: metrics
  namespaceSelector:
    matchNames:
    - testmetrics
  selector:
    matchLabels:
      app: minimalapiwebservice

In endpoint c'è il nome della porta definita nel Pod, in namespaceselector il Namespace dove è inerita l'applicazione e in selector il nome del Service. Infine in interval definisco quante volte Prometheus dovrà venire a prendere questi dati (in questo caso ogni 5 secondi).

Inserisco il Deploy nel mio cluster di Kubernetes:

kubectl apply -f deploy-webapp.yaml

E nel browser richiamo le due pagine di esempio:

http://localhost:8081 mostra una stringa di esempio.

http://localhost:8081/host mostra anche il nome del container dove gira la mia applicazione.

E infine controllo che la pagina che sarà utilizzata da Prometheus risponda:

http://localhost:8081/metrics

Se tutto funziona, ritorna la pagina vista in precedenza. Ora tornando in Prometheus, dopo qualche istante, apparirà dopo il refresh della pagina anche il mio servicemonitor in prima posizione:

Ora andando nella home page di Prometheus posso eseguire le query su di esso. Nel mio codice avevo inserito come custom Counter la voce webapimethods_path_counter:

Ora in Grafana provo a importare quei miei dati. Nella home page seleziono Create, Dashboard. Nella nuova schermata inserisco un nuovo panel.

Dopo averne inseriti alcuni si potrebbe avere un output come il seguente dove visualizzo oltre alle varie voci sul consumo di memoria, il numero di richieste che hanno restituito un valore 200 in rapporto con le 404 (pie), numero di Thread usati, numero di pagine richieste per Path (mio custom Counter):

Come sempre parto con l'idea di scrivere un post breve e mi esce un post di lunghezza allucinante. Va be', non ho il dono della sintesi, anche se qui avrei molto altro da scrivere... e non ho toccato l'argomento Alert... Ci rinuncio.

Per spegnere tutto:

kubectl delete -f deploy-webapp.yaml
helm uninstall my-grafana -n monitors
helm uninstall my-prometheus -n monitors

Qui il codice usato.

Commenti

Visualizza/aggiungi commenti

| Condividi su: Twitter, Facebook, LinkedIn

Per inserire un commento, devi avere un account.

Fai il login e torna a questa pagina, oppure registrati alla nostra community.

Nella stessa categoria
I più letti del mese