Um guia para reagir a padrões de design

A observabilidade dos sistemas dinâmicos complexos de hoje depende do conhecimento do domínio ou, mais importante, de "incógnitas" desconhecidas baseadas no conhecimento do domínio incompleto. Em outras palavras, os casos que ficam entre as lacunas nos surpreendem, como mostra a citação a seguir:

Levamos menos de uma hora para descobrir como trazer a rede de volta; demoramos algumas horas extras porque levamos muito tempo para controlar os IMPs mal comportados e fazê-los voltar ao normal. Um sistema de alerta de software integrado (assumindo, é claro, que seja imune a falsos positivos) pode nos permitir restaurar a rede muito mais rapidamente, reduzindo significativamente a duração da interrupção. Isto não quer dizer que um melhor sistema de alarme e controlo seja um substituto para uma investigação e concepção cuidadosas na tentativa de alocar adequadamente a utilização de recursos vitais, mas apenas que é um complemento necessário para lidar com as inevitáveis ​​quedas que mesmo os mais um design cuidadoso inevitavelmente cairá. Case Between Cracks - (Rosen, RFC 789)

Essencialmente, observabilidade é como expomos o comportamento de um sistema (esperançosamente de alguma forma disciplinada) e entendemos esse comportamento.

Neste artigo, discutiremos a importância da observabilidade e examinaremos os fundamentos de como escrever aplicações Rust observáveis.

Por que a observabilidade é importante?

Devido à proliferação de implantações de microsserviços e mecanismos de orquestração, como acessar opções avançadas de inicialização no Windows 11 (6 maneiras) Nossos sistemas se tornaram mais complexos, com grandes empresas executando milhares de microsserviços e até mesmo startups executando centenas de microsserviços.

A dura realidade dos microsserviços é que eles repentinamente forçam cada desenvolvedor a se tornar um engenheiro de sistemas distribuídos/em nuvem, lidando com as complexidades inerentes aos sistemas distribuídos. Especificamente, falhas parciais, onde a indisponibilidade de um ou mais serviços pode afetar negativamente o sistema de formas desconhecidas. – (Meiklejohn et al., Teste de injeção de falha de nível de serviço)

Nesta era de complexidade, a implementação da observabilidade pode contribuir muito para a construção, solução de problemas e benchmarking de sistemas no longo prazo. Fornecer observabilidade começa com a coleta de dados de saída (telemetria e instrumentação) de nossos sistemas em execução, em um nível apropriado de abstração (normalmente organizado em torno de caminhos de solicitação) para que possamos explorar e dissecar padrões de dados e encontrar correlações cruzadas.

No papel, isso parece fácil de conseguir. Como desabilitar o teclado que acompanha o laptop no Windows 11? 3 maneiras fáceis Reunimos os três pilares (logs, métricas e rastreamentos) e pronto. No entanto, esses três pilares são apenas bits em si, e coletar os bits mais úteis e analisar a coleção de bits juntos é o mais complexo.

Formar a abstração correta é a parte difícil. Pode ser muito específico de um domínio e depende da construção de um modelo de comportamento do nosso sistema que esteja aberto a mudanças e pronto para o inesperado. Envolve que os desenvolvedores tenham que se envolver mais em como gerar e diagnosticar eventos em seus aplicativos e sistemas.

Jogar declarações de log em todos os lugares e coletar todas as métricas possíveis perde valor a longo prazo e causa outros problemas. Precisamos expor e aprimorar resultados significativos para que a associação de dados seja possível.

Afinal, este é um artigo do Rust e, embora o Rust tenha sido construído com segurança, velocidade e eficiência em mente, expor o comportamento do sistema não era um de seus princípios fundadores.

Como podemos tornar os aplicativos Rust mais observáveis?

Partindo dos primeiros princípios, como instrumentamos nosso código, coletamos rastros significativos e derivamos dados para nos ajudar a explorar "incógnitas" desconhecidas? Se tudo for orientado a eventos e tivermos rastreamentos que capturam uma série de eventos/ações, incluindo solicitações/respostas, leituras/gravações de banco de dados e/ou falhas de cache, etc., então é importante ter que se comunicar com o mundo exterior para observabilidade ponta a ponta Qual é o truque para construir um aplicativo Rust do zero? Quais são os blocos de construção?

Infelizmente, há mais de um truque ou solução mágica aqui, especialmente ao escrever serviços Rust, o que deixa muito para o desenvolvedor. Primeiro, a única coisa em que podemos realmente confiar para entender e depurar "incógnitas" desconhecidas são os dados de telemetria, e devemos ter certeza de apresentar dados de telemetria contextuais significativos (por exemplo, campos correlativos como , , e ). Em segundo lugar, precisamos de uma forma de explorar esse resultado e correlacioná-lo entre sistemas e serviços. request_path parent_spantrace_id categoryassunto

