Las organizaciones que dependen de sistemas distribuidos a menudo escriben sus aplicaciones en Go para aprovechar las características de concurrencia como los canales y goroutines (por ejemplo, Heroku, Basecamp, Cockroach Labs y Datadog). Si eres responsable de construir o dar soporte a aplicaciones Go, una estrategia de logging bien pensada puede ayudarte a entender el comportamiento de los usuarios, localizar errores y monitorizar el rendimiento de tus aplicaciones.
Este post te mostrará algunas herramientas y técnicas para gestionar los logs de Golang. Comenzaremos con la cuestión de qué paquete de registro utilizar para diferentes tipos de requisitos. A continuación, explicaremos algunas técnicas para hacer que sus registros sean más buscables y fiables, reduciendo la huella de recursos de su configuración de registro, y la estandarización de sus mensajes de registro.
- Conozca su paquete de registro
- Usa log para simplificar
- Usa logrus para registros formateados
- Usa glog si te preocupa el volumen
- Mejores prácticas para escribir y almacenar los registros de Golang
- Evite declarar goroutines para el registro
- Escriba sus registros en un archivo
- Implementar una interfaz de registro estándar
- Centralizar los logs de Golang
- Rastrear los registros de Golang a través de microservicios
- Los registros de Golang limpios y completos
Conozca su paquete de registro
Go le da una gran cantidad de opciones al elegir un paquete de registro, y vamos a explorar varios de ellos a continuación. Mientras que logrus es la más popular de las bibliotecas que cubrimos, y le ayuda a implementar un formato de registro consistente, los otros tienen casos de uso especializados que vale la pena mencionar. Esta sección examinará las bibliotecas log, logrus y glog.
Usa log para simplificar
La biblioteca de registro incorporada de Golang, llamada log
, viene con un registrador por defecto que escribe en el error estándar y añade una marca de tiempo sin necesidad de configuración. Usted puede utilizar estos registros en bruto y listo para el desarrollo local, cuando la obtención de una rápida retroalimentación de su código puede ser más importante que la generación de registros ricos y estructurados.
Por ejemplo, puede definir una función de división que devuelve un error a la persona que llama, en lugar de salir del programa, cuando se intenta dividir por cero.
package mainimport ( "log" "errors" "fmt" )func divide(a float32, b float32) (float32, error) { if b == 0 { return 0, errors.New("can't divide by zero") } return a / b, nil}func main() { var a float32 = 10 var b float32 ret, err := divide(a,b) if err != nil{ log.Print(err) } fmt.Println(ret)}
Debido a que nuestro ejemplo divide por cero, dará como resultado el siguiente mensaje de registro:
2019/01/31 11:48:00 can't divide by zero
Usa logrus para registros formateados
Recomendamos escribir los registros de Golang usando logrus, un paquete de registro diseñado para el registro estructurado que es muy adecuado para el registro en JSON. El formato JSON hace posible que las máquinas analicen fácilmente sus registros de Golang. Y puesto que JSON es un estándar bien definido, hace que sea sencillo añadir contexto mediante la inclusión de nuevos campos-un analizador debería ser capaz de recogerlos automáticamente.
Usando logrus, puede definir campos estándar para añadir a sus registros JSON utilizando la función WithFields
, como se muestra a continuación. A continuación, puede hacer llamadas al registrador en diferentes niveles, como Info()
, Warn()
y Error()
. La biblioteca logrus escribirá el registro como JSON automáticamente e insertará los campos estándar, junto con cualquier campo que hayas definido sobre la marcha.
package mainimport ( log "github.com/sirupsen/logrus")func main() { log.SetFormatter(&log.JSONFormatter{}) standardFields := log.Fields{ "hostname": "staging-1", "appname": "foo-app", "session": "1ce3f6v", } log.WithFields(standardFields).WithFields(log.Fields{"string": "foo", "int": 1, "float": 1.1}).Info("My first ssl event from Golang")}
El registro resultante incluirá el mensaje, el nivel de registro, la marca de tiempo y los campos estándar en un objeto JSON:
{"appname":"foo-app","float":1.1,"hostname":"staging-1","int":1,"level":"info","msg":"My first ssl event from Golang","session":"1ce3f6v","string":"foo","time":"2019-03-06T13:37:12-05:00"}
Usa glog si te preocupa el volumen
Algunas librerías de registro te permiten habilitar o deshabilitar el registro en niveles específicos, lo cual es útil para mantener el volumen del registro bajo control cuando te mueves entre desarrollo y producción. Una de estas bibliotecas es glog
, que le permite utilizar banderas en la línea de comandos (por ejemplo, -v
para la verbosidad) para establecer el nivel de registro cuando ejecuta su código. A continuación, puede utilizar una función V()
en las declaraciones if
para escribir sus registros Golang sólo en un determinado nivel de registro.
Por ejemplo, puede utilizar glog para escribir el mismo error «No se puede dividir por cero» de antes, pero sólo si está registrando en el nivel de verbosidad de 2
. Puede establecer la verbosidad a cualquier número entero de 32 bits con signo, o utilizar las funciones Info()
, Warning()
, Error()
y Fatal()
para asignar los niveles de verbosidad 0
a 3
(respectivamente).
if err != nil && glog.V(2){ glog.Warning(err) }
Puede hacer que su aplicación consuma menos recursos registrando sólo ciertos niveles en producción. Al mismo tiempo, si no hay impacto en los usuarios, a menudo es una buena idea registrar tantas interacciones con su aplicación como sea posible, a continuación, utilizar el software de gestión de registros como Datadog para encontrar los datos que necesita para su investigación
Mejores prácticas para escribir y almacenar los registros de Golang
Una vez que haya elegido una biblioteca de registro, también querrá planificar en qué parte de su código hacer llamadas al registrador, cómo almacenar sus registros, y cómo darles sentido. En esta sección, vamos a recomendar una serie de buenas prácticas para organizar sus registros de Golang:
- Haga llamadas al registrador desde el proceso principal de su aplicación, no dentro de goroutines.
- Escriba los registros de su aplicación en un archivo local, incluso si los envía a una plataforma central más tarde.
- Estandarice sus registros con un conjunto de mensajes predefinidos.
- Envíe sus registros a una plataforma central para que pueda analizarlos y agregarlos.
- Use cabeceras HTTP e identificadores únicos para registrar el comportamiento de los usuarios en los microservicios.
Evite declarar goroutines para el registro
Hay dos razones para evitar la creación de sus propias goroutines para manejar la escritura de registros. Primero, puede llevar a problemas de concurrencia, ya que duplicados del logger intentarían acceder al mismo io.Writer
. En segundo lugar, las bibliotecas de registro suelen iniciar goroutines por sí mismas, gestionando cualquier problema de concurrencia internamente, y el inicio de sus propias goroutines sólo interferirá.
Escriba sus registros en un archivo
Incluso si está enviando sus registros a una plataforma central, recomendamos escribirlos en un archivo en su máquina local primero. Querrá asegurarse de que sus registros estén siempre disponibles localmente y no se pierdan en la red. Además, escribir en un archivo significa que puede desacoplar la tarea de escribir sus registros de la tarea de enviarlos a una plataforma central. Sus aplicaciones no necesitarán establecer conexiones o transmitir sus registros, y puede dejar estas tareas a software especializado como el Agente Datadog. Si está ejecutando sus aplicaciones Go dentro de una infraestructura en contenedores que aún no incluye almacenamiento persistente, por ejemplo, contenedores que se ejecutan en AWS Fargate, es posible que desee configurar su herramienta de gestión de registros para recopilar registros directamente de los flujos STDOUT y STDERR de sus contenedores (esto se maneja de manera diferente en Docker y Kubernetes).
Implementar una interfaz de registro estándar
Cuando se escriben llamadas a los registradores desde dentro de su código, los equipos a menudo utilizan diferentes nombres de atributos para describir la misma cosa. Los atributos inconsistentes pueden confundir a los usuarios y hacer imposible la correlación de registros que deberían formar parte de la misma imagen. Por ejemplo, dos desarrolladores pueden registrar el mismo error, un nombre de cliente faltante cuando se maneja una carga, de diferentes maneras.
Una buena manera de hacer cumplir la estandarización es crear una interfaz entre el código de su aplicación y la biblioteca de registro. La interfaz contiene mensajes de registro predefinidos que implementan un determinado formato, lo que facilita la investigación de los problemas al asegurar que los mensajes de registro pueden ser buscados, agrupados y filtrados.
En este ejemplo, declararemos un tipo Event
con un mensaje predefinido. Luego usaremos mensajes Event
para hacer llamadas a un logger. Los compañeros de equipo pueden escribir registros de Golang proporcionando una cantidad mínima de información personalizada, dejando que la aplicación haga el trabajo de implementar un formato estándar.
Primero, escribiremos un paquete logwrapper
que los desarrolladores pueden incluir dentro de su código.
package logwrapperimport ( "github.com/sirupsen/logrus")// Event stores messages to log later, from our standard interfacetype Event struct { id int message string}// StandardLogger enforces specific log message formatstype StandardLogger struct { *logrus.Logger}// NewLogger initializes the standard loggerfunc NewLogger() *StandardLogger {var baseLogger = logrus.New()var standardLogger = &StandardLogger{baseLogger}standardLogger.Formatter = &logrus.JSONFormatter{}return standardLogger}// Declare variables to store log messages as new Eventsvar ( invalidArgMessage = Event{1, "Invalid arg: %s"} invalidArgValueMessage = Event{2, "Invalid value for argument: %s: %v"} missingArgMessage = Event{3, "Missing arg: %s"})// InvalidArg is a standard error messagefunc (l *StandardLogger) InvalidArg(argumentName string) { l.Errorf(invalidArgMessage.message, argumentName)}// InvalidArgValue is a standard error messagefunc (l *StandardLogger) InvalidArgValue(argumentName string, argumentValue string) { l.Errorf(invalidArgValueMessage.message, argumentName, argumentValue)}// MissingArg is a standard error messagefunc (l *StandardLogger) MissingArg(argumentName string) { l.Errorf(missingArgMessage.message, argumentName)}
Para utilizar nuestra interfaz de registro, sólo tenemos que incluirla en nuestro código y hacer llamadas a una instancia de StandardLogger
.
package mainimport ( li "<PATH_TO_PACKAGE>/logwrapper")func main() { var standardLogger = := li.NewLogger() // You can then call a method of our standard logger in the context of an error // you would like to log. standardLogger.InvalidArgValue("client", "nil")}
Cuando ejecutemos nuestro código, obtendremos el siguiente log JSON:
{"level":"error","msg":"Invalid value for argument: client: nil","time":"2019-03-04T11:21:07-05:00"}
Centralizar los logs de Golang
Si tu aplicación está desplegada en un cluster de hosts, no es sostenible entrar por SSH en cada uno de ellos para hacer tail, grep, e investigar tus logs. Una alternativa más escalable es pasar los registros de los archivos locales a una plataforma central.
Una solución es utilizar el paquete syslog de Golang para reenviar los registros de toda su infraestructura a un único servidor syslog.
Personalice y agilice su gestión de registros de Golang con Datadog.
Otra es utilizar una solución de gestión de registros. Datadog, por ejemplo, puede seguir sus archivos de registro y reenviar los registros a una plataforma central para su procesamiento y análisis.
Puede utilizar atributos para graficar los valores de ciertos campos de registro a lo largo del tiempo, ordenados por grupo. Por ejemplo, podrías hacer un seguimiento del número de errores por service
para saber si hay una incidencia en uno de tus servicios. Mostrando los registros sólo del servicio go-logging-demo
, podemos ver cuántos registros de error ha producido este servicio en un intervalo determinado.
También puedes utilizar los atributos para profundizar en las posibles causas, por ejemplo, ver si un pico de registros de error pertenece a un host específico. A continuación, puede crear una alerta automatizada basada en los valores de sus registros.
Rastrear los registros de Golang a través de microservicios
Cuando se soluciona un error, a menudo es útil ver qué patrón de comportamiento condujo a él, incluso si ese comportamiento implica una serie de microservicios. Puedes lograr esto con el rastreo distribuido, visualizando el orden en el que tu aplicación ejecuta funciones, consultas a la base de datos y otras tareas, y siguiendo estos pasos de ejecución mientras hacen su camino a través de una red. Una forma de implementar el rastreo distribuido dentro de sus registros es pasar información contextual como cabeceras HTTP.
En este ejemplo, un microservicio recibe una solicitud y comprueba si hay un ID de rastreo en la cabecera x-trace
, generando uno si no existe. Al hacer una petición a otro microservicio, generamos entonces un nuevo spanID -para esta y para cada petición- y lo añadimos a la cabecera x-span
.
func microService1(w http.ResponseWriter, r *http.Request) { client := &http.Client{} trace := r.Header.Get("x-trace") if ( trace == "") { trace = generateTraceId() } span := generateSpanId() // Hit the second microservice with the appropriate headers reqService2, _ := http.NewRequest("GET", "<ADDRESS>", nil) reqService2.Header.Add("x-trace", trace) reqService2.Header.Add("x-span", span) resService2, _ := client.Do(reqService2)}
Los microservicios descendentes utilizan las cabeceras x-span
de las peticiones entrantes para especificar los padres de los spans que generan, y envían esa información como la cabecera x-parent
al siguiente microservicio de la cadena.
func microService2(w http.ResponseWriter, r *http.Request) { trace := r.Header.Get("x-trace") span := generateSpanId() parent := r.Header.Get("x-span") if (trace == "") { w.Header().Set("x-parent", parent) } w.Header().Set("x-trace", trace) w.Header().Set("x-span", span) if (parent == "") { w.Header().Set("x-parent", span) } w.WriteHeader(http.StatusOK) io.WriteString(w, fmt.Sprintf(aResponseMessage, 2, trace, span, parent))}
Si se produce un error en uno de nuestros microservicios, podemos utilizar los atributos trace
, parent
y span
para ver la ruta que ha seguido una petición, permitiéndonos saber qué hosts -y posiblemente qué partes del código de la aplicación- debemos investigar.
En el primer microservicio:
{"appname":"go-logging","level":"debug","msg":"Hello from Microservice One","trace":"eUBrVfdw","time":"2017-03-02T15:29:26+01:00","span":"UzWHRihF"}
En el segundo:
{"appname":"go-logging","level":"debug","msg":"Hello from Microservice Two","parent":"UzWHRihF","trace":"eUBrVfdw","time":"2017-03-02T15:29:26+01:00","span":"DPRHBMuE"}
Si quieres profundizar en las posibilidades de rastreo de Golang, puedes utilizar una librería de rastreo como OpenTracing o una plataforma de monitorización que soporte el rastreo distribuido para aplicaciones Go. Por ejemplo, Datadog puede construir automáticamente un mapa de servicios utilizando los datos de su biblioteca de rastreo de Golang; visualizar las tendencias en sus rastros a lo largo del tiempo; y hacerle saber acerca de los servicios con tasas de solicitud inusuales, tasas de error o latencia.
Los registros de Golang limpios y completos
En este post, hemos destacado los beneficios y las ventajas de varias bibliotecas de registro de Go. También hemos recomendado formas de asegurar que sus registros estén disponibles y accesibles cuando los necesite, y que la información que contienen sea consistente y fácil de analizar.
Para empezar a analizar todos sus registros de Go con Datadog, regístrese para una prueba gratuita.