Rastreo distribuido para funciones

Descubra cómo activar el rastreo y ver rastreos de funciones al depurar con OCI Functions.

Cuando se llama a una función pero no se ejecuta o se ejecuta como se esperaba, debe investigar el problema en un nivel detallado. La función de rastreo distribuido observa la ejecución de la función a medida que se mueve a través de los diferentes componentes del sistema. Puede rastrear e instrumentar funciones independientes para depurar problemas de ejecución y rendimiento. También puede utilizar el rastreo de funciones para depurar problemas con aplicaciones sin servidor completas que incluyen varias funciones y servicios, como:

  • una función que llama a otra función
  • una función que llama a otros servicios, como el servicio Object Storage
  • una función que sirve como backend para un gateway de API desplegado en el servicio de gateway de API
  • una función disparada en respuesta a un evento por el servicio Events, el servicio Notifications o Connector Hub

Las capacidades de rastreo de OCI Functions las proporciona el servicio Oracle Cloud Infrastructure Application Performance Monitoring. Las funciones de Application Performance Monitoring (APM) permiten identificar y solucionar fallos y problemas de latencia en las funciones que cree y despliegue.

En el servicio Application Performance Monitoring:

  • Un dominio de APM contiene los sistemas supervisados por Application Performance Monitoring. Un dominio de APM es una instancia de un recopilador de datos de rastreo y período que almacena, agrega, muestra y visualiza los datos.
  • Un rastreo es el flujo completo de una solicitud que pasa por todos los componentes de un sistema distribuido en un período de tiempo determinado. Consta de un árbol completo de intervalos, todos relacionados con el mismo flujo de solicitud global único.
  • Un intervalo es una operación o una unidad lógica de trabajo con un nombre, una hora de inicio y una duración, dentro de un rastreo. Un intervalo es un segmento de tiempo asociado con la duración de una unidad de trabajo dentro del flujo de solicitud general.

El explorador de rastreo de Application Performance Monitoring permite visualizar todo el flujo de solicitudes y explorar los detalles de rastreo y período para el diagnóstico. Puede ver y supervisar rastreos lentos y rastreos con errores. Para aislar e identificar incidencias de rastreo, puede aumentar el detalle de períodos específicos, como cargas de páginas, llamadas AJAX y solicitudes de servicio. Para obtener más información sobre el servicio Application Performance Monitoring, consulte Application Performance Monitoring.

Para activar el rastreo de una función, debe:

  1. Configure una política para otorgar permiso al servicio OCI Functions para acceder a los dominios de APM, si la política aún no existe (consulte Sentencias de política para otorgar acceso al servicio OCI Functions y a los usuarios de OCI Functions a los recursos de rastreo).
  2. Configure un dominio de APM.
  3. Active el rastreo para la aplicación Functions y seleccione el dominio de APM que ha creado.
  4. Active el rastreo para una o más funciones.

Al activar el rastreo para una función, OCI Functions genera automáticamente un "plazo de llamada de función por defecto". El intervalo por defecto captura información sobre el contexto de ejecución de la función, incluido el tiempo total que se tarda en procesar la solicitud y devolver una respuesta al emisor de la llamada. Además del período de llamada de función por defecto, puede agregar código a las funciones para definir períodos personalizados. Utilice intervalos personalizados para capturar más información específica de la función para ayudar con la depuración. Por ejemplo, puede definir intervalos personalizados para capturar el inicio y el final de unidades de trabajo específicas. Por ejemplo, las unidades de trabajo podrían incluir la obtención de la contraseña de base de datos del almacén, la apertura de una conexión a base de datos y la recuperación de registros de la base de datos.

Se han agregado cuatro variables al contexto de OCI Functions que proporcionan información de rastreo útil. Estas variables incluyen:

  • FN_APP_NAME: Nombre de la aplicación de la función.
  • FN_FN_NAME: Nombre de función.
  • OCI_TRACE_COLLECTOR_URL: URL de dominio de APM con clave de datos.
  • OCI_TRACING_ENABLED: ¿Está activado el rastreo?
    • Cuando se recupera de variables de entorno, devuelve 0 o 1.
    • Cuando se recupera del contexto de la función, devuelve true o false según corresponda para el idioma utilizado.

Política de IAM necesaria para activar el rastreo

Para poder activar el rastreo, el grupo al que pertenece debe tener permiso para acceder a los dominios de APM existentes o para crear dominios de APM. Además, OCI Functions debe tener permiso para acceder a los dominios de APM. Consulte Sentencias de política para proporcionar a los usuarios del servicio OCI Functions y OCI Functions acceso a recursos de rastreo.

Uso de la consola para activar el rastreo y ver rastreos de funciones

Se necesitan un par de pasos para activar el rastreo y ver los rastreos de funciones para el servicio Oracle Cloud Infrastructure Application Performance Monitoring (APM). En primer lugar, active el rastreo para la aplicación que contiene la función. A continuación, active el rastreo para una o más funciones. A continuación, puede ver los rastreos de funciones en el explorador de rastreo de APM.

Uso de la consola para activar el rastreo