Nesta postagem, nos concentraremos principalmente na coleta e coleta de dados de saída contextuais significativos, mas também discutiremos a melhor forma de conectar-se a plataformas que fornecem processamento, análise e visualização adicionais. Felizmente, ferramentas básicas estão disponíveis para instrumentar programas Rust para coletar dados de eventos estruturados e processar e emitir rastreamentos para comunicação assíncrona e síncrona.

Vamos nos concentrar na estrutura mais padrão e flexível, o Tracing, que envolve períodos, eventos e assinantes, e como aproveitar sua capacidade de composição e personalização.

No entanto, embora tenhamos uma estrutura extensa, como Tracing , que nos ajuda a escrever a base dos serviços observáveis ​​em Rust, a telemetria significativa não vem "pronta para uso" ou "de graça".

Acertar as abstrações no Rust não é tão simples quanto em outras linguagens. Em vez disso, uma aplicação robusta deve ser construída sobre comportamentos em camadas, os quais fornecem controle exemplar para o desenvolvedor informado, mas podem ser complicados para os menos experientes.

Decomporemos o espaço do problema em uma série de camadas combináveis ​​que atuam em quatro unidades distintas de comportamento:

  • Armazene informações de contexto para uso futuro

  • Aumente os logs estruturados com informações contextuais

  • Derivando métricas por detecção e duração do intervalo

  • Interoperabilidade de telemetria aberta para rastreamento distribuído

Semelhante a como o artigo original do QuickCheck sobre testes baseados em propriedades dependia de usuários especificando propriedades e fornecendo instâncias para tipos definidos pelo usuário, construir um serviço Rust observável de ponta a ponta requer uma compreensão de como os rastreamentos são gerados, como os dados são especificados e mantido e como a telemetria faz sentido à medida que seu aplicativo cresce. Isso é especialmente verdadeiro ao depurar e/ou explorar inconsistências, falhas parciais e características de desempenho questionáveis.

A coleta de traços orientará tudo nos exemplos deste artigo, onde intervalos e eventos serão as lentes através das quais uniremos uma imagem completa de uma quantidade conhecida. Teremos logs, mas os trataremos como eventos estruturados. Coletaremos métricas, mas automatizaremos com instrumentação e abrangência, e exportaremos dados de rastreamento compatíveis com OpenTelemetry para uma plataforma de rastreamento distribuída como Jaeger.

Escopo, eventos e rastreamento

Antes de entrar nos detalhes da implementação, vamos começar com alguns termos e conceitos com os quais precisamos estar familiarizados, como spans, rastreamentos e eventos.

entre

Um span representa uma operação ou segmento que pertence a um rastreamento e serve como o principal bloco de construção do rastreamento distribuído. Para qualquer solicitação, o intervalo inicial (sem pai) é chamado de intervalo raiz. Geralmente é expresso como a latência ponta a ponta de uma solicitação de usuário inteira, dado um rastreamento distribuído.

Também pode haver períodos filho subsequentes, que podem ser aninhados em outros períodos pai diferentes. O tempo total de execução de um intervalo inclui o tempo gasto nesse intervalo e toda a subárvore representada por seus filhos.

Aqui está um exemplo de log de span pai que é compactado intencionalmente para uma nova solicitação:

nível=INFO span name="solicitação 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=/músicas trace_id=b2b32ad7414392aedde4177572b3fea3

Este log de extensão contém informações e metadados importantes, como caminho de solicitação ( ), carimbo de data/hora ( ), método de solicitação ( ) e identificador de rastreamento ( , e ), respectivamente. Usaremos essas informações para demonstrar como unir os traços do início ao fim. http.route 2022-10-30T22:30:28.798564Zhttp.method spanparent_span``trace_id

Por que é chamado de span? Ben Sigelman, autor do documento de infraestrutura de rastreamento Dapper do Google, considera estes fatores em uma breve história do The Span: Difícil de amar, difícil de matar:

  • No próprio código, a API parece um cronômetro

  • Ao visualizar um traço como um gráfico direcionado, a estrutura de dados se parece com um nó ou vértice

  • No contexto do registro estruturado de vários processos (nota lateral: é disso que se trata o rastreamento distribuído no final do dia), pode-se pensar em spans como dois eventos

  • Dado um diagrama de tempo simples, é fácil chamar esse conceito de duração ou janela

evento

Um evento representa uma única operação no tempo, onde algo acontece durante a execução de algum programa arbitrário. Em contraste com o log não estruturado fora de banda, tratamos os eventos como a unidade principal de ingestão que ocorre no contexto de um determinado intervalo e são estruturados usando campos de valor-chave (semelhantes aos registros de intervalo acima). Mais precisamente, esses eventos são chamados de eventos span:

level=INFO msg="processamento concluído da solicitação do fornecedor" subject=vendor.response categoria=http.response vendor.status=200 vendor.response_headers="{\"content-type\": \"application/json\", \" variar\": \"Accept-Encoding, User-Agent\", \"transfer-encoding\": \"chunked\"}" vendor.url=http://localhost:8080/.well-known/jwks. json vendor.request_path=/.well-known/jwks.json target="application::middleware::logging" location="src/middleware/logging.rs:354" timestamp=2022-10-31T02:45:30.683888Z

Nosso aplicativo também pode ter eventos de log estruturados arbitrários que ocorrem fora do contexto de extensão. Por exemplo, exibindo definições de configuração na inicialização ou monitorando quando os caches são atualizados.

vestígio

Um rastreamento é uma coleção de intervalos que representam algum fluxo de trabalho, como uma solicitação do servidor ou uma etapa de processamento de fila/fluxo para um item. Essencialmente, um traço é um gráfico acíclico direcionado de extensões, onde as arestas que conectam as extensões indicam uma relação causal entre uma extensão e seu pai.

Aqui está um exemplo de rastreamento visualizado na IU do Jaeger:

Se esse aplicativo fizesse parte de um rastreamento distribuído maior, nós o veríamos aninhado em um intervalo pai maior.

Agora, com esses termos em mãos, como começamos a implementar o esqueleto de um aplicativo Rust pronto para observabilidade?

Combine várias camadas de rastreamento para construir assinantes

A estrutura de rastreamento é dividida em diferentes componentes (como caixas). Para nossos propósitos, focaremos neste conjunto de dependências: .toml

opentelemetry = { versão = "0.17", recursos = ["rt-tokio", "trace"] } 
opentelemetry-otlp = { versão = "0.10", recursos = ["métricas", "tokio", "tônico", " 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" 
assinante de rastreamento = {versão = "0,3", recursos = ["filtro de ambiente", "json", "registro"]}

Essa caixa nos permite gravar assinantes de rastreamento a partir de unidades menores de comportamento (chamadas camadas) para coletar e enriquecer dados de rastreamento. trace_subscriber

Ele próprio é responsável por registrar novos spans (com spans) na criação, registrar e anexar valores de campo e anotações subsequentes aos spans, e filtrar spans e eventos. ID do assinante

Quando combinada com um Assinante, a camada utiliza ganchos disparados durante todo o ciclo de vida do intervalo:

. }

Como as camadas do código são organizadas? Vamos começar com o método setup, gerando um registro definido por quatro combinadores ou camadas: com

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

Esta função geralmente é chamada quando o método do servidor é inicializado. A própria camada de armazenamento fornece saída zero, mas atua como um armazenamento de informações para coletar informações de rastreamento contextuais para aprimorar e estender a saída downstream de outras camadas no pipeline. setup_tracing main()principal.rs

Este método controla quais spans e eventos estão habilitados para esta camada, queremos capturar basicamente tudo, esta é a opção mais detalhada. with_filter``LevelFilter::TRACE

Vamos examinar cada camada e ver como cada camada opera nos dados de rastreamento que coleta e conecta ao ciclo de vida do span. A personalização do comportamento de cada camada envolve a implementação dos ganchos do ciclo de vida associados à característica, conforme mostrado abaixo. Camada``on_new_span

Ao longo do caminho, demonstraremos como essas unidades comportamentais podem aumentar os formatos de span e log de eventos, derivar automaticamente algumas métricas e enviar o que coletamos downstream para uma plataforma de rastreamento distribuída, como Jaeger, Honeycomb ou Datadog. Começaremos com o nosso, que fornece informações contextuais das quais outras camadas podem se beneficiar. Camada de armazenamento

Armazene informações de contexto para uso futuro

em novo período

impl<S> Layer<S> para StorageLayer 
onde 
    S: Subscriber + for<'span> LookupSpan<'span>, 
{ 
    fn on_new_span(&self, attrs: &Attributes<'_>, id: &Id, ctx: Context<'_ , S>) { 
        let span = ctx.span(id).expect("Span não encontrado"); 
        // Queremos herdar os campos do span pai, se houver. 
        deixe mut visitante = if let Some(parent_span) = span.parent() { 
            deixe mut extensions = parent_span.extensions_mut(); 
            deixe mut inner = extensions 
                .get_mut::<Storage>() 
                .map(|v| v.to_owned()) 
                .
                PARENT_SPAN, // "parent_span" 
                Cow::from(parent_span.id().into_u64().to_string()), 
            ); 
            inner 
        } else { 
            Storage::default() 
        }; 
​let
        mut extensions = span.extensions_mut(); 
        attrs.record(&mut visitante); 
        extensões.inserir(visitante); 
    } 
...

Ao iniciar um novo span (via), por exemplo, uma solicitação à aplicação para um endpoint, nosso código verifica se já estamos dentro do span pai. Caso contrário, o padrão será o vazio recém-criado, que é o que está oculto. on_new_span POST/songs HashmapArmazenamento::default()

Para simplificar, usamos como padrão chaves mapeadas para referências de string e valores para ponteiros inteligentes copy-on-write (Cow) em torno de referências de string:

#[derive(Clone, Debug, Default)] 
pub(crate) struct Storage<'a> { 
    valores: HashMap<&'a str, Cow<'a, str>>, 
}

