Una guía para los patrones de diseño de React

La observabilidad de los complejos sistemas dinámicos actuales depende del conocimiento del dominio o, lo que es más importante, de "incógnitas" desconocidas basadas en un conocimiento del dominio incompleto. En otras palabras, los casos que quedan entre las grietas nos sorprenden, como lo muestra la siguiente cita:

Nos tomó menos de una hora descubrir cómo recuperar la red; nos tomó algunas horas adicionales porque nos tomó mucho tiempo controlar los IMP que se comportaban mal y volver a la normalidad. Un sistema de alerta de software incorporado (suponiendo, por supuesto, que sea inmune a falsos positivos) puede permitirnos restaurar la red mucho más rápido, reduciendo significativamente la duración de la interrupción. Esto no quiere decir que un mejor sistema de alarma y control sea un sustituto de una investigación y un diseño cuidadosos para tratar de asignar adecuadamente la utilización de recursos vitales, sólo que es un complemento necesario para abordar las inevitables caídas que incluso los más Un diseño cuidadoso inevitablemente caerá en Caso entre grietas - (Rosen, RFC 789)

Esencialmente, la observabilidad es la forma en que exponemos el comportamiento de un sistema (con suerte, de alguna manera disciplinada) y entendemos ese comportamiento.

En este artículo, discutiremos la importancia de la observabilidad y examinaremos los conceptos básicos de cómo escribir aplicaciones Rust observables.

¿Por qué es importante la observabilidad?

Debido a la proliferación de implementaciones de microservicios y motores de orquestación, cómo acceder a opciones de arranque avanzadas en Windows 11 (6 formas) Nuestros sistemas se han vuelto más complejos, con grandes empresas ejecutando miles de microservicios e incluso nuevas empresas ejecutando cientos de microservicios.

La dura realidad de los microservicios es que de repente obligan a todos los desarrolladores a convertirse en ingenieros de sistemas distribuidos o en la nube, lidiando con las complejidades inherentes de los sistemas distribuidos. Específicamente, fallas parciales, donde la indisponibilidad de uno o más servicios puede afectar negativamente al sistema de manera desconocida. – (Meiklejohn et al., Pruebas de inyección de fallas de nivel de servicio)

En esta era de complejidad, implementar la observabilidad puede ser de gran ayuda para construir, solucionar problemas y comparar sistemas a largo plazo. Proporcionar observabilidad comienza con la recopilación de datos de salida (telemetría e instrumentación) de nuestros sistemas en ejecución, en un nivel apropiado de abstracción (generalmente organizado en torno a rutas de solicitud) para que podamos explorar y analizar patrones de datos y encontrar correlaciones cruzadas.

Sobre el papel, esto parece fácil de lograr. ¿Cómo desactivar el teclado que viene con la computadora portátil en Windows 11? Tres formas sencillas Reunimos los tres pilares (registros, métricas y seguimientos) y terminamos. Sin embargo, estos tres pilares son solo bits en sí mismos, y recopilar los bits más útiles y analizar la colección de bits en conjunto es lo más complejo.

Formar la abstracción correcta es la parte difícil. Puede ser muy específico de un dominio y se basa en la construcción de un modelo del comportamiento de nuestro sistema que esté abierto al cambio y listo para lo inesperado. Implica que los desarrolladores tengan que involucrarse más en cómo generar y diagnosticar eventos en sus aplicaciones y sistemas.

Lanzar declaraciones de registro por todos lados y recopilar todas las métricas posibles pierde valor a largo plazo y causa otros problemas. Necesitamos exponer y mejorar resultados significativos para que la asociación de datos sea posible.

Después de todo, este es un artículo de Rust y, si bien Rust se creó teniendo en cuenta la seguridad, la velocidad y la eficiencia, exponer el comportamiento del sistema no fue uno de sus principios fundacionales.

¿Cómo podemos hacer que las aplicaciones Rust sean más observables?

Partiendo de los primeros principios, ¿cómo instrumentamos nuestro código, recopilamos rastros significativos y derivamos datos que nos ayuden a explorar "incógnitas" desconocidas? Si todo está controlado por eventos y tenemos seguimientos que capturan una variedad de eventos/acciones, incluidas solicitudes/respuestas, lecturas/escrituras de bases de datos y/o errores de caché, etc., entonces es importante tener que comunicarnos con el mundo exterior. para una observabilidad de un extremo a otro ¿Cuál es el truco para crear una aplicación Rust desde cero? ¿Cuáles son los componentes básicos?

Lamentablemente, aquí hay más de un truco o solución milagrosa, especialmente al escribir servicios Rust, lo que deja mucho en manos del desarrollador. Primero, lo único en lo que realmente podemos confiar para comprender y depurar "incógnitas" desconocidas son los datos de telemetría, y debemos asegurarnos de presentar datos de telemetría contextuales significativos (por ejemplo, campos correlacionables como, y). En segundo lugar, necesitamos una forma de explorar ese resultado y correlacionarlo entre sistemas y servicios. request_path parent_spantrace_id categoryasunto

En esta publicación, nos centraremos principalmente en recopilar y recopilar datos de salida contextuales significativos, pero también discutiremos la mejor manera de conectarnos a plataformas que brinden procesamiento, análisis y visualización adicionales. Afortunadamente, hay herramientas centrales disponibles para instrumentar los programas Rust para recopilar datos de eventos estructurados y procesar y emitir rastros para comunicación asincrónica y sincrónica.