Para activar el rastreo, siga estos pasos.

  1. En la página de lista Aplicaciones, seleccione la aplicación con funciones para las que desea activar el rastreo. Si necesita ayuda para buscar la página de lista o la aplicación, consulte Listado de aplicaciones.
  2. Seleccione el separador Supervisión y vaya a la sección Rastreos.
  3. Para activar el rastreo para la aplicación:
    1. En el menú Acciones (tres puntos), seleccione Configurar y especifique:
      • compartimento: compartimento en el que se va a crear el rastreo. Por defecto, el compartimento actual.
      • Dominio de APM: dominio de APM (definido en el servicio Application Performance Monitoring) en el que crear el rastreo. Para utilizar un dominio de APM existente, seleccione un dominio de APM existente de la lista. O bien, para crear un nuevo dominio de APM, seleccione APM Domain (Dominio de APM). Para obtener más información sobre los dominios de APM, consulte Introducción a Application Performance Monitoring.
        Nota

        El dominio de APM debe tener claves de datos públicas y privadas para que el rastreo de funciones funcione. Si las claves no existen, puede crearlas a través de la interfaz de la consola.
    2. Seleccione Activar Rastreo para activar el rastreo para la aplicación.

    Una vez activado el rastreo para la aplicación de Functions, ahora puede activar el rastreo para una o más funciones de la aplicación.

  4. Para activar el rastreo para funciones específicas en la aplicación:
    1. Seleccione el separador Funciones.
    2. Seleccione la opción Activar rastreo en el menú Acciones (tres puntos) de las funciones para las que desea activar el rastreo.

      La opción Activar Rastreo sólo se muestra si ha activado previamente el rastreo para la aplicación. Tenga en cuenta lo siguiente:

      • Si no se muestra la opción Activar rastreo, debe activar el rastreo para la aplicación. Si aún no ha activado el rastreo para la aplicación, consulte el paso anterior.
      • Si activó previamente el rastreo para la aplicación, pero posteriormente lo desactivó, se muestra un enlace Activar rastreo de aplicación. Seleccione el enlace Activar Rastreo de Aplicación para volver a activar el rastreo de la aplicación (consulte el paso anterior). Después de volver a activar el rastreo para la aplicación, puede activar el rastreo para funciones específicas.

Cuando haya activado el rastreo para la aplicación y una o más funciones, puede ver los rastreos de funciones.

Uso de la consola para ver rastreos de funciones

Para ver los rastreos de las funciones que tienen el rastreo activado:

  1. En la página de lista Aplicaciones, seleccione la aplicación que contiene las funciones para las que desea ver rastreos. Si necesita ayuda para buscar la página de lista o la aplicación, consulte Listado de aplicaciones.
  2. Para ver rastreos de funciones:
    1. Para ver los rastreos de todas las funciones que tienen el rastreo activado en la aplicación:
      1. Seleccione el separador Supervisión y vaya a la sección Rastreos.
      2. Seleccione el nombre del rastreo.
        Nota

        Un nombre de rastreo solo se muestra si ya ha activado el rastreo para la aplicación.
    2. Para ver el rastreo de una función específica que tiene el rastreo activado:
      1. Seleccione el separador Funciones.
      2. Seleccione la opción Ver rastreo en el menú Acciones (tres puntos) de la función para la que desea ver el rastreo.
        Nota

        La opción Ver rastreo solo se muestra si ya ha activado el rastreo para la función.

    Los rastreos de las funciones seleccionadas se muestran en el explorador de rastreo de APM. Por defecto, se muestra un rastreo para el intervalo de llamada a la función por defecto y cualquier intervalo personalizado definido para la función.

  3. En el explorador de rastreo de APM:
    1. Seleccione un rastreo para ver los intervalos de ese rastreo.
    2. Seleccione un período para ver los detalles capturados para ese período.

    Para obtener más información sobre el uso del explorador de rastreo de APM, consulte Uso del explorador de rastreo.

Rastreo de una Cadena de Funciones

Por defecto, el rastreo de funciones proporciona un rastreo para una llamada de función completa. Sin embargo, a menudo con las aplicaciones modernas en la nube, necesita encadenar llamadas a funciones. El rastreo de OCI Functions proporciona la capacidad de rastrear la ejecución de una función llamada por otra función. Esta capacidad significa que puede examinar la ejecución de cada función en una cadena de llamadas en un solo árbol de intervalos en el explorador de rastreo de APM.

Para rastrear una cadena de funciones, debe propagar las cabeceras X-B3 X-B3-TraceId, X-B3-SpanId, X-B3-ParentSpanId y X-B3-Sampled en la solicitud de llamada de función desde el código de función.

Una vez ejecutada la función, los datos de rastreo de las funciones se recopilan y están disponibles en el explorador de rastreo de APM. Para obtener más información sobre el uso del explorador de rastreo de APM, consulte Uso del explorador de rastreo.

Rastreo de una cadena de funciones con Python

A continuación, se muestra un ejemplo de cómo puede rastrear una cadena de funciones. Si desea probar este ejemplo, debe crear dos funciones de ejemplo. Siga estos pasos para configurar las funciones.

  1. Cree la función de rastreo de Python: fn init --runtime python <your-function-name-1>
  2. ¡Crea tu "Hello World!" Función Python: fn init --runtime python <your-function-name-2>
  3. Despliegue ambas funciones: fn -v deploy --app <app-name>
  4. Obtenga el OCID de la segunda función y llame al punto final: fn inspect function your-app-name your-function-name-2
  5. Cree un archivo JSON para transferir la información necesaria a la primera función. Por ejemplo, el archivo test.json podría tener el siguiente aspecto:
    
    {
        "function_ocid": "ocid1.fnfunc.oc1.iad.aaaaaaaaxxxxxxxxxxx",
        "function_endpoint": "https://xxxxxxxxx.us-ashburn-1.functions.oci.oraclecloud.com",
        "function_body": "",
        "__comment": "Alternatively, you can set function_body to { \"name\": \"Oracle\" }"
    }                    
                    
  6. Cuando se llama a la primera función, puede transferir la información de la segunda función mediante test.json: fn invoke <app-name> <your-function-name-1> < test.json