Graças aos spans, o armazenamento entre camadas persiste em campos entre camadas durante a vida útil do span, permitindo-nos associar dados arbitrários a spans de forma mutável ou ler imutavelmente dados persistentes (incluindo nossas próprias estruturas de dados). extensões


Mais artigos excelentes da LogRocket:

  • Não perca um momento de repetição, aqui está um boletim informativo com curadoria da LogRocket

  • Saiba como o Galileo da LogRocket cancela ruídos e resolve proativamente problemas em seu aplicativo

  • Use useEffect do React para otimizar o desempenho do aplicativo

  • alternar entre várias versões do nó

  • Aprenda como usar adereços filhos React com TypeScript

  • Explore o uso de CSS para criar um cursor de mouse personalizado

  • Os conselhos consultivos não são apenas para executivos. Junte-se ao Conselho Consultivo de Conteúdo da LogRocket. Você ajudará a entender o tipo de conteúdo que criamos e terá acesso a encontros exclusivos, provas sociais e brindes.


Muitos desses ganchos do ciclo de vida envolvem luta livre, o que pode ser um pouco detalhado. O registro é o que realmente coleta e armazena dados de span, que podem então ser implementados implementando .extensions``LookupSpan

Outro código a destacar é registrar valores de campos de vários tipos visitando cada tipo de valor, que é uma característica obrigatória: attrs.record(&mut visitante)

// Apenas uma amostra dos métodos implementados 
impl Visit for Storage<'_> { 
    /// Visita um valor inteiro assinado de 64 bits. 
    fn record_i64(&mut self, campo: &Field, valor: i64) { 
        self.values 
            ​​.insert(field.name(), Cow::from(value.to_string())); 
    } 
    ... // elidido por questões de brevidade 
    fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) { 
        // Nota: isso é invocado via `debug!` e `info! macros 
        permitem debug_formatted = formato!("{:?}", valor); 
        self.values.insert(field.name(), Cow::from(debug_formatted));    
    } 
...

Depois de registrarmos todos os valores de cada tipo, o visitante armazenará todos esses valores em um armazenamento que poderá ser usado pelas camadas downstream para acionamentos do ciclo de vida no futuro. Mapa de hash

registrar em arquivo

impl<S> Layer<S> para StorageLayer 
onde 
    S: Subscriber + for<'span> LookupSpan<'span>, 
{ 
... // elidido por questões de brevidade 
    fn on_record(&self, span: &Id, valores: &Record<'_ >, ctx: Context<'_, S>) { 
        let span = ctx.span(span).expect("Span não encontrado"); 
        deixe extensões mut = span.extensions_mut(); 
        deixe visitante = extensões 
            .get_mut::<Armazenamento>() 
            .expect("Visitante não encontrado no 'registro'!"); 
        valores.registro(visitante); 
    } 
... // omitido por questão de brevidade

À medida que avançamos em cada um dos gatilhos do ciclo de vida, notaremos que os padrões são semelhantes. Obtemos um identificador de escopo mutável na extensão de armazenamento do span e registramos os valores conforme eles chegam.

Este gancho notifica a camada que um intervalo com o identificador fornecido registrou o valor fornecido por meio de uma chamada como: debug_span!``info_span!

deixe span = info_span!( 
    "vendor.cdbaby.task", 
    assunto = "vendor.cdbaby", 
    categoria = "vendor" 
);

evento

registrar(visitante); 
                } 
            }) 
        }); 
    }
... // omitido por questão de brevidade

Para nossa camada de armazenamento de contexto, geralmente não há necessidade de se conectar a eventos (como mensagens). No entanto, isso é valioso para armazenar informações sobre campos de eventos que queremos preservar, o que pode ser útil em outra camada posteriormente no pipeline. rastreamento::erro!

Um exemplo é armazenar eventos atribuídos a erros para que possamos rastrear erros na camada de métricas (por exemplo, uma matriz de campos vinculados à chave de erro). ON_EVENT_KEEP_FIELDS

ao entrar e fechar

