Hacer que el código se pueda encontrar mediante el uso de ID de mensajes únicos a nivel mundial

39

Un patrón común para localizar un error sigue este script:

  1. Observe la rareza, por ejemplo, sin salida o un programa colgado.
  2. Localice el mensaje relevante en el registro o la salida del programa, por ejemplo, "No se pudo encontrar Foo". (Lo siguiente solo es relevante si esta es la ruta tomada para localizar el error. Si un seguimiento de la pila u otra información de depuración está disponible, esa es otra historia).
  3. Busque 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 dónde 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 Could not find {name}se imprime "No se pudo encontrar Foo" (o una cadena con plantilla ). De hecho, varias veces un error de ortografía 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 golpe de motor de búsqueda relevante de inmediato.

La conclusión obvia de esto es que deberíamos usar ID de mensaje únicos a nivel mundial en el código, codificándolo como parte de la cadena del mensaje y posiblemente verificando que solo haya una aparición de cada ID en la base del código. En términos de mantenibilidad, ¿cuáles cree esta comunidad que son los pros y los contras más importantes de este enfoque, y cómo implementaría esto o de lo contrario se aseguraría de que la implementación nunca sea necesaria (suponiendo que el software siempre tenga errores)?

l0b0
fuente
54
Haga uso de sus rastros de pila en su lugar. El seguimiento de la pila no solo le dirá con precisión dónde se produjo el error, sino también cada función que llamó a cada función que lo llamó. Registre todo el rastreo cuando ocurra una excepción, si es necesario. Si está trabajando en un lenguaje que no tiene excepciones, como C, esa es una historia diferente.
Robert Harvey
66
@ l0b0 un pequeño consejo sobre la redacción. "¿Qué piensa esta comunidad ... pros y contras" son frases que pueden considerarse demasiado amplias. Este es un sitio que permite preguntas "buenas subjetivas" y, a cambio de permitir este tipo de preguntas, se espera que usted, como OP, haga el trabajo de "guiar" los comentarios y respuestas hacia un consenso significativo.
rwong
@rwong ¡Gracias! Siento que la pregunta ya ha recibido una respuesta muy buena y puntual, aunque esto podría haberse hecho mejor en un foro. Retiré mi respuesta al comentario de Robert Harvey después de leer la respuesta aclaratoria de JohnWu, en caso de que se refiera a eso. Si no, ¿tienes algún consejo específico para pastorear?
l0b0
1
Mis mensajes se ven como "No se pudo encontrar a Foo durante la llamada a la barra ()". Problema resuelto. Encogimiento de hombros. La desventaja es que es un poco permeable para ser visto por los clientes, pero de todos modos tendemos a ocultarles los detalles de los mensajes de error, por lo que está disponible solo para los administradores de sistemas que no pudieron darles a los monos que pueden ver algunos nombres de funciones. De lo contrario, sí, una pequeña identificación / código único y agradable hará el truco.
Lightness compite con Mónica el
1
¡Esto es MUY útil cuando un cliente lo llama por teléfono y su computadora no funciona en inglés! Mucho menos de un problema en estos días ya que ahora tenemos de correo electrónico y archivos de registro .....
Ian

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" la traza 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, la 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 / usuario API está tratando de lograr.
    • La idea es que un software acepte y procese comandos provenientes de alguna parte.
    • Durante este proceso, decenas a cientos o miles de llamadas a funciones pueden haber tenido lugar.
    • Nos gustaría que cualquier telemetría generada a lo largo de este proceso sea rastreable hasta el comando de nivel más alto que desencadena 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 la falla en 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 fue escrito para implementar este tipo de requisitos de trazabilidad. Esto hace que la correlación del mensaje de bajo nivel con el comando de alto nivel sea más difícil. El problema es particularmente peor en los sistemas de subprocesos múltiples libremente, donde muchas solicitudes y respuestas pueden superponerse, y el procesamiento puede descargarse a un subproceso diferente que el subproceso de recepción de solicitud original.

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

Incluso las funciones de utilidad necesitarán agregar un argumento "trazador", de modo que si falla, el mensaje de registro permitirá correlacionarse con un cierto comando de alto nivel.

Otra falla que dificultará el rastreo de telemetría es la falta de referencias de objetos (punteros nulos o referencias). Cuando falta algún dato crucial, puede ser imposible informar algo útil para la falla.