Ahora está listo para actualizar la primera función con las actualizaciones de código necesarias.

Configurar paquetes

Actualice el archivo requirements.txt para incluir los siguientes paquetes:


fdk
oci
            

Guardar el archivo.

Actualización del código de función para propagar las cabeceras X-B3

La función Python llama a la función handler y transfiere la información de JSON desde el comando de llamada. La función handler se divide en varios bloques pequeños para simplificarla. El archivo de origen completo se proporciona en la parte inferior de esta sección.

Cargar los datos de JSON

En esta primera parte, los datos de JSON se cargan desde la llamada a la función.


import io
import json
import logging
import oci
from fdk import response

def handler(ctx, data: io.BytesIO=None):
    app_name = ctx.AppName()
    func_name = ctx.FnName()
    logging.getLogger().info("Inside app: " + app_name + " | function: " + func_name + " | method: handler")

    try:
        body = json.loads(data.getvalue())
        function_endpoint = body.get("function_endpoint")
        function_ocid = body.get("function_ocid")
        function_body = body.get("function_body")
    except (Exception) as ex:
        print('ERROR: Missing key in payload', ex, flush=True)
        raise
            

Creación de Llamada de Cliente y Recopilación de Información de Cabecera

Cree el cliente de llamada de Functions mediante el SDK de Python de OCI y los principales de recursos de Functions. A continuación, recupere tracing_context y extraiga la información necesaria para crear las cabeceras HTTP.


    signer = oci.auth.signers.get_resource_principals_signer()
    client = oci.functions.FunctionsInvokeClient(config={}, signer=signer, service_endpoint=function_endpoint)
    
    #
    # Zipkin X-B3- header propagation
    #
    tracing_context = ctx.TracingContext()
    trace_id = tracing_context.trace_id()
    span_id = tracing_context.span_id()
    parent_span_id = tracing_context.parent_span_id()
    is_sampled = tracing_context.is_sampled()
            

Propagar las cabeceras X-B3

El SDK de Python de OCI le permite definir cabeceras personalizadas. Utilice esta técnica para transferir las cabeceras X-B3 a la segunda llamada de función. La información de cabecera se transfiere para trace_id, span_id, parent_span_id y is_sampled. Por último, se llama a la segunda función con client y la respuesta se transfiere a la respuesta de esta función.


    # if tracing is enabled, is_sampled will be true in the tracing context
    if is_sampled:
        # To propagate headers in the OCI SDK in the request to the next function,
        # add the X-B3- headers in the request. This header will be included in ALL
        # subsequent calls made.
        if trace_id is not None:
            client.base_client.session.headers['X-B3-TraceId'] = trace_id
            logging.getLogger().info("Inside app: " + app_name + " | function: " + func_name + " | method: handler | trace_id: " + trace_id)
        if span_id is not None:
            client.base_client.session.headers['X-B3-SpanId'] = span_id
            logging.getLogger().info("Inside app: " + app_name + " | function: " + func_name + " | method: handler | span_id: " + span_id)
        if parent_span_id is not None:
            client.base_client.session.headers['X-B3-ParentSpanId'] = parent_span_id
            logging.getLogger().info("Inside app: " + app_name + " | function: " + func_name + " | method: handler | parent_span_id: " + parent_span_id)
        client.base_client.session.headers['X-B3-Sampled'] = str(int(is_sampled))
        logging.getLogger().info("Inside app: " + app_name + " | function: " + func_name + " | method: handler | is_sampled: " + str(int(is_sampled)))
    else:
        # function.trace is DISABLED
        logging.getLogger().info("Inside app: " + app_name + " | function: " + func_name + " | method: handler | function tracing is DISABLED")

    resp = client.invoke_function(function_id=function_ocid, invoke_function_body=function_body)
    logging.getLogger().info("Inside app: " + app_name + " | function: " + func_name + " | method: handler | Response: " + resp.data.text)

    return response.Response(
        ctx, 
        response_data=resp.data.text,
        headers={"Content-Type": "application/json"}
    )
            
Revisar código de origen de función finalizada

Este es el código fuente completo para la función de Python de ejemplo.


#
# oci-invoke-function-python version 2.0.
#
# Copyright (c) 2021 Oracle, Inc.
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
#

import io
import json
import logging
import oci
from fdk import response