impl<S> Layer<S> para StorageLayer 
onde 
    S: Subscriber + for<'span> LookupSpan<'span>, 
{ 
... // elidido por questões de brevidade 
    fn on_enter(&self, span: &Id, ctx: Context<'_ , S>) { 
        let span = ctx.span(span).expect("Span não encontrado"); 
        deixe extensões mut = span.extensions_mut(); 
        if extensions.get_mut::<Instant>().is_none() { 
            extensions.insert(Instant::now); 
        } 
    } 

    fn on_close(&self, id: Id, ctx: Context<'_, S>) { 
        let span = ctx.span(&id).expect("Span não encontrado"); 
        deixe mut extensões = span. extensões_mut(); 
        deixe elapsed_milliseconds = extensões
            .get_mut::<Instant>() 
            .map(|i| i.elapsed().as_millis()) 
            .unwrap_or(0); 

        deixe visitante = extensões 
            .get_mut::<Armazenamento>() 
            .expect("Visitante não encontrado no 'registro'"); 

        visitante.values.insert( 
            LATENCY_FIELD, // "latency_ms" 
            Cow::from(format!("{}", elapsed_milliseconds)), 
        ); 
    } 
... // omitido por questão de brevidade

Um período é essencialmente um intervalo de tempo marcado, com início e fim bem definidos. Para intervalos de spans, queremos capturar o tempo decorrido entre o momento em que uma entrada tem um determinado span() e o momento em que ela é fechada para uma determinada operação. id``Instantâneo::agora

Armazenar a latência de cada período em nossa extensão permite que outras camadas derivem métricas automaticamente e facilita os propósitos de exploração ao depurar o log de eventos para um determinado período. Abaixo, podemos ver a abertura e o fechamento do intervalo de tarefa/processo do provedor, que leva 18ms do início ao fim: id``id=452612587184455697

nível = INFO span_name = fornecedor.lastfm.task span = 452612587184455697 parent_span = span = 452612587184455696 span_event = new_span timestamp = 2022-10-31T12: 35: 36.913335Z trace_id = c53cb20e4ab4fa42 aa5836d26e974de2 http.client_ip=127.0.0.1:51029 subject=vendor.lastfm application.request_path=/músicas http.method=POST categoria=fornecedor http.host=127.0.0.1:3030 http.route=/músicas request_id=01GGQ0MJ94E24YYZ6FEXFPKVFP 
nível=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 subject=vendor.lastfm application.request_path=/songs http.method=POST categoria=fornecedor http .host=127.0.0.1:3030 http.route=/músicas request_id=01GGQ0MJ94E24YYZ6FEXFPKVFP

Aumente os logs estruturados com informações contextuais

Agora, veremos como os dados armazenados são aproveitados para a saída de telemetria real, observando a camada de formatação do log de eventos:

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

Muitos exemplos favorecem formatadores personalizados ao escrever implementações personalizadas de camada e assinante:

  • Um passo a passo on-line útil que mostra como criar um registrador JSON personalizado diferente daquele fornecido por padrão no trackbox

  • Formato Bunyan

  • Logfmt do Embark Studio

Observe que o exemplo de log acima usa o mesmo formato, inspirado na implementação do InfluxDB)

Recomendamos usar uma camada ou biblioteca publicada ou seguir os tutoriais listados acima para obter detalhes sobre como gerar os dados no formato de sua preferência.

Este artigo cria nossa própria camada de formatador personalizado e, neste artigo, nos familiarizaremos novamente com o ciclo de vida do span, especificamente os spans e os logs de eventos, e agora aproveitaremos nossos mapas de armazenamento.

em novo período

impl<S, Wr, W> Camada<S> para LogFmtLayer<Wr, W> 
onde 
    Wr: Write + 'estático, 
    W: for<'writer> MakeWriter<'writer> + 'estático, 
    S: Assinante + for<' span> LookupSpan<'span>, 
{ 
    fn on_new_span(&self, _attrs: &Attributes<'_>, id: &Id, ctx: Context<'_, S>) { let mut p = self.printer.write() 
        ; 
        deixe metadados = ctx.metadata(id).expect("Expandir metadados ausentes"); 
        p.write_level(metadata.level()); 
        p.write_span_name(metadata.nome()); 
        write_span_id(id); 
        p.write_span_event("novo_span"); 
        pág. write_timestamp(); 

        deixe span = ctx.span(id).expect("Span não encontrado");
        deixe extensões = span.extensions(); 
        if let Some(visitor) = extensions.get::<Storage>() { 
            for (key, value) in visitante.values() {                
                p.write_kv( 
                    decorar_field_name(translate_field_name(key)), 
                    value.to_string(), 
                ) 
            } 
        } 
        p.write_newline(); 
    } 
... // omitido por questão de brevidade

O código acima imprime uma representação de texto formatada do evento span usando a característica. A chamada para a impressora e todos os métodos de impressora executam os atributos de formatação específicos nos bastidores (neste caso, novamente). MakeWriter decorate_field_nameescreve``logfmt

Voltando ao nosso exemplo anterior de span log, chaves como , , e agora estão definidas de forma mais clara. O trecho de código a ser chamado aqui é como fazemos um loop sobre os valores lidos no mapa de armazenamento, promovendo as informações que observamos e coletamos na camada anterior. nível spanspan_name``para (chave, valor)

Usamos isso para fornecer contexto para aumentar eventos de log estruturados em outra camada. Em outras palavras, compomos subcomportamentos específicos nos dados de rastreamento por meio de camadas para construir um único assinante para todo o rastreamento. Por exemplo, chaves de campo like e remove desta camada de armazenamento. http.route``http.host

evento

impl<S, Wr, W> Camada<S> para LogFmtLayer<Wr, W> 
onde 
    Wr: Write + 'estático, 
    W: for<'writer> MakeWriter<'writer> + 'estático, 
    S: Assinante + for<' span> LookupSpan<'span>, 
{ 
... // elidido por questões de brevidade 
    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); 
        //grava informações da fonte 
        p.write_source_info(event); 
        p.write_timestamp(); 

        ctx.lookup_current().map(|current_span| { 
            p.
            deixe extensões = current_span.extensions(); 
            extensions.get::<Armazenamento>().map(|visitor| { 
                for (chave, valor) em visitante.values() { 
                    if !ON_EVENT_SKIP_FIELDS.contains(key) { 
                        p.write_kv( 
                            decorar_field_name(translate_field_name(key)) , 
                            valor.to_string(), 
                        ) 
                    } 
                } 
            }) 
        }); 
        p.write_newline(); 
    } 