Nos centraremos en el marco más estándar y flexible, Tracing, que se basa en intervalos, eventos y suscriptores, y en cómo aprovechar su capacidad de composición y personalización.

Sin embargo, aunque tenemos un marco extenso, como Tracing , que nos ayuda a escribir las bases de los servicios observables en Rust, la telemetría significativa no viene "lista para usar" ni "gratis".

Obtener las abstracciones correctas en Rust no es tan sencillo como en otros lenguajes. En cambio, se debe crear una aplicación sólida sobre comportamientos en capas, todos los cuales brindan un control ejemplar para el desarrollador informado, pero pueden resultar engorrosos para los menos experimentados.

Descompondremos el espacio del problema en una serie de capas componibles que actúan sobre cuatro unidades distintas de comportamiento:

  • Almacenar información de contexto para uso futuro.

  • Aumente los registros estructurados con información contextual

  • Derivación de métricas por detección y duración del intervalo

  • Interoperabilidad de telemetría abierta para rastreo distribuido

De manera similar a cómo el documento QuickCheck original sobre pruebas basadas en propiedades se basaba en que los usuarios especificaran propiedades y proporcionaran instancias para tipos definidos por el usuario, crear un servicio Rust observable de extremo a extremo requiere comprender cómo se generan los rastros, cómo se especifican los datos y qué telemetría tiene sentido a medida que crece su aplicación. Esto es especialmente cierto al depurar y/o explorar inconsistencias, fallas parciales y características de rendimiento cuestionables.

La recopilación de trazas impulsará todo lo que se muestra en los ejemplos de este artículo, donde los intervalos y los eventos serán los lentes a través de los cuales unimos una imagen completa de una cantidad conocida. Tendremos registros, pero los trataremos como eventos estructurados. Recopilaremos métricas, pero las automatizaremos con instrumentación y expansión, y exportaremos datos de seguimiento compatibles con OpenTelemetry a una plataforma de seguimiento distribuida como Jaeger.

Alcance, eventos y seguimiento

Antes de entrar en detalles de implementación, comencemos con algunos términos y conceptos con los que debemos estar familiarizados, como intervalos, trazas y eventos.

al otro lado de

Un tramo representa una operación o segmento que pertenece a un seguimiento y sirve como componente principal del seguimiento distribuido. Para cualquier solicitud determinada, el intervalo inicial (sin padre) se denomina intervalo raíz. Generalmente se expresa como la latencia de un extremo a otro de una solicitud de usuario completa dada una traza distribuida.

También puede haber tramos secundarios posteriores, que se pueden anidar en otros tramos principales diferentes. El tiempo total de ejecución de un lapso incluye el tiempo transcurrido en ese lapso y todo el subárbol representado por sus hijos.

A continuación se muestra un ejemplo de un registro de intervalo principal que se comprime intencionalmente para una nueva solicitud:

nivel=INFO span nombre="solicitud HTTP" span=9008298766368774 parent_span=9008298766368773 span_event=new_span timestamp=2022-10-30T22:30:28.798564Z http.client_ip=127.0.0.1:61033 http.host=127.0. 0.1:3030 http .method=POST http.route=/songs trace_id=b2b32ad7414392aedde4177572b3fea3

Este registro de intervalo contiene información y metadatos importantes, como la ruta de solicitud ( ), la marca de tiempo ( ), el método de solicitud ( ) y el identificador de seguimiento ( y ), respectivamente. Usaremos esta información para demostrar cómo unir trazas de principio a fin. http.ruta 2022-10-30T22:30:28.798564Zhttp.método spanparent_span``trace_id

¿Por qué se llama lapso? Ben Sigelman, autor del artículo de infraestructura de rastreo Dapper de Google, considera estos factores en una breve historia de The Span: Difícil de amar, difícil de matar:

  • En el código mismo, la API parece un temporizador.

  • Cuando se ve una traza como un gráfico dirigido, la estructura de datos parece un nodo o vértice.

  • En el contexto del registro estructurado y multiproceso (nota al margen: de esto se trata el rastreo distribuido al final del día), uno podría pensar en los intervalos como dos eventos.

  • Dado un diagrama de tiempo simple, es fácil llamar a este concepto duración o ventana.

evento

Un evento representa una única operación en el tiempo, donde algo sucede durante la ejecución de algún programa arbitrario. A diferencia del registro no estructurado fuera de banda, tratamos los eventos como la unidad central de ingesta que ocurre dentro del contexto de un intervalo determinado y se estructuran mediante campos clave-valor (similar a los registros de intervalo anteriores). Más precisamente, estos eventos se denominan eventos de intervalo:

nivel=INFO msg="solicitud de proveedor finalizada de procesamiento" sujeto=proveedor.response categoría=http.response proveedor.status=200 proveedor.response_headers="{\"content-type\": \"application/json\", \" variar\": \"Codificación de aceptación, Agente de usuario\", \"codificación de transferencia\": \"fragmentado\"}" proveedor.url=http://localhost:8080/.well-known/jwks. json seller.request_path=/.well-known/jwks.json target="application::middleware::logging" location="src/middleware/logging.rs:354" timestamp=2022-10-31T02:45:30.683888Z