En términos de escribir los mensajes de registro:

  • Algunos proyectos de software pueden requerir localización (traducción a un idioma extranjero) incluso para mensajes de registro destinados solo a administradores.
  • Algunos proyectos de software pueden necesitar una separación clara entre datos confidenciales y no confidenciales, incluso con el propósito de iniciar sesión, y los administradores no tendrían la posibilidad de ver accidentalmente ciertos datos confidenciales.
  • No intente ofuscar el mensaje de error. Eso socavaría la confianza de los clientes. Los administradores de los clientes esperan leer esos registros y entenderlos. 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 registro de telemetría y asen a su personal de soporte técnico. Esperan saberlo. Entrene a su personal de soporte técnico para explicar el registro de telemetría correctamente.
rwong
fuente
1
De hecho, AOP ha promocionado, principalmente, su capacidad inherente para resolver este problema, agregando Tracer a cada llamada relevante, con una invasión mínima a la base del código.
obispo
También agregaría a la lista de "escribir mensajes de registro" que es importante caracterizar la falla en términos de "por qué" y "cómo solucionarlo" en lugar de simplemente "qué" sucedió.
obispo
58

Imagine que tiene una función de utilidad trivial que se utiliza en cientos de lugares en su código:

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

Si tuviéramos que hacer lo que sugieres, 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 como resultado una excepción dividir por cero.

Digamos que ve 27349262 en su salida o en sus registros. ¿Dónde busca el código que pasó el valor cero? Recuerde, la función, con su ID única, se usa en cientos de lugares. Entonces, si bien puede saber que se produjo la división por cero, no tiene idea de quién 0es.

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

Si la verbosidad del seguimiento de la pila es lo que te molesta, 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 un seguimiento de pila abreviado que vaya solo a nniveles, puede 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

Tal vez más fácil que mantener identificaciones de mensajes y más flexible.

Robar mi código de DotNetFiddle

John Wu
fuente
32
Hmm, supongo que no hice mi punto lo suficientemente claro. Sé que son únicos Robert ... por ubicación de código . No son únicos por ruta de código . Conocer la ubicación suele ser inútil, por ejemplo, si el verdadero problema es que una entrada no se configuró correctamente. He editado mi idioma ligeramente para enfatizar.
John Wu
1
Buenos puntos, los dos. Hay un problema diferente con los seguimientos de pila, que pueden o no ser un factor decisivo dependiendo de la situación: su tamaño puede hacer que inunden los mensajes, especialmente si desea incluir todo el seguimiento de la pila en lugar de una versión abreviada como algunos idiomas hacer por defecto. Quizás una alternativa sería escribir un registro de seguimiento de la pila por separado e incluir índices numerados para ese registro en la salida de la aplicación.
l0b0
12
Si obtiene tantos de estos que le preocupa inundar su E / S, hay algo muy mal. ¿O solo estás siendo tacaño? El verdadero éxito en el rendimiento es probablemente la pila de relajarse.
John Wu
9
Editado con una solución para acortar los rastros de la pila, en caso de que esté escribiendo registros en un disquete 3.5;)
John Wu
77
@JohnWu Y tampoco olvide "IOException 'File not Found' en [...]" que le informa sobre cincuenta capas de la pila de llamadas pero no le dice qué archivo sangriento exacto no se encontró.
Joker_vD
6

SAP NetWeaver ha estado haciendo esto por décadas.

Ha demostrado ser una herramienta valiosa cuando se solucionan errores en el gigantesco código masivo que es el típico sistema SAP ERP.

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 desee emitir un mensaje de error, solo debe indicar la clase, el número, la gravedad y las variables específicas del mensaje. La representación de texto del mensaje se crea en tiempo de ejecución. Por lo general, ve la clase y el número de mensaje en cualquier contexto donde aparecen los mensajes. Esto tiene varios efectos geniales:

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

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

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

  • Las representaciones de texto de los mensajes son traducibles. Entonces, al alentar el uso de mensajes en lugar de cadenas, también obtienes capacidades i18n.

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

error1

Buscando ese error en el repositorio de errores:

error2

Encuéntralo en la base de código:

error3