def handler(ctx, data: io.BytesIO=None):
    app_name = ctx.AppName()
    func_name = ctx.FnName()
    logging.getLogger().info("Inside app: " + app_name + " | function: " + func_name + " | method: handler")

    try:
        body = json.loads(data.getvalue())
        function_endpoint = body.get("function_endpoint")
        function_ocid = body.get("function_ocid")
        function_body = body.get("function_body")
    except (Exception) as ex:
        print('ERROR: Missing key in payload', ex, flush=True)
        raise
    
    signer = oci.auth.signers.get_resource_principals_signer()
    client = oci.functions.FunctionsInvokeClient(config={}, signer=signer, service_endpoint=function_endpoint)
    
    #
    # Zipkin X-B3- header propagation
    #
    tracing_context = ctx.TracingContext()
    trace_id = tracing_context.trace_id()
    span_id = tracing_context.span_id()
    parent_span_id = tracing_context.parent_span_id()
    is_sampled = tracing_context.is_sampled()

    # if tracing is enabled, is_sampled will be true in the tracing context
    if is_sampled:
        # To propagate headers in the OCI SDK in the request to the next function,
        # add the X-B3- headers in the request. This header will be included in ALL
        # subsequent calls made.
        if trace_id is not None:
            client.base_client.session.headers['X-B3-TraceId'] = trace_id
            logging.getLogger().info("Inside app: " + app_name + " | function: " + func_name + " | method: handler | trace_id: " + trace_id)
        if span_id is not None:
            client.base_client.session.headers['X-B3-SpanId'] = span_id
            logging.getLogger().info("Inside app: " + app_name + " | function: " + func_name + " | method: handler | span_id: " + span_id)
        if parent_span_id is not None:
            client.base_client.session.headers['X-B3-ParentSpanId'] = parent_span_id
            logging.getLogger().info("Inside app: " + app_name + " | function: " + func_name + " | method: handler | parent_span_id: " + parent_span_id)
        client.base_client.session.headers['X-B3-Sampled'] = str(int(is_sampled))
        logging.getLogger().info("Inside app: " + app_name + " | function: " + func_name + " | method: handler | is_sampled: " + str(int(is_sampled)))
    else:
        # function.trace is DISABLED
        logging.getLogger().info("Inside app: " + app_name + " | function: " + func_name + " | method: handler | function tracing is DISABLED")

    resp = client.invoke_function(function_id=function_ocid, invoke_function_body=function_body)
    logging.getLogger().info("Inside app: " + app_name + " | function: " + func_name + " | method: handler | Response: " + resp.data.text)

    return response.Response(
        ctx, 
        response_data=resp.data.text,
        headers={"Content-Type": "application/json"}
    )
            

Adición de períodos personalizados a funciones

Con el rastreo de funciones activado, el período de llamada a la función por defecto proporciona un rastreo para toda la llamada a la función. El intervalo por defecto puede proporcionar buena información, pero al investigar el código puede que desee profundizar más. Los intervalos personalizados se agregan directamente al código y le permiten definir intervalos para un método o un bloque de código. Los datos resultantes proporcionan una mejor imagen de su función a medida que se ejecuta.

Para poder utilizar períodos personalizados, debe activar el rastreo para la aplicación y las funciones mediante el servicio Oracle Cloud Infrastructure Application Performance Monitoring (APM). Para definir el rastreo, debe:

  1. Configure una política para otorgar permiso al servicio OCI Functions para acceder a los dominios de APM, si la política aún no existe (consulte Sentencias de política para otorgar acceso al servicio OCI Functions y a los usuarios de OCI Functions a los recursos de rastreo).
  2. Configure un dominio de APM.
  3. Active el rastreo para la aplicación Functions y seleccione el dominio de APM que ha creado.
  4. Active el rastreo para una o más funciones.

Estos pasos ya se han tratado. Sin embargo, se necesitan un par de cosas más para intervalos personalizados:

  • Seleccione una biblioteca de cliente de rastreo distribuido, por ejemplo Zipkin.
  • Agregue bibliotecas de cliente a las dependencias de función.
  • En el código de función, utilice la variable de contexto de función OCI_TRACING_ENABLED para comprobar si el rastreo está activado.
  • En el código de función, utilice la variable de contexto de función OCI_TRACE_COLLECTOR_URL para enviar los períodos personalizados al dominio de APM.
  • Agregue instrumentación al código de función.
Nota

Para utilizar intervalos personalizados, debe tener las siguientes versiones mínimas de los FDK de Fn Project:

  • FDK DE Java: 1.0.129
  • FDK de Python: 0.1.22
  • Nodo FDK: 0.0.20
Adición de períodos personalizados a funciones Java

A continuación, se muestra un ejemplo de cómo utilizar Zipkin para agregar períodos personalizados a la función Java. Si desea probar este ejemplo, puede crear una función "Hello World!" de Java y agregar código de período personalizado. Para crear una función de ejemplo:

  • Cree una función Java: fn init --runtime java apm-fn-java
  • Para simplificar, elimine el directorio src/test.

Configurar Maven

Agregue las siguientes dependencias a la sección <dependencies> del archivo pom.xml de Maven.


<dependency>
    <groupId>io.zipkin.reporter2</groupId>
    <artifactId>zipkin-sender-urlconnection</artifactId>
    <version>2.16.3</version>
</dependency>
<dependency>
    <groupId>io.zipkin.reporter2</groupId>
    <artifactId>zipkin-reporter-brave</artifactId>
    <version>2.16.3</version>
</dependency>
<dependency>
    <groupId>io.zipkin.brave</groupId>
    <artifactId>brave</artifactId>
    <version>5.13.3</version>
</dependency>
    <dependency>
    <groupId>io.zipkin.brave</groupId>
    <artifactId>brave-core</artifactId>
    <version>4.13.6</version>
</dependency>
            

Guardar el archivo.

El método HandleRequest

Las observaciones sobre el método siguen el código fuente handleRequest.


package com.example.fn;
import brave.Span;
import brave.Tracer;
import brave.Tracing;
import brave.propagation.*;
import brave.sampler.Sampler;
import com.fnproject.fn.api.tracing.TracingContext;
import com.github.kristofa.brave.IdConversion;
import zipkin2.reporter.Sender;
import zipkin2.reporter.brave.AsyncZipkinSpanHandler;
import zipkin2.reporter.urlconnection.URLConnectionSender;

public class HelloFunction {
    Sender sender;
    AsyncZipkinSpanHandler zipkinSpanHandler;
    Tracing tracing;
    Tracer tracer;
    String apmUrl;
    TraceContext traceContext;