Nuestra aplicación también puede tener eventos de registro estructurados arbitrarios que ocurren fuera del contexto del intervalo. Por ejemplo, mostrar los ajustes de configuración al inicio o monitorear cuando se actualizan los cachés.

rastro

Un seguimiento es una colección de intervalos que representan algún flujo de trabajo, como una solicitud del servidor o un paso de procesamiento de cola/flujo para un elemento. Esencialmente, una traza es un gráfico acíclico dirigido de tramos, donde los bordes que conectan tramos indican una relación causal entre un tramo y su padre.

A continuación se muestra un seguimiento de ejemplo visualizado en la interfaz de usuario de Jaeger:

Si esta aplicación fuera parte de un seguimiento distribuido más grande, la veríamos anidada dentro de un intervalo principal más grande.

Ahora, con estos términos en la mano, ¿cómo comenzamos a implementar el esqueleto de una aplicación Rust lista para la observabilidad?

Combine múltiples capas de seguimiento para generar suscriptores

El marco de seguimiento se divide en diferentes componentes (como cajas). Para nuestros propósitos nos centraremos en este conjunto de dependencias: .toml

opentelemetry = { versión = "0.17", características = ["rt-tokio", "trace"] } 
opentelemetry-otlp = { versión = "0.10", características = ["metrics", "tokio", "tonic", " tonic-build", "prost", "tls", "tls-roots"], default-features = false} opentelemetry 
-semantic-conventions = "0.9" 
tracing = "0.1" 
tracing-appender = "0.2" 
tracing-opentelemetry = "0.17" 
tracing-subscriber = {versión = "0.3", características = ["env-filter", "json", "registry"]}

Esta caja nos permite escribir suscriptores de seguimiento desde unidades de comportamiento más pequeñas (llamadas capas) para recopilar y enriquecer datos de seguimiento. seguimiento_suscriptor

Él mismo es responsable de registrar nuevos intervalos (con intervalos) durante la creación, registrar y agregar valores de campo y anotaciones posteriores a los intervalos, y filtrar intervalos y eventos. ID del suscriptor

Cuando se combina con un Suscriptor, la capa utiliza ganchos activados durante todo el ciclo de vida del tramo:

. }

¿Cómo están organizadas las capas del código? Comencemos con el método de configuración, generando un registro definido por cuatro combinadores o capas: con