Sin embargo, hay inconvenientes. Como puede ver, estas líneas de código ya no se documentan por sí mismas. Cuando lee el código fuente y ve una MESSAGEdeclaración como las de la captura de pantalla anterior, solo puede inferir del contexto lo que realmente significa. Además, a veces las personas implementan controladores de errores personalizados que reciben la clase y el número de mensaje en tiempo de ejecución. En ese caso, el error no se puede encontrar automáticamente o no se puede encontrar en la ubicación donde realmente ocurrió el error. La solución para el primer problema es acostumbrarse a agregar siempre un comentario en el código fuente que le dice al lector lo que significa el mensaje. El segundo se resuelve agregando un código muerto para asegurarse de que la búsqueda automática de mensajes funcione. 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 en las que esto no es posible. Hay, por ejemplo, algunas herramientas de modelado de procesos empresariales basadas en la interfaz de usuario en las que puede configurar mensajes de error para que aparezcan cuando se infrinjan las reglas empresariales. 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 usan. Eso significa que confiar demasiado en la lista donde se usa cuando se trata de encontrar la causa de un error puede ser una pista falsa.

Philipp
fuente
Los catálogos de mensajes también han sido parte de GNU / Linux , y UNIX en general como un estándar POSIX , durante algún tiempo.
obispo
@bishop Por lo general, no estoy programando específicamente para sistemas POSIX, por lo que no estoy familiarizado con él. Tal vez podría publicar otra respuesta que explique los catálogos de mensajes POSIX y lo que el OP podría aprender de su implementación.
Philipp
3
Fui parte de un proyecto que hizo esto en los oughties. Un problema con el que nos encontramos fue que, junto con todo lo demás, colocamos el mensaje humano de "no se pudo conectar a la base de datos" en la base de datos.
JimmyJames
5

El problema con ese enfoque es que conduce a un registro cada vez más detallado. 99.9999% del cual nunca mirará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, recorrer el código y limita su registro a dos lugares por proceso. p.ej.

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

Ahora puedo usar exactamente el mismo estado en mi máquina de desarrollo para reproducir el error, revisando el código en mi depurador y escribiendo una nueva prueba unitaria para confirmar la corrección.

Además, si es necesario, puedo evitar más registros solo registrando fallas o manteniendo el estado en otro lugar (¿base de datos? ¿Cola de mensajes?)

Obviamente tenemos que tener mucho cuidado al registrar datos confidenciales. Entonces, esto funciona particularmente bien si su solución está usando colas de mensajes o el patrón de almacén de eventos. Como el registro solo necesita decir "Mensaje xyz falló"

Ewan
fuente
Poner datos confidenciales en una cola todavía lo está registrando. Esto es desaconsejado, al igual que el almacenamiento de entradas sensibles en el DB sin alguna forma de criptografía.
jpmc26
Si su sistema se queda sin colas o un DB, entonces los datos ya están allí, y también la seguridad. Registrar demasiado es malo porque el registro tiende a quedar fuera de sus controles de seguridad.
Ewan
Bien, pero ese es el punto. No es aconsejable porque los datos permanecen allí permanentemente y generalmente en texto completamente claro. Para los datos confidenciales, es mejor no correr el riesgo y minimizar el período de almacenamiento, y luego ser muy consciente y cuidadoso de cómo lo está almacenando.
jpmc26
Es tradicionalmente permanente porque está escribiendo en un archivo. Pero una cola de error es transitoria.
Ewan
Yo diría que probablemente depende de la implementación (y posiblemente incluso de la configuración) de la cola. No puede volcarlo en ninguna cola y esperar que sea seguro. ¿Y qué pasa después de que la cola se consume? Los registros aún deben estar en algún lugar para que alguien los vea. Además, ese no es un vector de ataque adicional que me gustaría abrir incluso temporalmente. Si un ataque descubre que hay datos confidenciales allí, incluso las entradas más recientes pueden ser valiosas. Y luego existe el riesgo de que alguien no lo sepa y active un interruptor para que también comience a registrarse en el disco. Es solo una lata de gusanos.
jpmc26
1

Sugeriría que iniciar sesión no es la forma de hacerlo, sino que esta circunstancia se considera excepcional (bloquea su programa) y se debe lanzar una excepción. Digamos que su 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 su código de llamada no está configurado para lidiar con el hecho de que Foo no existe y que podría ser:

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 la 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 llamadas:

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 parece mejor bloquearse que intentar avanzar por un camino que puede corrompe su sistema.

Arrozal
fuente
0

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

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

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

Todas estas cosas se pueden hacer de fábrica 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 herramientas como Zipkin . Una ID para rastrear el comportamiento de una acción del usuario en todo el sistema. Al colocar sus registros en un motor de búsqueda central, no solo puede encontrar las acciones más largas, sino que también llama los registros que se aplican a esa acción (como la pila ELK ).

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

Berin Loritsch
fuente