    public String handleRequest(String input, TracingContext tracingContext) {
        try {
            intializeZipkin(tracingContext);
            // Start a new trace or a span within an existing trace representing an operation
            Span span = tracer.newChild(traceContext).name("MainHandle").start();
            System.out.println("Inside Java Hello World function");
            try (Tracer.SpanInScope ws = tracer.withSpanInScope(span)) {
                method1();
                method2();
                method3();
            } catch (RuntimeException | Error e) {
                span.error(e); // Unless you handle exceptions, you might not know the operation failed!
                throw e;
            } finally {
                span.finish(); // note the scope is independent of the span. Always finish a span.
                tracing.close();
                zipkinSpanHandler.flush();
            }
        } catch (Exception e) {
            return e.getMessage();
        }
        return "Hello, AppName " + tracingContext.getAppName() + " :: fnName " + tracingContext.getFunctionName();
    }
            
  • El objeto TracingContext tracingConext transfiere toda la información relacionada con APM necesaria para realizar conexiones al servicio APM.
  • Se llama al método intializeZipkin, que actualiza tracingContext y crea un objeto tracer que se utiliza para configurar períodos personalizados.
  • Se crea un span para el período personalizado principal. A continuación, se llama a tres métodos en el ámbito del período principal.
  • Observe que en el bloque finally se cierran todos los objetos de rastreo.

El método initializeZipkin

Las observaciones sobre el método intializeZipkin siguen el código fuente.

     
    public void intializeZipkin(TracingContext tracingContext) throws Exception {
        System.out.println("Initializing the variables");
        apmUrl = tracingContext.getTraceCollectorURL();
        sender = URLConnectionSender.create(apmUrl);
        zipkinSpanHandler = AsyncZipkinSpanHandler.create(sender);
        tracing = Tracing.newBuilder()
                .localServiceName(tracingContext.getServiceName())
                .sampler(Sampler.NEVER_SAMPLE)
                .addSpanHandler(zipkinSpanHandler)
                .build();
        tracer = tracing.tracer();
        tracing.setNoop(!tracingContext.isTracingEnabled());
        traceContext = TraceContext.newBuilder()
                .traceId(IdConversion.convertToLong(tracingContext.getTraceId()))
                .spanId(IdConversion.convertToLong(tracingContext.getSpanId()))
                .sampled(tracingContext.isSampled()).build();
    }   
            
  • traceContext se transfiere para crear todos los objetos utilizados para crear períodos personalizados.
  • apmURL se recupera del método getTraceCollectorURL(). La URL es el punto final del dominio de APM y se utiliza para crear el objeto tracer que crea los períodos personalizados.
  • Un creador toma zipkinSpanHandler y el nombre del servicio para crear un objeto tracer. Este objeto tracer se utiliza para crear períodos personalizados.

Creación de períodos personalizados

Con el objeto tracer inicializado, se pueden crear períodos personalizados.


public void method1() {
    System.out.println("Inside Method1 function");
    TraceContext traceContext = tracing.currentTraceContext().get();
    Span span = tracer.newChild(traceContext).name("Method1").start();
    try {
        Thread.sleep(200);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        span.finish();
    }
}
            
  • El método method1 crea un período personalizado denominado "Method1".
Revisar código de origen de función finalizada

Este es el código fuente completo para la función de rastreo de Java de ejemplo.


package com.example.fn;
import brave.Span;
import brave.Tracer;
import brave.Tracing;
import brave.propagation.*;
import brave.sampler.Sampler;
import com.fnproject.fn.api.tracing.TracingContext;
import com.github.kristofa.brave.IdConversion;
import zipkin2.reporter.Sender;
import zipkin2.reporter.brave.AsyncZipkinSpanHandler;
import zipkin2.reporter.urlconnection.URLConnectionSender;

public class HelloFunction {
    Sender sender;
    AsyncZipkinSpanHandler zipkinSpanHandler;
    Tracing tracing;
    Tracer tracer;
    String apmUrl;
    TraceContext traceContext;
    public void intializeZipkin(TracingContext tracingContext) throws Exception {
        System.out.println("Initializing the variables");
        apmUrl = tracingContext.getTraceCollectorURL();
        sender = URLConnectionSender.create(apmUrl);
        zipkinSpanHandler = AsyncZipkinSpanHandler.create(sender);
        tracing = Tracing.newBuilder()
                .localServiceName(tracingContext.getServiceName())
                .sampler(Sampler.NEVER_SAMPLE)
                .addSpanHandler(zipkinSpanHandler)
                .build();
        tracer = tracing.tracer();
        tracing.setNoop(!tracingContext.isTracingEnabled());
        traceContext = TraceContext.newBuilder()
                .traceId(IdConversion.convertToLong(tracingContext.getTraceId()))
                .spanId(IdConversion.convertToLong(tracingContext.getSpanId()))
                .sampled(tracingContext.isSampled()).build();
    }

    public String handleRequest(String input, TracingContext tracingContext) {
        try {
            intializeZipkin(tracingContext);
            // Start a new trace or a span within an existing trace representing an operation
            Span span = tracer.newChild(traceContext).name("MainHandle").start();
            System.out.println("Inside Java Hello World function");
            try (Tracer.SpanInScope ws = tracer.withSpanInScope(span)) {
                method1();
                method2();
                method3();
            } catch (RuntimeException | Error e) {
                span.error(e); // Unless you handle exceptions, you might not know the operation failed!
                throw e;
            } finally {
                span.finish(); // note the scope is independent of the span. Always finish a span.
                tracing.close();
                zipkinSpanHandler.flush();
            }
        } catch (Exception e) {
            return e.getMessage();
        }
        return "Hello, AppName " + tracingContext.getAppName() + " :: fnName " + tracingContext.getFunctionName();
    }

