Hacer que se pueda encontrar el código utilizando identificadores de mensajes únicos a nivel mundial

39

Un patrón común para localizar un error sigue esta secuencia de comandos:

  1. Observe la rareza, por ejemplo, no hay salida o un programa que cuelga.
  2. Localice el mensaje relevante en el registro o en la salida del programa, por ejemplo, "No se pudo encontrar Foo". (Lo siguiente solo es relevante si esta es la ruta tomada para ubicar el error. Si hay un seguimiento de la pila u otra información de depuración disponible, eso es otra historia).
  3. Localice el código donde se imprime el mensaje.
  4. Depure el código entre el primer lugar donde Foo ingresa (o debería ingresar) la imagen y el lugar donde se imprime el mensaje.

El tercer paso es donde el proceso de depuración a menudo se detiene porque hay muchos lugares en el código donde se imprime "No se pudo encontrar Foo" (o una cadena con plantilla Could not find {name} ). De hecho, varias veces un error ortográfico me ayudó a encontrar la ubicación real mucho más rápido de lo que lo haría de otra manera: hizo que el mensaje fuera único en todo el sistema y, a menudo, en todo el mundo, lo que resultó en un importante motor de búsqueda. inmediatamente.

La conclusión obvia de esto es que deberíamos usar identificadores de mensaje únicos a nivel mundial en el código, codificándolos como parte de la cadena del mensaje, y posiblemente verificando que solo haya una ocurrencia de cada identificador en la base del código. En términos de capacidad de mantenimiento, ¿qué piensa esta comunidad que son los pros y los contras más importantes de este enfoque, y cómo implementaría esto o de otra manera garantizaría que la implementación nunca sea necesaria (asumiendo que el software siempre tendrá errores)?

    
pregunta l0b0 30.01.2018 - 03:54

6 respuestas

12

En general, esta es una estrategia válida y valiosa. Aquí hay algunos pensamientos.

Esta estrategia también se conoce como "telemetría" en el sentido de que cuando se combina toda esa información, ayudan a "triangular" el seguimiento de ejecución y permiten que un solucionador de problemas tenga sentido de lo que el usuario / aplicación está tratando de lograr, y lo que realmente sucedió.

Algunos datos esenciales que deben recopilarse (que todos sabemos) son:

  • Ubicación del código, es decir, pila de llamadas y la línea de código aproximada
    • La "línea de código aproximada" no es necesaria si las funciones se descomponen razonablemente en unidades adecuadamente pequeñas.
  • Cualquier dato que sea pertinente para el éxito / fracaso de la función
  • Un "comando" de alto nivel que puede determinar lo que el usuario humano / agente externo / API está tratando de lograr.
    • La idea es que un software acepte y procese comandos provenientes de algún lugar.
    • Durante este proceso, es posible que se hayan realizado de docenas a cientos o miles de llamadas a funciones.
    • Nos gustaría que cualquier telemetría generada a lo largo de este proceso sea rastreable hasta el comando de más alto nivel que activa este proceso.
    • Para los sistemas basados en la web, la solicitud HTTP original y sus datos serían un ejemplo de dicha "información de solicitud de alto nivel"
    • Para los sistemas GUI, el usuario que haga clic en algo encajaría en esta descripción.

Muchas veces, los enfoques de registro tradicionales se quedan cortos, debido a que no se pudo rastrear un mensaje de registro de bajo nivel hasta el comando de nivel más alto que lo activa. Un seguimiento de pila solo captura los nombres de las funciones superiores que ayudaron a manejar el comando de nivel más alto, no los detalles (datos) que a veces se necesitan para caracterizar ese comando.

Normalmente, el software no se escribió para implementar este tipo de requisitos de trazabilidad. Esto hace que sea más difícil correlacionar el mensaje de bajo nivel con el comando de alto nivel. El problema es particularmente peor en los sistemas de múltiples subprocesos libres, donde muchas de las solicitudes y respuestas pueden superponerse, y el procesamiento se puede descargar a un subproceso diferente al del subproceso de recepción de solicitudes original.

Por lo tanto, para obtener el máximo valor de la telemetría, se necesitarán cambios en la arquitectura general del software. La mayoría de las interfaces y llamadas de función deberán modificarse para aceptar y propagar un argumento "rastreador".

Incluso las funciones de utilidad necesitarán agregar un argumento "de seguimiento", de modo que si falla, el mensaje de registro permitirá que se correlacione con un determinado comando de alto nivel.

Otro fallo que dificultará el seguimiento de la telemetría es la falta de referencias de objetos (punteros nulos o referencias). Cuando faltan algunos datos cruciales, podría ser imposible informar algo útil por el fallo.

En cuanto a la escritura de los mensajes de registro:

  • Algunos proyectos de software pueden requerir localización (traducción a un idioma extranjero) incluso para los mensajes de registro que solo están destinados a administradores.
  • Es posible que algunos proyectos de software necesiten una separación clara entre los datos confidenciales y los datos no confidenciales, incluso para el registro, y que los administradores no tengan la oportunidad de ver accidentalmente ciertos datos confidenciales.
  • No intentes ofuscar el mensaje de error. Eso minaría la confianza de los clientes. Los administradores de los clientes esperan leer esos registros y darle sentido. No les haga sentir que hay algún secreto de propiedad que debe ocultarse a los administradores de los clientes.
  • Espere que los clientes traigan un trozo de registro de telemetría y pongan a la parrilla a su personal de soporte técnico. Ellos esperan saber. Capacite a su personal de soporte técnico para explicar el registro de telemetría correctamente.