... // omitido por questão de brevidade

Embora um tanto tediosos, os padrões para implementar esses métodos de ciclo de vida de extensão estão ficando mais fáceis de entender. Pares de valores-chave de campo, como destino e localização, são formatados de acordo com as informações de origem, fornecendo-nos o que vimos anteriormente. As chaves curtidas também são obtidas no armazenamento de contexto. target="application::middleware::logging" location="src/middleware/logging.rs:354"vendor.request_path``vendor.url

Embora possa ser necessário mais trabalho para implementar corretamente quaisquer especificações de formatação, agora podemos ver o controle e a personalização refinados que a estrutura de rastreamento fornece. Essas informações contextuais são como podemos formar dependências dentro do ciclo de vida da solicitação.

Derivando métricas por detecção e duração do intervalo

As métricas, em particular, são na verdade muito ruins para a observabilidade em si, e a cardinalidade das métricas, o número de combinações exclusivas de nomes de métricas e valores de dimensão, pode ser facilmente abusada.

em branco

Mostramos como derivar logs estruturados de eventos. Os próprios indicadores devem ser formados a partir dos eventos ou períodos que os contêm.

Ainda precisamos de métricas fora de banda, como aquelas coletadas em torno de processos (por exemplo, uso de CPU, bytes de disco gravados/lidos). Mas se já podemos instrumentar o código no nível da função para determinar quando certas coisas acontecem, algumas métricas não deveriam “cair de graça”? Conforme mencionado, temos as ferramentas, mas só precisamos implementá-las.

O rastreamento fornece uma maneira acessível de anotar funções que o usuário deseja instrumentar, o que significa criar, inserir e fechar escopos sempre que uma função anotada é executada. O próprio compilador Rust faz uso intenso desses instrumentos anotados em toda a base de código:

#[instrument(skip(self, op), level = "trace")] 
pub(super) fn full_perform_op<R: fmt::Debug, Op>( 
    &mut self, 
    locais: Locais, 
    categoria: ConstraintCategory<'tcx>, 
    op: Op) -> Falível<R>

Para nossos propósitos, vejamos uma função de banco de dados assíncrona simples instrumentada com algumas definições de campo muito específicas: save_event

#[instrument( 
    level = "info", 
    name = "record.save_event", 
    skip_all, 
    campos(category="db", subject="aws_db", event_id = %event.event_id, 
           event_type=%event.event_type, otel. kind="client", db.system="aws_db", 
           metric_name="db_event", metric_label_event_table=%self.event_table_name, 
           metric_label_event_type=%event.event_type) 
        err(Display) 
)] 
pub async fn save_event(&self, event: &Event ) -> de qualquer maneira::Result<()> { 
    self.db_client 
        .put_item() 
        .table_name(&self.event_table_name) 
        .set(Some(event)) 
        .enviar() 
        .aguardar... 
}

Nossas funções de detecção possuem campos de prefixo como , e . Essas chaves correspondem a nomes e rótulos de métricas normalmente encontrados nas configurações de monitoramento do Prometheus. Voltaremos a esses campos de prefixo mais tarde. Primeiro, vamos estender o filtro que configuramos inicialmente com alguns filtros adicionais. métrica nameevent_type event_tableMetricsLayer

Essencialmente, esses filtros fazem duas coisas: 1) geram métricas para todos os eventos no nível de log de rastreamento ou superior (mesmo que eles não possam registrar no stdout dependendo do nível de log configurado); 2) passam a função de instrumentação com um prefixo adicional de eventos, como descrito acima. registro``nome = "registro. save_event"