    public void method1() {
        System.out.println("Inside Method1 function");
        TraceContext traceContext = tracing.currentTraceContext().get();
        Span span = tracer.newChild(traceContext).name("Method1").start();
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            span.finish();
        }
    }

    public void method2() {
        System.out.println("Inside Method2 function");
        TraceContext traceContext = tracing.currentTraceContext().get();
        Span span = tracer.newChild(traceContext).name("Method2").start();
        try {
            Thread.sleep(400);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            span.finish();
        }
    }

    public void method3() {
        System.out.println("Inside Method3 function");
        TraceContext traceContext = tracing.currentTraceContext().get();
        Span span = tracer.newChild(traceContext).name("Method3").start();
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            span.finish();
        }
    }
}
            
Adición de períodos personalizados a funciones de Python

A continuación, se muestra un ejemplo de cómo utilizar Zipkin para agregar períodos personalizados a la función de Python. Si desea probar este ejemplo, puede crear una función "Hello World!" de Python y agregar código de período personalizado. Para crear una función de ejemplo:

  • Crear una función de Python: fn init --runtime python apm-fn-python

Configurar paquetes

Actualice el archivo requirements.txt para incluir los siguientes paquetes:


fdk
requests
py_zipkin
            

Guardar el archivo.

Creación de Clase de Manejador y Intervalo Personalizado Principal

La función Python llama a la función handler y transfiere en el contexto de la función para crear períodos personalizados.

def handler(ctx, data: io.BytesIO = None):
    tracing_context = ctx.TracingContext()
    with zipkin_span(
        service_name=tracing_context.service_name(),
        span_name="Customer Code",
        transport_handler=(
            lambda encoded_span: transport_handler(
                encoded_span, tracing_context
            )
        ),
        zipkin_attrs=tracing_context.zipkin_attrs(),
        encoding=Encoding.V2_JSON,
        binary_annotations=tracing_context.annotations()
    ):
        name = "World"
        try:
            body = json.loads(data.getvalue())
            name = body.get("name")
        except (Exception, ValueError) as ex:
            logging.getLogger().info('error parsing json payload: ' + str(ex))

        logging.getLogger().info("Inside Python Hello World function")
        time.sleep(0.005)
        example(ctx)
        return response.Response(
            ctx, response_data=json.dumps(
                {"message": "Hello {0}".format(name)}),
            headers={"Content-Type": "application/json"}
        )         
  • tracing_context se transfiere desde el contexto de la función y contiene toda la información necesaria para crear y configurar períodos personalizados.
    Nota

    Si el rastreo no está activado, el contexto de rastreo es un objeto vacío. Con un contexto de rastreo vacío, el indicador is_sampled se define en None y py_zipkin no emite períodos.
  • La sentencia with zipkin_span se utiliza para crear períodos.
    • La información de tracing_context se utiliza para obtener service_name, llamar a transport_handler y definir zipking_attrs.
    • Se especifica un nombre de período personalizado simplemente definiendo span_name.
    • Los atributos de rastreo necesarios para Zipkin se recuperan del contexto de rastreo: tracing_context.zipkin_attrs().
  • Con la configuración personalizada del intervalo, el bloque principal ejecuta el código fijo "Hello World!" Con la única excepción, una llamada a la función example.

La función transport_handler

La función transport_handler se comunica con el dominio de APM con mensajes sobre la ejecución del período.

# transport handler, needed by py_zipkin
def transport_handler(encoded_span, tracing_context):
    return requests.post(
        tracing_context.trace_collector_url(),
        data=encoded_span,
        headers={"Content-Type": "application/json"},
    )            
  • trace_collector_url se devuelve desde el contexto de la función. Esta URL proporciona el punto final de comunicación para los períodos personalizados al dominio de APM.

Creación de un intervalo personalizado en una función de ejemplo

La función de ejemplo muestra la creación de un período personalizado.

def example(ctx):
    with zipkin_span(
        service_name=ctx.TracingContext().service_name(),
        span_name="Get ADB Password from OCI Vault",
        binary_annotations=ctx.TracingContext().annotations()
    ) as example_span_context:
        try:
            logging.getLogger().debug("Get ADB Password from OCI Vault")
            time.sleep(0.005)
            # throwing an exception to show how to add error messages to spans
            raise Exception('Request failed')
        except (Exception, ValueError) as error:
            example_span_context.update_binary_annotations(
                {"Error": True, "errorMessage": str(error)}
            )
        else:
            FakeResponse = namedtuple("FakeResponse", "status, message")
            fakeResponse = FakeResponse(200, "OK")
            # how to update the span dimensions/annotations
            example_span_context.update_binary_annotations(
                {
                    "responseCode": fakeResponse.status,
                    "responseMessage": fakeResponse.message
                }
            )            
  • La sentencia with zipkin_span se utiliza para identificar el período personalizado y asignarle un nombre.
  • El bloque example_span_context emite una excepción y devuelve un mensaje de error.
Revisar código de origen de función finalizada

Este es el código fuente completo para la función de rastreo de Python de ejemplo.


import io
import json
import logging

from fdk import response

import requests
import time
from py_zipkin import Encoding
from py_zipkin.zipkin import zipkin_span
from collections import namedtuple


# transport handler, needed by py_zipkin
def transport_handler(encoded_span, tracing_context):
    return requests.post(
        tracing_context.trace_collector_url(),
        data=encoded_span,
        headers={"Content-Type": "application/json"},
    )