respondido por el rwong 30.01.2018 - 08:08
59

Imagina que tienes una función de utilidad trivial que se usa en cientos de lugares en tu código:

decimal Inverse(decimal input)
{
    return 1 / input;
}

Si hiciéramos lo que usted sugiere, podríamos escribir

decimal Inverse(decimal input)
{
    try 
    {
        return 1 / input;
    }
    catch(Exception ex)
    {
        log.Write("Error 27349262 occurred.");
    }
}

Un error que podría ocurrir es si la entrada fuera cero; esto daría lugar a una excepción de división por cero.

Entonces, digamos que ve 27349262 en su salida o sus registros. ¿Dónde miras para encontrar el código que pasó el valor cero? Recuerde, la función, con su ID única, se utiliza en cientos de lugares. Por lo tanto, si bien puede saber que se produjo la división por cero, no tiene idea de a quién pertenece 0 .

Me parece que si vas a molestarte en registrar los identificadores de mensajes, también puedes registrar el seguimiento de la pila.

Si lo que te molesta es la verbosidad del seguimiento de la pila, no tienes que volcarlo como una cadena de la forma en que el tiempo de ejecución te lo da. Puedes personalizarlo. Por ejemplo, si desea que un seguimiento de pila abreviado vaya solo a los niveles n , podría escribir algo como esto (si usa c #):

static class ExtensionMethods
{
    public static string LimitedStackTrace(this Exception input, int layers)
    {
        return string.Join
        (
            ">",
            new StackTrace(input)
                .GetFrames()
                .Take(layers)
                .Select
                (
                    f => f.GetMethod()
                )
                .Select
                (
                    m => string.Format
                    (
                        "{0}.{1}", 
                        m.DeclaringType, 
                        m.Name
                    )
                )
                .Reverse()
        );
    }
}

Y úsalo así:

public class Haystack
{
    public static void Needle()
    {
        throw new Exception("ZOMG WHERE DID I GO WRONG???!");
    }

    private static void Test()
    {
        Needle();
    }

    public static void Main()
    {
        try
        {
            Test();
        }
        catch(System.Exception e)
        {
            //Get 3 levels of stack trace
            Console.WriteLine
            (
                "Error '{0}' at {1}", 
                e.Message, 
                e.LimitedStackTrace(3)
            );  
        }
    }
}

Salida:

Error 'ZOMG WHERE DID I GO WRONG???!' at Haystack.Main>Haystack.Test>Haystack.Needle

Quizás sea más fácil que mantener las ID de los mensajes, y más flexible.

Robar mi código de DotNetFiddle

    
respondido por el John Wu 30.01.2018 - 04:14
6

SAP NetWeaver está haciendo esto por décadas.

Se ha demostrado que es una herramienta valiosa para solucionar errores en el gigante de código masivo que es el sistema SAP ERP típico.

Los mensajes de error se administran en un repositorio central donde cada mensaje se identifica por su clase de mensaje y número de mensaje.

Cuando desea mostrar un mensaje de error, solo indica las variables de clase, número, gravedad y específicas del mensaje. La representación de texto del mensaje se crea en tiempo de ejecución. Por lo general, se ve la clase y el número de mensaje en cualquier contexto donde aparecen los mensajes. Esto tiene varios efectos nítidos:

  • Puede encontrar automáticamente cualquier línea de código en el código base de ABAP que cree un mensaje de error específico.

  • Puede establecer puntos de interrupción dinámicos del depurador que se activan cuando se genera un mensaje de error específico.

  • Puede buscar errores en los artículos de la base de conocimientos de SAP y obtener resultados de búsqueda más relevantes que si busca "No se pudo encontrar a Foo".

  • Las representaciones de texto de los mensajes son traducibles. Por lo tanto, al fomentar el uso de mensajes en lugar de cadenas, también obtiene capacidades i18n.

Un ejemplo de una ventana emergente de error con el número de mensaje:

Buscandoeseerrorenelrepositoriodeerrores:

Búscaloenelcódigobase:

Sinembargo,hayinconvenientes.Comopuedever,estaslíneasdecódigoyanoseautodocumentan.CuandoleeselcódigofuenteyvesunadeclaraciónMESSAGEcomolasdelacapturadepantallaanterior,solopuedesinferirdelcontextoloquerealmentesignifica.Además,aveceslaspersonasimplementancontroladoresdeerrorespersonalizadosquerecibenlaclasedemensajeyelnúmeroentiempodeejecución.Enesecaso,elerrornosepuedeencontrarautomáticamenteonosepuedeencontrarenlaubicacióndonderealmenteocurrióelerror.Lasoluciónparaelprimerproblemaeshacerqueseaunhábitoagregarsiempreuncomentarioenelcódigofuenteydecirleallectorloquesignificaelmensaje.Elsegundoseresuelveagregandouncódigomuertoparaasegurarsedequelabúsquedaautomáticademensajesfunciona.Ejemplo:

" Do not use special characters
my_custom_error_handler->post_error( class = 'EU' number = '271').
IF 1 = 2.
   MESSAGE e271(eu).
ENDIF.    

Pero hay algunas situaciones donde esto no es posible. Por ejemplo, hay algunas herramientas de modelado de procesos de negocios basadas en UI donde puede configurar mensajes de error para que aparezcan cuando se violan las reglas de negocios. La implementación de esas herramientas está completamente basada en datos, por lo que estos errores no se mostrarán en la lista donde se usa. Eso significa que confiar demasiado en la lista donde se usa cuando se intenta encontrar la causa de un error puede ser una pista falsa.

    
respondido por el Philipp 30.01.2018 - 14:55
5

El problema con este enfoque es que conduce a un registro cada vez más detallado. 99.9999% de los cuales nunca verás.

En cambio, recomiendo capturar el estado al comienzo de su proceso y el éxito / fracaso del proceso.

Esto le permite reproducir el error localmente, recorriendo el código y limitando su registro a dos lugares por proceso. por ejemplo.

OrderPlaced {id:xyz; ...order data..}
OrderPlaced {id:xyz; ...Fail, ErrorMessage..}

Ahora puedo usar el mismo estado exacto en mi máquina dev para reproducir el error, revisar el código en mi depurador y escribir una nueva prueba de unidad para confirmar la corrección.

Además, puedo, si es necesario, evitar más registros al registrar solo fallos o mantener el estado en otra parte (¿base de datos? ¿cola de mensajes?)

Obviamente, debemos tener mucho cuidado al registrar datos confidenciales. Así que esto funciona particularmente bien si su solución está utilizando colas de mensajes o el patrón de almacenamiento de eventos. Como el registro solo necesita decir "Error en el mensaje xyz"

    
respondido por el Ewan 30.01.2018 - 14:40
1

Sugeriría que el registro no es la forma de hacerlo, sino que esta circunstancia se considera excepcional (bloquea su programa) y se debe lanzar una excepción. Di que tu código era:

public Foo GetFoo() {

     //Expecting that this should never by null.
     var aFoo = ....;

     if (aFoo == null) Log("Could not find Foo.");

     return aFoo;
}

Parece que tu código de llamada no está configurado para lidiar con el hecho de que Foo no existe y que posiblemente deberías estar:

public Foo GetFooById(int id) {
     var aFoo = ....;

     if (aFoo == null) throw new ApplicationException("Could not find Foo for ID: " + id);

     return aFoo;
}

Y esto devolverá un seguimiento de pila junto con la excepción que se puede usar para ayudar a la depuración.

Alternativamente, si esperamos que Foo pueda ser nulo cuando se recupere y eso está bien, necesitamos corregir los sitios de llamada:

void DoSomeFoo(Foo aFoo) {

    //Guard checks on your input - complete with stack trace!
    if (aFoo == null) throw new ArgumentNullException(nameof(aFoo));

    ... operations on Foo...
}

El hecho de que su software se cuelgue o actúe "extrañamente" en circunstancias inesperadas me parece incorrecto. Si necesita un Foo y no puede manejar que no esté allí, entonces es mejor que se produzca un error que intentar avanzar por un Ruta que puede dañar tu sistema.

    
respondido por el Paddy 30.01.2018 - 15:06
0

Las bibliotecas de registro adecuadas proporcionan mecanismos de extensión, por lo que si desea conocer el método en el que se originó el mensaje de registro, pueden hacerlo de forma inmediata. Tiene un impacto en la ejecución, ya que el proceso requiere generar un seguimiento de pila y atravesarlo hasta que esté fuera de la biblioteca de registro.

Dicho esto, realmente depende de lo que quieras que haga tu ID:

  • ¿Correlacionar los mensajes de error proporcionados al usuario a sus registros?
  • ¿Proporciona una notación sobre qué código se estaba ejecutando cuando se generó el mensaje?
  • ¿Mantener un registro del nombre de la máquina y la instancia de servicio?
  • ¿Mantener un registro de la ID del hilo?

Todas estas cosas se pueden hacer fuera de la caja con el software de registro adecuado (es decir, no Console.WriteLine() o Debug.WriteLine() ).

Personalmente, lo que es más importante es la capacidad de reconstruir rutas de ejecución. Para eso están diseñadas las herramientas como Zipkin . Una identificación para rastrear el comportamiento de una acción del usuario en todo el sistema. Al poner sus registros en un motor de búsqueda central, no solo puede encontrar las acciones de mayor duración, sino que también puede acceder a los registros que se aplican a esa acción (como pila ELK ).

Las IDs opacas que cambian con cada mensaje no son muy útiles. Una ID coherente utilizada para rastrear el comportamiento a través de un conjunto completo de microservicios ... inmensamente útil.

    
respondido por el Berin Loritsch 30.01.2018 - 19:38

Lea otras preguntas en las etiquetas