Depois disso, resta retornar à implementação da nossa camada de indicadores para realizar automaticamente a derivação do indicador.

quando desligado

const PREFIX_LABEL: &str = "metric_label_"; 
const METRIC_NAME: &str = "nome_métrico"; 
const OK: &str = "ok"; 
const ERRO: &str = "erro"; 
const LABEL: &str = "rótulo"; 
const RESULT_LABEL: &str = "resultado"; 

impl<S> Camada<S> para MetricsLayer 
onde 
    S: Assinante + for<'span> LookupSpan<'span>, 
{ 
    fn on_close(&self, id: Id, ctx: Context<'_, S>) { 
        let span = ctx.span(&id).expect("Span não encontrado"); 
        deixe extensões mut = span.extensions_mut(); 
        deixe elapsed_secs_f64 = extensões 
            .get_mut::
        if let Some(visitor) = extensions.get_mut::<Storage>() { 
            let mut rótulos = vec![]; 
            for (chave, valor) em visitante.values() { 
                if key.starts_with(PREFIX_LABEL) { 
                    rótulos.push(( 
                        key.strip_prefix(PREFIX_LABEL).unwrap_or(LABEL), 
                        value.to_string(), 
                    )) 
                } 
            } 
            .. . // elidido por questão de brevidade 
            let name = visitante 
                .values() 
                .get(METRIC_NAME) 
                .unwrap_or(&Cow::from(span_name)) 
                .to_string();
            if visitante.valores().contains_key(ERROR) 
                rótulos.push((RESULT_LABEL, String::from(ERROR))) 
            } else { 
                rótulos.push((RESULT_LABEL, String::from(OK))) 
            } 
            ... // elidido por questão de brevidade 
            métricas::increment_counter!(format!("{}_total", name), &labels); 
            métricas::histograma!( 
                formato!("{}_duration_seconds", nome), 
                elapsed_secs_f64, 
                &labels 
            ); 
            ... // omitido por questão de brevidade

Há muitos bits sendo usados ​​neste exemplo, alguns dos quais são omitidos. No entanto, sempre podemos conduzir nossos cálculos de histograma por meio de macros, acessando o final do intervalo de span. on_close elapsed_secs_f64métricas::histograma!

Observe que aproveitamos o projeto métricas-rs aqui. Qualquer pessoa pode modelar esta função da mesma maneira usando outra biblioteca de indicadores que fornece suporte para contador e histograma. Do mapa de armazenamento, extraímos todas as chaves rotuladas e usamos essas chaves para gerar rótulos para contadores e histogramas incrementados derivados automaticamente. métrica_*

Além disso, se armazenarmos um evento com erro, podemos usá-lo como parte do rótulo, com base em/para diferenciar nossas funções geradas. Dada qualquer função instrumentada, usaremos o mesmo comportamento de código para derivar métricas dela. ok``erro

A saída que encontramos do endpoint do Prometheus mostrará um contador semelhante a este:

db_event_total{event_table="events",event_type="Song",result="ok",span_name="save_event\"} 8

Detecte fechamentos assíncronos e relacionamentos de span indiretos

Uma questão que surge de vez em quando é como detectar código que faz referência a extensões indiretas, relacionamentos não-pai-filho, ou os chamados seguimentos de referências.

Isto aplicar-se-á a operações assíncronas que geram pedidos a serviços a jusante para efeitos secundários ou processos que enviam dados para Service Bus onde a resposta direta ou saída devolvida não é útil na operação que a gerou.

Para esses casos, podemos instrumentar diretamente fechamentos assíncronos (ou futuros) entrando em um determinado intervalo (capturado abaixo para referência) associado ao nosso futuro assíncrono toda vez que pesquisamos e saímos do futuro, como segue: follow_from``.instrument(process_span )

// Inicia um intervalo em torno do processo de geração de contexto 
let process_span = debug_span!( 
    parent: None, 
    "process.async", 
    subject = "songs.async", 
    categoria = "songs" 
); 
process_span.follows_from(Span::current()); 

tokio::spawn( 
    movimento assíncrono { 
        match context.process().await { 
            Ok(r) => debug!(song=?r, "processado com sucesso"), 
            Err(e) => avisar!(error=?e , "falha no processamento"), 
        } 
    } 
    .instrument(process_span), 
);

Interoperabilidade de telemetria aberta para rastreamento distribuído

Grande parte da utilidade da observabilidade vem do fato de que a maioria dos serviços atuais são, na verdade, compostos de muitos microsserviços. Todos nós deveríamos espalhar nossas mentes.

Se vários serviços precisarem se interconectar entre redes, provedores, nuvens e até mesmo pares orientados para borda ou locais, algumas ferramentas padrão e independentes de fornecedor deverão ser aplicadas. É aqui que o OpenTelemetry (OTel) entra em ação, e muitas plataformas de observabilidade conhecidas ficam mais do que satisfeitas em ingerir dados de telemetria compatíveis com OTel.

Embora exista um conjunto completo de ferramentas Rust de código aberto que funcionam dentro do ecossistema OTel, muitas estruturas Web Rust conhecidas ainda não incorporaram o padrão OTel de forma integrada.

Estruturas web populares que abrangem axum, como Actix e Tokio, dependem de implementações personalizadas e bibliotecas externas para fornecer integração (actix-web-opentelemetry e axum-tracing-opentelemetry, respectivamente). As integrações de terceiros têm sido de longe a opção mais popular e, embora promovam flexibilidade e controle do usuário, podem tornar mais difícil para quem deseja adicionar integrações de forma quase perfeita.

Não entraremos em detalhes sobre como escrever uma implementação personalizada aqui, mas o middleware HTTP canônico como o Tower permite substituir a implementação padrão dos intervalos de criação de solicitação. Se implementado de acordo com a especificação, os seguintes campos deverão ser definidos nos metadados do span:

  • http.client_ip: endereço IP do cliente

  • http.flavor: A versão do protocolo usada (HTTP/1.1, HTTP/2.0, etc.)

  • http.host: o valor do cabeçalho Host

  • http.method: método de solicitação

  • http.route: a rota correspondente

  • http.request_content_length: comprimento do conteúdo da solicitação

  • http.response_content_length: comprimento do conteúdo da resposta

  • http.scheme: esquema de URI a ser usado (ou HTTP``HTTPS)

  • http.status_code: código de status de resposta

  • http.target: o destino completo da solicitação, incluindo caminho e parâmetros de consulta

  • http.user_agent: o valor do cabeçalho User-Agent

  • otel.kind: Em geral, encontre mais informações aqui servidor

  • otel.name: Um nome que consiste em http.method``http.route

  • otel.status_code: se a resposta for bem-sucedida; se for 5xxOK``ERROR

  • trace_id: um identificador para um rastreamento, usado entre processos para agrupar todos os trechos de um rastreamento específico

Inicializar rastreador

Rastreando e expondo outra camada que podemos usar para compor nossos assinantes para adicionar informações de contexto OTel a todos os spans e concatenar e emitir esses spans para uma plataforma de observabilidade como Datadog ou Honeycomb, ou diretamente para uma instância Jaeger ou Tempo em execução, que amostra o rastreamento dados para consumo gerenciável. tracing-opentelemetry``rust-opentelemetry

Inicializar um para gerar e gerenciar spans é fácil: Tracer

pub fn init_tracer(configurações: &Otel) -> Result<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(VERSION), 
        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); 
deixe
    trace = opentelemetry_otlp::new_pipeline()
        .tracing() 
        .with_exporter(exportador(mapa, endpoint?)) 
        .with_trace_config(sdk::trace::config().with_resource(resource)) 
        .install_batch(runtime::Tokio) 
        .map_err(|e| de qualquer maneira!( "falha ao inicializar o rastreador: {:#?}", e))?; 
​Ok
    (trace) 
}

Também é trivial incluir em nosso pipeline de camadas. Também podemos filtrar por nível e usar filtros dinâmicos para pular eventos que queremos evitar no rastreamento:

.with( 
    tracing_opentelemetry::layer() 
        .with_tracer(tracer) 
        .with_filter(LevelFilter::DEBUG) 
        .with_filter(dynamic_filter_fn(|_metadata, ctx| { 
            !ctx.lookup_current() 
                // Excluir os eventos "Connection" da sessão Rustls 
                / / que não tem um span pai 
                .map(|s| s.parent().is_none() && s.name() == "Connection") 
                .unwrap_or_default() 
        })), 
)

Com essa inicialização do pipeline, todos os rastreamentos do nosso aplicativo podem ser obtidos por ferramentas como o Jaeger, conforme mostrado anteriormente neste artigo. Então, tudo o que resta é associação, divisão e divisão de dados.

para concluir

Ao combinar essas camadas de rastreamento, podemos expor informações sobre o comportamento do sistema de maneira granular e granular, ao mesmo tempo em que obtemos saída e contexto suficientes para começar a entender esse comportamento. Toda essa personalização ainda tem um preço: não é totalmente automática, mas o padrão é idiomático e há muitos casos de uso normais para usar a camada de código aberto.

Em todos os aspectos, esta postagem deve ajudar a tornar mais fácil para os usuários experimentarem o uso de rastreamentos para coletar interações personalizadas de aplicativos e demonstrar até onde podemos ir na preparação de nossos aplicativos para lidar com o inevitável. Este é apenas o começo de nossa bela amizade com os acontecimentos e quando eles ocorrem, daí a observabilidade. A maneira como depuramos e resolvemos problemas no longo prazo é sempre um trabalho contínuo.

Acho que você gosta

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