def handler(ctx, data: io.BytesIO = None):
    tracing_context = ctx.TracingContext()
    with zipkin_span(
        service_name=tracing_context.service_name(),
        span_name="Customer Code",
        transport_handler=(
            lambda encoded_span: transport_handler(
                encoded_span, tracing_context
            )
        ),
        zipkin_attrs=tracing_context.zipkin_attrs(),
        encoding=Encoding.V2_JSON,
        binary_annotations=tracing_context.annotations()
    ):
        name = "World"
        try:
            body = json.loads(data.getvalue())
            name = body.get("name")
        except (Exception, ValueError) as ex:
            logging.getLogger().info('error parsing json payload: ' + str(ex))

        logging.getLogger().info("Inside Python Hello World function")
        time.sleep(0.005)
        example(ctx)
        return response.Response(
            ctx, response_data=json.dumps(
                {"message": "Hello {0}".format(name)}),
            headers={"Content-Type": "application/json"}
        )


def example(ctx):
    with zipkin_span(
        service_name=ctx.TracingContext().service_name(),
        span_name="Get ADB Password from OCI Vault",
        binary_annotations=ctx.TracingContext().annotations()
    ) as example_span_context:
        try:
            logging.getLogger().debug("Get ADB Password from OCI Vault")
            time.sleep(0.005)
            # throwing an exception to show how to add error messages to spans
            raise Exception('Request failed')
        except (Exception, ValueError) as error:
            example_span_context.update_binary_annotations(
                {"Error": True, "errorMessage": str(error)}
            )
        else:
            FakeResponse = namedtuple("FakeResponse", "status, message")
            fakeResponse = FakeResponse(200, "OK")
            # how to update the span dimensions/annotations
            example_span_context.update_binary_annotations(
                {
                    "responseCode": fakeResponse.status,
                    "responseMessage": fakeResponse.message
                }
            )
            
Adición de períodos personalizados a funciones de nodo

A continuación, se muestra un ejemplo de cómo utilizar Zipkin para agregar períodos personalizados a la función Node.js. Si desea probar este ejemplo, puede crear una función "Hello World!" del nodo y agregar código de intervalo personalizado. Para crear una función de ejemplo:

  • Crear una función de nodo: fn init --runtime node apm-fn-node

Configurar dependencias de nodos

Actualice el archivo package.json para incluir los siguientes paquetes:


{
    "name": "apm-tracing-node-fdk-simple-trace-final",
    "version": "1.0.0",
    "description": "Example APM tracing function",
    "main": "func.js",
    "author": "",
    "license": "Apache-2.0",
    "dependencies": {
        "@fnproject/fdk": ">=0.0.13",
        "node-fetch": "^2.6.1",
        "zipkin": "^0.22.0",
        "zipkin-transport-http": "^0.22.0"
    }
}
            

Guardar el archivo.

Actualizar método de controlador

Las observaciones clave sobre el método fdk.handle siguen el código fuente.


// ZipkinJS core components.
const { 
    ExplicitContext, 
    Annotation, 
    Tracer, 
    TraceId, 
    BatchRecorder, 
    jsonEncoder, 
    sampler, 
    option
} = require('zipkin');

// An HTTP transport for dispatching Zipkin traces.
const {HttpLogger} = require('zipkin-transport-http');

fdk.handle(async function(input, ctx){
    tracer = createOCITracer(ctx);
    
    var result;
    // Start a new 'scoped' server handling span.
    await tracer.scoped(async function () {
        // Fetch some resource
        result = await tracer.local('fetchResource', () => {
            return fetchResource();
        });
        // Perform some processing
        result = await tracer.local('processResource', () => {
            return someComputation(result);
        });
        // Update some resource
        result = await tracer.local('updateResource', () => {
            return updateResource(result);
        });
        await flush();
    }); 
    
    return result;

})            
            
  • tracer se crea y, a continuación, se utiliza para crear un período personalizado principal. A continuación, se crean intervalos secundarios para las funciones fetchResource, processResource y updateResource.

Revisión de la función createOCITracer

Las observaciones clave sobre la función siguen el código fuente.


/**
 * Creates a basic Zipkin Tracer using values from context of the function
 * invocation.
 *
 * @param {*} ctx The function invocation context.
 * @returns       A configured Tracer for automatically tracing calls.
 */
function createOCITracer (ctx) {
  // An OCI APM configured Tracer
  //
  const tracingCxt = ctx.tracingContext
  const tracer = new Tracer({
    ctxImpl: new ExplicitContext(),
    recorder: new BatchRecorder({
      logger: new HttpLogger({
        // The configured OCI APM endpoint is available in the function
        // invocation context.
        endpoint: tracingCxt.traceCollectorUrl,
        jsonEncoder: jsonEncoder.JSON_V2
      })
    }),
    // APM Dimensions that should be included in all traces can be configured
    // directly on Tracer.
    defaultTags: createOCITags(ctx),
    // A custom sampling strategy can be defined.
    sampler: createOCISampler(ctx),
    localServiceName: tracingCxt.serviceName,
    supportsJoin: true,
    traceId128Bit: true
  })

  // The initial function invocation trace identifiers can be added directly.
  // If this is not defined a default TraceId is created.
  const traceId = createOCITraceId(tracer, ctx)
  tracer.setId(traceId)
  return tracer
}
            
  • El contexto de función (ctx) se transfiere a esta función, que proporciona la información necesaria para conectarse al dominio de APM. Si sigue las llamadas de función, puede ver cómo se crean los ID de rastreo y los campos.
Revisar código de origen de función finalizada

Este es el código fuente completo para la función de rastreo de nodos de ejemplo.