fn setup_tracing( 
    escritor: tracing_appender::non_blocking::NonBlocking, 
    settings_otel: &Otel, 
) -> Resultado<()> { 
    let tracer = init_tracer(settings_otel)?; 
    let registro = tracing_subscriber::Registry::default() 
        .with(StorageLayer.with_filter(LevelFilter::TRACE)) 
        .with(tracing_opentelemetry::layer()...                 
        .with(LogFmtLayer::new(writer).with_target( verdadero)... 
        .con(MetricsLayer)... 
        ); 
     ...

Esta función generalmente se llama cuando se inicializa el método del servidor. La propia capa de almacenamiento no proporciona resultados, pero actúa como un almacén de información para recopilar información de seguimiento contextual para mejorar y ampliar la salida posterior de otras capas del proceso. setup_tracing main()principal.rs

Este método controla qué intervalos y eventos están habilitados para esta capa, queremos capturar básicamente todo, esta es la opción más detallada. with_filter``LevelFilter::TRACE

Examinemos cada capa y veamos cómo opera cada capa con los datos de seguimiento que recopila y vincula al ciclo de vida del tramo. Personalizar el comportamiento de cada capa implica implementar los ganchos del ciclo de vida asociados con el rasgo, como se muestra a continuación. Capa``on_new_span

En el camino, demostraremos cómo estas unidades de comportamiento pueden aumentar la extensión y los formatos de registro de eventos, derivar automáticamente algunas métricas y enviar lo que recopilamos a una plataforma de seguimiento distribuida como Jaeger, Honeycomb o Datadog. Comenzaremos con el nuestro, que proporciona información contextual de la que otras capas pueden beneficiarse. Capa de almacenamiento

Almacenar información de contexto para uso futuro.

en nuevo lapso

impl<S> Layer<S> para StorageLayer 
donde 
    S: Suscriptor + para<'span> LookupSpan<'span>, 
{ 
    fn on_new_span(&self, attrs: &Attributes<'_>, id: &Id, ctx: Context<'_ , S>) { 
        let span = ctx.span(id).expect("Span no encontrado"); 
        // Queremos heredar los campos del intervalo principal, si lo hay. 
        let mut visitante = if let Some(parent_span) = span.parent() { 
            let mut extensiones = parent_span.extensions_mut(); 
            let mut internal = extensiones 
                .get_mut::<Almacenamiento>() 
                .map(|v| v.to_owned()) 
                .
                Vaca::from(parent_span.id().into_u64().to_string()), 
            ); 
            inside 
        } else { 
            Almacenamiento::default() 
        }; 
​let
        mut extensions = span.extensions_mut(); 
        attrs.record(&mut visitante); 
        extensiones.insert(visitante); 
    } 
...

Al iniciar un nuevo intervalo (a través de), por ejemplo, una solicitud a la aplicación a un punto final, nuestro código verifica si ya estamos dentro del intervalo principal. De lo contrario, se utilizará de forma predeterminada el vacío recién creado, que es lo que está envuelto debajo del capó. on_new_span POST/canciones HashmapAlmacenamiento::default()

Para simplificar, utilizamos de forma predeterminada claves asignadas a referencias de cadenas y valores a punteros inteligentes de copia en escritura (Cow) alrededor de referencias de cadenas:

#[derive(Clonar, Depurar, Predeterminado)] 
pub(crate) struct Storage<'a> { 
    valores: HashMap<&'a str, Cow<'a, str>>, 
}

Gracias a los intervalos, el almacenamiento entre capas conserva los campos en todas las capas durante la vida útil del intervalo, lo que nos permite asociar de forma variable datos arbitrarios a intervalos o leer de forma inmutable desde datos persistentes (incluidas nuestras propias estructuras de datos). extensiones


Más artículos excelentes de LogRocket:

  • No se pierda ni un momento de repetición: aquí tiene un boletín informativo seleccionado de LogRocket

  • Descubra cómo Galileo de LogRocket cancela el ruido y resuelve proactivamente problemas en su aplicación

  • Utilice useEffect de React para optimizar el rendimiento de la aplicación

  • cambiar entre múltiples versiones de nodo

  • Aprenda a usar los accesorios secundarios de React con TypeScript

  • Explore el uso de CSS para crear un cursor de mouse personalizado

  • Los consejos asesores no son sólo para ejecutivos. Únase al consejo asesor de contenido de LogRocket. Ayudará a comprender el tipo de contenido que creamos y obtendrá acceso a reuniones exclusivas, pruebas sociales y regalos.


Muchos de estos ganchos del ciclo de vida involucran la lucha libre, que puede ser un poco detallada. El registro es lo que realmente recopila y almacena datos de extensión, que luego se pueden implementar implementando .extensions``LookupSpan

Otro código a destacar es registrar valores de campo de varios tipos visitando cada tipo de valor, que es un rasgo que se debe implementar: attrs.record(&mut visitante)

// Solo una muestra de los métodos implementados 
impl Visita para almacenamiento<'_> { 
    /// Visita un valor entero de 64 bits con signo. 
    fn record_i64(&mut self, campo: &Campo, valor: i64) { 
        self.values 
            ​​.insert(field.name(), Cow::from(value.to_string())); 
    } 
    ... // omitido por brevedad 
    fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) { // 
        Nota: esto se invoca mediante `debug!` e `info! macros 
        let debug_formatted = format!("{:?}", valor); 
        self.values.insert(field.name(), Cow::from(debug_formatted));    
    } 
...

Una vez que hayamos registrado todos los valores de cada tipo, el visitante almacenará todos estos valores en un almacenamiento que las capas posteriores podrán utilizar para activadores del ciclo de vida en el futuro. mapa hash

registrar en el archivo

impl<S> Capa<S> para StorageLayer 
donde 
    S: Suscriptor + para<'span> LookupSpan<'span>, 
{ 
... // omitido por brevedad 
    fn on_record(&self, span: &Id, valores: &Record<'_ >, ctx: Context<'_, S>) { 
        let span = ctx.span(span).expect("Span no encontrado"); 
        let mut extensiones = span.extensions_mut(); 
        let visitante = extensiones 
            .get_mut::<Almacenamiento>() 
            .expect("¡Visitante no encontrado en 'registro'!"); 
        valores.record(visitante); 
    } 
... // omitido por brevedad

A medida que avanzamos en cada uno de los desencadenantes del ciclo de vida, notaremos que los patrones son similares. Obtenemos un identificador de alcance mutable en la extensión de la tienda del intervalo y registramos los valores a medida que llegan.

Este gancho notifica a la capa que un intervalo con el identificador dado ha registrado el valor dado mediante una llamada como: debug_span!``info_span!

let span = info_span!( 
    "proveedor.cdbaby.task", 
    asunto = "proveedor.cdbaby", 
    categoría = "proveedor" 
);

evento

registro(visitante); 
                } 
            }) 
        }); 
... // omitido por brevedad
    }

Para nuestra capa de almacén de contexto, normalmente no es necesario conectarse a eventos (como mensajes). Sin embargo, esto es valioso para almacenar información sobre campos de eventos que queremos preservar, lo que podría ser útil en otra capa más adelante en el proceso. rastreo::error!

Un ejemplo es almacenar eventos atribuidos a errores para que podamos rastrear los errores en la capa de métricas (por ejemplo, una matriz de campos vinculados a la clave de error). ON_EVENT_KEEP_FIELDS

al entrar y cerrar

impl<S> Layer<S> para StorageLayer 
donde 
    S: Suscriptor + for<'span> LookupSpan<'span>, 
{ 
... // omitido por brevedad 
    fn on_enter(&self, span: &Id, ctx: Context<'_ , S>) { 
        let span = ctx.span(span).expect("Span no encontrado"); 
        let mut extensiones = span.extensions_mut(); 
        if extensiones.get_mut::<Instante>().is_none() { 
            extensiones.insert(Instante::ahora); 
        } 
    } 

    fn on_close(&self, id: Id, ctx: Context<'_, S>) { 
        let span = ctx.span(&id).expect("Span no encontrado"); 
        let extensiones mut = span. extensiones_mut(); 
        let transcurridos_millisegundos = extensiones
            .get_mut::<Instant>() 
            .map(|i| i.elapsed().as_millis()) 
            .unwrap_or(0); 

        let visitante = extensiones 
            .get_mut::<Almacenamiento>() 
            .expect("Visitante no encontrado en 'registro'"); 

        visitante.values.insert( 
            LATENCY_FIELD, // "latency_ms" 
            Vaca::from(formato!("{}", transcurridos_milisegundos)), 
        ); 
    } 
... // omitido por brevedad

Un lapso es esencialmente un intervalo de tiempo marcado, con un inicio y un final bien definidos. Para rangos de intervalos, queremos capturar el tiempo transcurrido entre el momento en que una entrada tiene un intervalo determinado () y el momento en que se cierra para una operación determinada. id``Instantáneo::ahora

Almacenar la latencia de cada intervalo en nuestra extensión permite que otras capas deriven métricas automáticamente y facilita los propósitos de exploración al depurar el registro de eventos para un intervalo determinado. A continuación, podemos ver la apertura y el cierre de la tarea/proceso del proveedor, que tarda 18 ms de principio a fin: id``id=452612587184455697

nivel=INFO span_name=vendor.lastfm.task span=452612587184455697 parent_span=span=452612587184455696 span_event=new_span timestamp=2022-10-31T12:35:36.913335Z trace_id=c53cb20e4ab4fa42a a5836d26e974de2 http.client_ip=127.0.0.1:51029 sujeto=proveedor.lastfm application.request_path=/songs http.method=POST categoría=proveedor http.host=127.0.0.1:3030 http.route=/songs request_id=01GGQ0MJ94E24YYZ6FEXFPKVFP 
nivel=INFO span_name=vendor.lastfm.task span=452612587184455697 parent_span=span=45 2612587184455696 span_event=close_span timestamp=2022-10-31T12:35:36.931975Z trace_id=c53cb20e4ab4fa42aa5836d26e974de2 latency_ms=18 http.client_ip=127.0.0.1:51029 sujeto=vendor.lastfm application.request_path=/songs http.method=POST categoría=proveedor http .host=127.0.0.1:3030 http.route=/songs request_id=01GGQ0MJ94E24YYZ6FEXFPKVFP

Aumente los registros estructurados con información contextual

Ahora, veremos cómo se aprovechan los datos almacenados para la salida de telemetría real observando la capa de formato del registro de eventos:

.with(LogFmtLayer::new(writer).with_target(true)...

Muchos ejemplos favorecen a los formateadores personalizados al escribir implementaciones de suscriptores y capas personalizadas:

  • Un útil tutorial en línea que muestra cómo crear un registrador JSON personalizado distinto del proporcionado de forma predeterminada en el trackbox.

  • formato bunyan

  • Registro de Embark Studio

Tenga en cuenta que el ejemplo de registro anterior utiliza el mismo formato, inspirado en la implementación de InfluxDB).

Recomendamos utilizar una capa o biblioteca publicada, o seguir los tutoriales enumerados anteriormente para entrar en detalles sobre cómo generar los datos en su formato preferido.

Este artículo crea nuestra propia capa de formateador personalizada y, para este artículo, nos volveremos a familiarizar con el ciclo de vida de los intervalos, específicamente los intervalos y los registros de eventos, y ahora aprovecharemos nuestros mapas de almacenamiento.

en nuevo lapso

impl<S, Wr, W> Layer<S> para LogFmtLayer<Wr, W> 
donde 
    Wr: Write + 'static, 
    W: for<'writer> MakeWriter<'writer> + 'static, 
    S: Suscriptor + for<' span> LookupSpan<'span>, 
{ 
    fn on_new_span(&self, _attrs: &Attributes<'_>, id: &Id, ctx: Context<'_, S>) { let mut p = self.printer.write() 
        ; 
        let metadata = ctx.metadata(id).expect("Abarca los metadatos que faltan"); 
        p.write_level(metadatos.nivel()); 
        p.write_span_name(metadatos.nombre()); 
        p.write_span_id(identificación); 
        p.write_span_event("new_span"); 
        pag. write_timestamp(); 

        let span = ctx.span(id).expect("Span no encontrado");
        let extensiones = span.extensiones(); 
        if let Some(visitante) = extensiones.get::<Almacenamiento>() { 
            for (clave, valor) en visitante.values() {                
                p.write_kv( 
                    decorar_campo_nombre(traducir_campo_nombre(clave)), 
                    valor.to_string(), 
                ) 
            } 
        } 
        p.write_newline(); 
    } 
... // omitido por brevedad

El código anterior imprime una representación de texto formateado del evento de intervalo utilizando el rasgo. La llamada a la impresora y todos los métodos de la impresora ejecutan los atributos de formato específicos detrás de escena (en este caso, nuevamente). MakeWriter decorate_field_nameescribe``logfmt

Volviendo a nuestro ejemplo anterior de registro de intervalos, claves como , y ahora están configuradas más claramente. El fragmento de código que se llamará aquí es cómo recorremos los valores leídos del mapa de almacenamiento, promocionando la información que observamos y recopilamos en la capa anterior. nivel spanspan_name``para (clave, valor)

Usamos esto para proporcionar contexto para aumentar los eventos de registro estructurados en otra capa. En otras palabras, componemos subcomportamientos específicos en datos de seguimiento a través de capas para crear un único suscriptor para todo el seguimiento. Por ejemplo, las claves de campo me gusta y eliminan de esta capa de almacenamiento. http.ruta``http.host

evento

impl<S, Wr, W> Layer<S> para LogFmtLayer<Wr, W> 
donde 
    Wr: Write + 'static, 
    W: for<'writer> MakeWriter<'writer> + 'static, 
    S: Suscriptor + for<' span> LookupSpan<'span>, 
{ 
... // omitido por brevedad 
    fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) { 
        let mut p = self.printer.write (); 
        p.write_level(event.metadata().level()); 
        evento.record(&mut *p); 
        // registrar la información de la fuente 
        p.write_source_info(event); 
        p.write_timestamp(); 

        ctx.lookup_current().map(|current_span| { 
            p.
            dejar extensiones = current_span.extensions(); 
            extensiones.get::<Almacenamiento>().map(|visitante| { 
                para (clave, valor) en visitante.valores() { 
                    if !ON_EVENT_SKIP_FIELDS.contains(clave) { 
                        p.write_kv( 
                            decorar_nombre_campo(traducir_nombre_campo(clave)) , 
                            valor.to_string(), 
                        ) 
                    } 
                } 
            }) 
        }); 
        p.write_newline(); 
    } 
... // omitido por brevedad

Si bien son algo tediosos, los patrones para implementar estos métodos de ciclo de vida son cada vez más fáciles de entender. Los pares clave-valor de campo, como destino y ubicación, tienen formato de acuerdo con la información de origen, lo que nos proporciona lo que vimos anteriormente. Las claves que me gustan también se obtienen del almacén de contexto. target="aplicación::middleware::logging" location="src/middleware/logging.rs:354"proveedor.request_path``vendor.url

Si bien puede requerir más trabajo implementar correctamente cualquier especificación de formato, ahora podemos ver el control detallado y la personalización que proporciona el marco de seguimiento. Esta información contextual es la forma en que, en última instancia, podemos formar dependencias dentro del ciclo de vida de la solicitud.

Derivación de métricas por detección y duración del intervalo

Las métricas, en particular, son en realidad muy malas para la observabilidad en sí, y se puede abusar fácilmente de la cardinalidad de las métricas, el número de combinaciones únicas de nombres de métricas y valores de dimensiones.

blanco

Hemos mostrado cómo derivar registros estructurados a partir de eventos. Los indicadores mismos deben formarse a partir de los eventos o períodos que los contienen.

Todavía necesitamos métricas fuera de banda, como las recopiladas en torno a los procesos (por ejemplo, uso de CPU, bytes de disco escritos/leídos). Pero si ya podemos instrumentar el código a nivel de función para determinar cuándo suceden ciertas cosas, ¿no deberían algunas métricas "salir gratis"? Como ya hemos mencionado, tenemos las herramientas, pero sólo tenemos que ponerlas en práctica.

El seguimiento proporciona una forma accesible de anotar funciones que el usuario desea instrumentar, lo que significa crear, ingresar y cerrar ámbitos cada vez que se ejecuta una función anotada. El propio compilador de Rust hace un uso intensivo de estos instrumentos anotados en todo el código base:

#[instrument(skip(self, op), nivel = "trace")] 
pub(super) fnfully_perform_op<R: fmt::Debug, Op>( 
    &mut self, 
    ubicaciones: Ubicaciones, 
    categoría: ConstraintCategory<'tcx>, 
    op: Op) -> Falible<R>

Para nuestros propósitos, veamos una función de base de datos asincrónica simple instrumentada con algunas definiciones de campos muy específicas: save_event

#[instrumento( 
    nivel = "info", 
    nombre = "record.save_event", 
    skip_all, 
    campos(category="db", sujeto="aws_db", event_id = %event.event_id, 
           event_type=%event.event_type, hotel. kind="cliente", db.system="aws_db", 
           metric_name="db_event", metric_label_event_table=%self.event_table_name, 
           metric_label_event_type=%event.event_type) 
        err(Pantalla) 
)] 
pub async fn save_event(&self, evento: &Evento ) -> de todos modos::Resultado<()> { 
    self.db_client 
        .put_item() 
        .table_name(&self.event_table_name) 
        .set(Some(event)) 
        .enviar() 
        .esperar... 
}

Nuestras funciones de detección tienen campos de prefijo como , y . Estas claves corresponden a nombres y etiquetas de métricas que normalmente se encuentran en la configuración de monitoreo de Prometheus. Volveremos a estos campos de prefijo más adelante. Primero, ampliemos el filtro que configuramos inicialmente con algunos filtros adicionales. métrica nametipo_evento event_tableCapa de métricas

Esencialmente, estos filtros hacen dos cosas: 1) generar métricas para todos los eventos en el nivel de registro de seguimiento o superior (aunque es posible que no se registren en la salida estándar dependiendo del nivel de registro configurado); 2) pasar la función de instrumentación con eventos de prefijo adicionales, como se describió anteriormente. registro``nombre = "registro. save_event"

Después de eso, todo lo que queda es regresar a nuestra implementación de capa de indicador para realizar automáticamente la derivación del indicador.

cuando está apagado

const PREFIX_LABEL: &str = "metric_label_"; 
const METRIC_NAME: &str = "metric_name"; 
const OK: &str = "ok"; 
ERROR constante: &str = "error"; 
const ETIQUETA: &str = "etiqueta"; 
const RESULT_LABEL: &str = "resultado"; 

impl<S> Capa<S> para MetricsLayer 
donde 
    S: Suscriptor + para<'span> LookupSpan<'span>, 
{ 
    fn on_close(&self, id: Id, ctx: Context<'_, S>) { 
        let span = ctx.span(&id).expect("Span no encontrado"); 
        let mut extensiones = span.extensions_mut(); 
        let transcurrido_secs_f64 = extensiones 
            .get_mut::
        if let Some(visitante) = extensiones.get_mut::<Almacenamiento>() { 
            let mut etiquetas = vec![]; 
            para (clave, valor) en visitante.values() { 
                if key.starts_with(PREFIX_LABEL) { 
                    etiquetas.push(( 
                        key.strip_prefix(PREFIX_LABEL).unwrap_or(LABEL), 
                        value.to_string(), 
                    )) 
                } 
            } 
            .. . // omitido por brevedad 
            let nombre = visitante 
                .values() 
                .get(METRIC_NAME) 
                .unwrap_or(&Cow::from(span_name)) 
                .to_string();
            si visitante.values().contains_key(ERROR) 
                etiquetas.push((RESULT_LABEL, String::from(ERROR))) 
            } else { 
                etiquetas.push((RESULT_LABEL, String::from(OK))) 
            } 
            ... // omitido por razones de brevedad 
            métricas::increment_counter!(format!("{}_total", nombre), &labels); 
            métricas::histograma!( 
                formato!("{}_duration_segundos", nombre), 
                transcurridos_secs_f64, 
                &labels 
            ); 
            ... // omitido por brevedad

En este ejemplo se desplazan muchos bits, algunos de los cuales se omiten. Sin embargo, siempre podemos realizar nuestros cálculos de histograma mediante macros accediendo al final del intervalo de tramo. on_close elapsed_secs_f64métricas::histograma!

Tenga en cuenta que aquí aprovechamos el proyecto metrics-rs. Cualquiera puede modelar esta función de la misma manera utilizando otra biblioteca de indicadores que brinde soporte para contadores e histogramas. Del mapa de almacenamiento, extraemos todas las claves etiquetadas y usamos estas claves para generar etiquetas para histogramas y contadores incrementales derivados automáticamente. métrico_*

Además, si almacenamos un evento con error, podemos usarlo como parte de la etiqueta, en función de / para diferenciar nuestras funciones generadas. Dada cualquier función instrumentada, usaremos el mismo comportamiento del código para derivar métricas de ella. ok``error

La salida que encontramos desde el punto final de Prometheus mostrará un contador similar a este:

db_event_total{event_table="eventos",event_type="Canción",result="ok",span_name="save_event\"} 8

Detectar cierres asíncronos y relaciones de tramo indirectas

Una pregunta que surge de vez en cuando es cómo detectar código que hace referencia a tramos que son relaciones indirectas, que no son entre padres e hijos, o lo que se conoce como seguimiento de referencias.

Esto se aplicará a operaciones asincrónicas que generan solicitudes a servicios posteriores para efectos secundarios o procesos que envían datos a Service Bus donde la respuesta directa o la salida devuelta no es útil en la operación que la generó.

Para estos casos, podemos instrumentar directamente cierres asincrónicos (o futuros) ingresando a un lapso determinado (capturado a continuación como referencia) asociado con nuestro futuro asincrónico cada vez que sondeamos y salimos del futuro, de la siguiente manera: follow_from``.instrument(process_span )

// Iniciar un lapso alrededor del contexto proceso spawn 
let Process_span = debug_span!( 
    parent: Ninguno, 
    "process.async", 
    sujeto = "songs.async", 
    categoría = "songs" 
); 
Process_span.follows_from(Span::current()); 

tokio::spawn( 
    movimiento asíncrono { 
        match context.process().await { 
            Ok(r) => depurar!(canción=?r, "procesado con éxito"), 
            Err(e) => ¡advertir!(error=?e , "procesamiento fallido"), 
        } 
    } 
    .instrument(process_span), 
);

Interoperabilidad de telemetría abierta para rastreo distribuido

Gran parte de la utilidad de la observabilidad proviene del hecho de que la mayoría de los servicios actuales en realidad se componen de muchos microservicios. Todos deberíamos abrir nuestras mentes.

Si varios servicios deben interconectarse a través de redes, proveedores, nubes e incluso pares orientados al borde o locales, se deben implementar algunas herramientas estándar e independientes del proveedor. Aquí es donde entra en juego OpenTelemetry (OTel), y muchas plataformas de observabilidad conocidas están más que felices de ingerir datos de telemetría compatibles con OTel.

Si bien existe un conjunto completo de herramientas Rust de código abierto que funcionan dentro del ecosistema OTel, muchos marcos web Rust conocidos aún tienen que incorporar el estándar OTel de forma integrada.

Los marcos web populares que abarcan axum, como Actix y Tokio, se basan en implementaciones personalizadas y bibliotecas externas para proporcionar integración (actix-web-opentelemetry y axum-tracing-opentelemetry respectivamente). Las integraciones de terceros han sido, con diferencia, la opción más popular y, si bien esto promueve la flexibilidad y el control del usuario, puede dificultar las cosas para quienes buscan agregar integraciones casi sin problemas.

No entraremos en detalles sobre cómo escribir una implementación personalizada aquí, pero el middleware HTTP canónico como Tower permite anular la implementación predeterminada de los intervalos de creación de solicitudes. Si se implementa según la especificación, se deben configurar los siguientes campos en los metadatos del intervalo:

  • http.client_ip: dirección IP del cliente

  • http.flavor: la versión del protocolo utilizado (HTTP/1.1, HTTP/2.0, etc.)

  • http.host: el valor del encabezado Host

  • http.method: método de solicitud

  • http.route: la ruta coincidente

  • http.request_content_length: longitud del contenido de la solicitud

  • http.response_content_length: longitud del contenido de la respuesta

  • http.scheme: esquema de URI a utilizar (o HTTP``HTTPS)

  • http.status_code: código de estado de respuesta

  • http.target: el destino de solicitud completo, incluida la ruta y los parámetros de consulta

  • http.user_agent: el valor del encabezado User-Agent

  • otel.kind: En general, encuentre más información aquí servidor

  • otel.name: Un nombre que consta de http.method``http.route

  • otel.status_code: si la respuesta es exitosa; si es 5xxOK``ERROR

  • trace_id: un identificador para un seguimiento, utilizado en todos los procesos para agrupar todos los tramos de un seguimiento en particular.

Inicializar rastreador

Rastrear y exponer otra capa que podemos usar para componer a nuestros suscriptores para agregar información de contexto de OTel a todos los tramos y concatenar y emitir esos tramos a una plataforma de observabilidad como Datadog o Honeycomb, o directamente a una instancia de Jaeger o Tempo en ejecución, que muestra el seguimiento. datos para un consumo manejable. rastreo-opentelemetry``rust-opentelemetry

Inicializar un para generar y administrar tramos es fácil: Tracer

pub fn init_tracer(settings: &Otel) -> Resultado<Tracer> { 
    global::set_text_map_propagator(TraceContextPropagator::new()); 
​let
    Resource = Resource::new(vec![ 
        otel_semcov::resource::SERVICE_NAME.string( PKG_NAME), 
        otel_semcov::resource::SERVICE_VERSION.string(VERSIÓN), 
        otel_semcov::resource::TELEMETRY_SDK_LANGUAGE.string(LANG), 
    ]); 
​let
    api_token = MetadataValue::from_str(&settings.api_token)?; 
    let endpoint = &settings.exporter_otlp_endpoint; 
​let
    mut map = MetadataMap::with_capacity(1); 
    map.insert("x-tracing-service-header", api_token); 
​let
    trace = opentelemetry_otlp::new_pipeline()
        .tracing() 
        .with_exporter(exportador(mapa, punto final)?) 
        .with_trace_config(sdk::trace::config().with_resource(recurso)) 
        .install_batch(runtime::Tokio) 
        .map_err(|e| de todos modos!( "no se pudo inicializar el rastreador: {:#?}", e))?; 
​Ok
    (trace) 
}

También es trivial incluirlo en nuestra canalización de capas. También podemos filtrar por nivel y utilizar filtros dinámicos para omitir eventos que queramos evitar en el seguimiento:

.with( 
    tracing_opentelemetry::layer() 
        .with_tracer(tracer) 
        .with_filter(LevelFilter::DEBUG) 
        .with_filter(dynamic_filter_fn(|_metadata, ctx| { 
            !ctx.lookup_current() 
                // Excluye los eventos de "Conexión" de la sesión de Rustls 
                / / que no tienen un intervalo principal 
                .map(|s| s.parent().is_none() && s.name() == "Conexión") 
                .unwrap_or_default() 
        })), 
)

Con esta inicialización de canalización, herramientas como Jaeger pueden extraer todos los seguimientos de nuestras aplicaciones, como se mostró anteriormente en este artículo. Luego, todo lo que queda es la asociación de datos, el corte y el corte en cubitos.

en conclusión

Al combinar estas capas de rastreo, podemos exponer información sobre el comportamiento del sistema de manera granular y granular, mientras obtenemos suficiente resultado y suficiente contexto para comenzar a comprender dicho comportamiento. Toda esta personalización todavía tiene un precio: no es completamente automática, pero el patrón es idiomático y hay muchos casos de uso normales para usar la capa de código abierto.

En general, esta publicación debería ayudar a que a los usuarios les resulte más fácil intentar recopilar interacciones de aplicaciones personalizadas mediante seguimientos y demostrar hasta dónde podemos llegar en la preparación de nuestras aplicaciones para manejar lo inevitable. Este es sólo el comienzo de nuestra hermosa amistad con los acontecimientos y cuándo ocurren, y por tanto, su observabilidad. La forma en que depuramos y resolvemos problemas a largo plazo es siempre un trabajo continuo.

Supongo que te gusta

Origin blog.csdn.net/weixin_47967031/article/details/132673136
Recomendado
Clasificación