const fdk = require('@fnproject/fdk')

// ZipkinJS core components.
const {
  ExplicitContext,
  Tracer,
  TraceId,
  BatchRecorder,
  jsonEncoder,
  sampler,
  option
} = require('zipkin')

// An HTTP transport for dispatching Zipkin traces.
const { HttpLogger } = require('zipkin-transport-http')

fdk.handle(async function (input, ctx) {
  var tracer = createOCITracer(ctx)

  var result
  // Start a new 'scoped' server handling span.
  await tracer.scoped(async function () {
    // Fetch some resource
    result = await tracer.local('fetchResource', () => {
      return fetchResource()
    })
    // Perform some processing
    result = await tracer.local('processResource', () => {
      return someComputation(result)
    })
    // Update some resource
    result = await tracer.local('updateResource', () => {
      return updateResource(result)
    })
    await flush()
  })

  return result
})

// ----------------------------------------------------------------------------
// App Simulation Functions
//

/**
 * Simulate fetching some required resource. This could be another OCI service
 * or an external call.
 *
 * @returns A Promise with the success or failure of the operation.
 */
function fetchResource () {
  return simulate(1000, { fetchResource: 'OK' })
}

/**
 * Simulate some work. This could be another OCI service.
 *
 * @returns A Promise with the success or failure of the operation.
 */
async function someComputation (toReturn) {
  var i
  for (i = 0; i < 5; i++) {
    await simulate(1000)
  }
  toReturn.processResource = 'OK'
  return toReturn
}

/**
 * Simulate updating some resource. This could be another OCI service or an
 * external call.
 *
 * @returns A Promise with the success or failure of the operation.
 */
async function updateResource (toReturn) {
  await simulate(500)
  toReturn.updateResource = 'OK'
  return toReturn
}

/**
 * A helper function to simulate an operation that takes a specified amount of time.
 *
 * @param {*} ms The simulated time for the activity in milliseconds.
 * @returns      A promise that resolves when the simulated activity finishes.
 */
function simulate (ms, result) {
  return new Promise(resolve => setTimeout(resolve, ms, result))
}

/**
 * Functions service may freeze or terminate the container on completion.
 * This function gives extra time to allow the runtime to flush any pending traces.
 * See: https://github.com/openzipkin/zipkin-js/issues/507
 *
 * @returns A Promise to await on.
 */
function flush () {
  return new Promise(resolve => setTimeout(resolve, 1000))
}

// ----------------------------------------------------------------------------
// OpenZipkin ZipkinJS Utility Functions
//

/**
 * Creates a basic Zipkin Tracer using values from context of the function
 * invocation.
 *
 * @param {*} ctx The function invocation context.
 * @returns       A configured Tracer for automatically tracing calls.
 */
function createOCITracer (ctx) {
  // An OCI APM configured Tracer
  //
  const tracingCxt = ctx.tracingContext
  const tracer = new Tracer({
    ctxImpl: new ExplicitContext(),
    recorder: new BatchRecorder({
      logger: new HttpLogger({
        // The configured OCI APM endpoint is available in the function
        // invocation context.
        endpoint: tracingCxt.traceCollectorUrl,
        jsonEncoder: jsonEncoder.JSON_V2
      })
    }),
    // APM Dimensions that should be included in all traces can be configured
    // directly on Tracer.
    defaultTags: createOCITags(ctx),
    // A custom sampling strategy can be defined.
    sampler: createOCISampler(ctx),
    localServiceName: tracingCxt.serviceName,
    supportsJoin: true,
    traceId128Bit: true
  })

  // The initial function invocation trace identifiers can be added directly.
  // If this is not defined a default TraceId is created.
  const traceId = createOCITraceId(tracer, ctx)
  tracer.setId(traceId)
  return tracer
}

/**
 * A ZipkinJS 'TraceId' can be created directly from the function invocation
 * context.
 *
 * @param {*} ctx The function invocation context.
 * @returns       A ZipkinJS 'TraceId' created from the invocation context.
 */
function createOCITraceId (tracer, ctx) {
  const tracingCxt = ctx.tracingContext
  if (tracingCxt.traceId && tracingCxt.spanId) {
    return new TraceId({
      traceId: tracingCxt.traceId,
      spanId: tracingCxt.spanId,
      sampled: new option.Some(tracingCxt.sampled),
      debug: new option.Some(tracingCxt.debug),
      shared: false
    })
  } else {
    return tracer.createRootId(
      new option.Some(tracingCxt.sampled),
      new option.Some(tracingCxt.debug)
    )
  }
}

/**
 * A ZipkinJS 'TraceId' can be crated directly from the function invocation
 * context.
 *
 * This configurations will automatically add the function meta-data as APM
 * dimensions to each trace. Function environment variable and other dimensions
 * could also be added.
 *
 * @param {*} ctx The function invocation context.
 * @returns       A map of key-value pairs, that will be added as APM
 *                dimensions to the traces.
 */
function createOCITags (ctx) {
  return {
    appID: ctx.appID,
    appName: ctx.appName,
    fnID: ctx.fnID,
    fnName: ctx.fnName
  }
}

/**
 * A ZipkinJS 'Sampler' can be created directly from the function invocation
 * context.
 *
 * This configuration will only create a trace if the function is configured
 * for tracing.
 *
 * @param {*} ctx The function invocation context.
 * @returns       A ZipkinJS 'TraceId' created from the invocation context.
 */
function createOCISampler (ctx) {
  return new sampler.Sampler((traceId) => ctx.tracingContext.isEnabled)
}