Estamos desarrollando la interfaz más amigable del mundo* para ver registros

Estamos desarrollando la interfaz más amigable del mundo* para ver registros Si alguna vez has utilizado interfaces web para ver logs, seguramente has notado lo engorrosas que suelen ser, y (a menudo) no son muy cómodas ni receptivas. Algunas se pueden habituar, otras son realmente horribles, pero creo que la razón de todos los problemas radica en que abordamos incorrectamente la tarea de ver logs: intentamos crear una interfaz web donde es mejor utilizar la CLI (interface de línea de comandos). Personalmente, me siento muy cómodo trabajando con tail, grep, awk y similares, y por eso la interfaz ideal para manejar logs sería algo similar a tail y grep, pero que se pueda usar para leer logs provenientes de múltiples servidores. Es decir, ¡por supuesto que leerlos desde ClickHouse!

*en la opinión personal de un usuario de Habr youROCK

Conoce logscli

No se me ocurrió un nombre para mi interfaz, y, para ser sincero, más bien existe en forma de prototipo, pero si quieres ver el código fuente de inmediato, eres bienvenido: https://github.com/YuriyNasretdinov/logscli (350 líneas de código seleccionado en Go).

Funcionalidades

Mi objetivo era crear una interfaz que parezca familiar para quienes están acostumbrados a tail/grep, es decir, que soporte las siguientes cosas:

  1. Ver todos los logs, sin filtrado.
  2. Mantener líneas que contengan una subcadena fija (flag -F sa-logic-subsets-canary-vs.yaml grep).
  3. Mantener líneas que coincidan con una expresión regular (flag -E sa-logic-subsets-canary-vs.yaml grep).
  4. Por defecto, la visualización es en orden cronológico inverso, ya que generalmente lo que más interesa son los logs más recientes.
  5. Mostrar el contexto cerca de cada línea (parámetros -A, -B y -C sa-logic-subsets-canary-vs.yaml grep, imprimiendo N líneas antes, después y alrededor de cada línea coincidente respectivamente).
  6. Ver logs entrantes en tiempo real, con y sin filtrado (esencialmente tail -f | grep).
  7. La interfaz debe ser compatible con less, head, tail y otros, por defecto los resultados deben ser devueltos sin restricciones en su cantidad; las líneas se imprimen en streaming mientras el usuario esté interesado en recibirlas; la señal SIGPIPE debe interrumpir silenciosamente el streaming de logs, como lo hacen tail, grep y otras utilidades de UNIX.

Implementación

Supondré que ya sabes de alguna manera cómo entregar logs a ClickHouse. Si no, te recomiendo probar lsd y kittenhouse, así como este artículo sobre la entrega de logs.

Para comenzar, es necesario definir el esquema de la base de datos. Dado que por lo general se desea obtener los registros ordenados por tiempo, parece lógico almacenarlos de esa manera. Si hay muchas categorías de registros y todas son similares, se puede hacer de la categoría de registros la primera columna de la clave primaria; esto permitirá tener una única tabla en lugar de varias, lo que será una gran ventaja al insertar en ClickHouse (en servidores con discos duros, se recomienda insertar datos no más de ~1 vez por segundo para todo el servidor).

Es decir, necesitamos aproximadamente el siguiente esquema de tablas:

CREATE TABLE logs(
    category LowCardinality(String), -- categoría de registros (opcional)
    time DateTime, -- tiempo del evento
    millis UInt16, -- milisegundos (también se pueden usar microsegundos, etc.): se recomienda almacenar, si hay muchos eventos, para facilitar la diferenciación entre ellos
    ..., -- tus propios campos, como el nombre del servidor, el nivel de registro, etc.
    message String -- texto del mensaje
) ENGINE=MergeTree()
ORDER BY (category, time, millis)

Desafortunadamente, no pude encontrar inmediatamente fuentes abiertas con registros realistas que pudieran ser descargados, así que tomé en su lugar como ejemplo reseñas de productos de Amazon hasta 2015. Sin duda, su estructura no es exactamente la misma que la de los registros de texto, pero para propósitos ilustrativos esto no es crucial.

instrucción para cargar reseñas de Amazon en ClickHouse

Creemos una tabla:

CREATE TABLE amazon(
   review_date Date,
   time DateTime DEFAULT toDateTime(toUInt32(review_date) * 86400 + rand() % 86400),
   millis UInt16 DEFAULT rand() % 1000,
   marketplace LowCardinality(String),
   customer_id Int64,
   review_id String,
   product_id LowCardinality(String),
   product_parent Int64,
   product_title String,
   product_category LowCardinality(String),
   star_rating UInt8,
   helpful_votes UInt32,
   total_votes UInt32,
   vine FixedString(1),
   verified_purchase FixedString(1),
   review_headline String,
   review_body String
)
ENGINE=MergeTree()
ORDER BY (time, millis)
SETTINGS index_granularity=8192

En el conjunto de datos de Amazon solo hay una fecha para la reseña, pero no un tiempo exacto, así que llenaremos estos datos al azar.

No es necesario descargar todos los archivos tsv y se puede limitar a los primeros ~10-20 para obtener ya un conjunto de datos suficientemente grande que no quepa en 16 GB de RAM. Para cargar los archivos TSV, utilicé el siguiente comando:

for i in *.tsv; do
    echo $i;
    tail -n +2 $i | pv |
    clickhouse-client --input_format_allow_errors_ratio 0.5 --query='INSERT INTO amazon(marketplace,customer_id,review_id,product_id,product_parent,product_title,product_category,star_rating,helpful_votes,total_votes,vine,verified_purchase,review_headline,review_body,review_date) FORMAT TabSeparated'
done

En un disco persistente estándar (que es HDD) en Google Cloud de 1000 GB (tamaño que elegí principalmente para que la velocidad fuera un poco mayor, aunque posiblemente un SSD de volumen adecuado hubiera salido más barato), la velocidad de carga fue de aproximadamente ~75 MB/seg en 4 núcleos.

  • Debo aclarar que trabajo en Google, pero utilicé una cuenta personal y este artículo no tiene relación con mi trabajo en la empresa.

Todas las ilustraciones las realizaré con este conjunto de datos, ya que es todo lo que tenía a mano.

Mostrar el progreso del escaneo de datos

Dado que en ClickHouse utilizaremos un escaneo completo de la tabla de registros y esta operación puede tomar un tiempo considerable y no devolver resultados durante mucho tiempo si se encuentran pocas coincidencias, es deseable poder mostrar el progreso de la ejecución de la consulta antes de obtener las primeras filas con resultados. Para ello, en la interfaz HTTP hay un parámetro que permite enviar el progreso en los encabezados HTTP: send_progress_in_http_headers=1. Lamentablemente, la biblioteca estándar de Go no puede leer los encabezados a medida que los recibe, pero la interfaz HTTP 1.0 (no confundir con 1.1) es compatible con ClickHouse, por lo que es posible abrir una conexión TCP en bruto con ClickHouse, enviar ahí GET /?query=... HTTP/1.0nn y obtener en respuesta los encabezados y el cuerpo de la respuesta sin ninguna escapatoria ni cifrado, por lo que en este caso no necesitamos utilizar la biblioteca estándar.

Transmisión de registros desde ClickHouse

En ClickHouse, desde hace relativamente tiempo (¿desde 2019?), existe una optimización para consultas con ORDER BY, así que una consulta del tipo

SELECT time, millis, message
FROM logs
WHERE message LIKE '%something%'
ORDER BY time DESC, millis DESC

comenzará a devolver inmediatamente las filas donde el mensaje contiene la subcadena "something", sin esperar a que termine el escaneo.

Además, sería muy conveniente si ClickHouse cancelara la consulta automáticamente cuando se cerrara la conexión, aunque este no es el comportamiento predeterminado. La cancelación automática de la consulta se puede habilitar con la opción cancel_http_readonly_queries_on_client_close=1.

Manejo correcto de SIGPIPE en Go

Cuando ejecutas, digamos, el comando some_cmd | head -n 10, ¿cómo exactamente finaliza el comando some_cmd su ejecución cuando head ha leído 10 líneas? La respuesta es simple: cuando head finaliza, el pipe se cierra y stdout del comando some_cmd pasa a apuntar, condicionalmente, a "a ninguna parte". Cuando some_cmd intenta escribir en un pipe cerrado, recibe una señal SIGPIPE, que por defecto finaliza el programa en silencio..

En Go, esto también ocurre por defecto, pero el manejador de señales SIGPIPE al final también imprime "signal: SIGPIPE" o un mensaje similar, y para eliminar este mensaje, simplemente hay que manejar SIGPIPE de la forma que deseemos, es decir, salir silenciosamente.

ch := make(chan os.Signal)
signal.Notify(ch, syscall.SIGPIPE)
go func() {
    <-ch
    os.Exit(0)
}()

Mostrar contexto del mensaje

Frecuentemente se desea ver el contexto en el que ocurrió un error (por ejemplo, qué solicitud provocó la caída, o qué problemas acompañantes eran visibles antes de la falla), grep y para esto sirven las opciones -A, -B y -C, que muestran el número especificado de líneas después, antes y alrededor del mensaje respectivamente.

Lamentablemente, no encontré una forma simple de hacer lo mismo en ClickHouse, por lo tanto, para mostrar el contexto, se envía una consulta adicional para cada línea del resultado con un formato aproximadamente similar (los detalles dependen de la clasificación y de si se muestra el contexto antes o después):

SELECT time,millis,review_body FROM amazon
WHERE (time = 'TIEMPO_DEL_EVENTO' AND millis < MILISEGUNDOS_DEL_EVENTO) OR (time < 'TIEMPO_DEL_EVENTO')
ORDER BY time DESC, millis DESC
LIMIT CANTIDAD_DE_LINEAS_DEL_CONTEXTO
SETTINGS max_threads=1

Dado que la consulta se envía casi inmediatamente después de que ClickHouse devolvió la línea correspondiente, esta se almacena en caché y, en general, la consulta se ejecuta bastante rápido y utiliza un poco de CPU (normalmente, la consulta toma alrededor de ~6 ms en mi máquina virtual).

Mostrar nuevos mensajes en tiempo real

Para mostrar los mensajes entrantes en un modo (casi) en tiempo real, simplemente ejecutamos la consulta cada pocos segundos, recordando la última marca de tiempo que encontramos hasta ese momento.

Ejemplos de comandos

¿Cómo se ven los comandos típicos de logscli en la práctica?

Si ha cargado el conjunto de datos de Amazon que mencioné al principio del artículo, podrá ejecutar los siguientes comandos:

# Показать строки, где встречается слово walmart
$ logscli -F 'walmart' | less

# Показать самые свежие 10 строк, где встречается "terrible"
$ logscli -F terrible -limit 10

# То же самое без -limit:
$ logscli -F terrible | head -n 10

# Показать все строки, подходящие под /times [0-9]/, написанные для vine и у которых высокий рейтинг
$ logscli -E 'times [0-9]' -where="vine='Y' AND star_rating>4" | less

# Показать все строки со словом "panic" и 3 строки контекста вокруг
$ logscli -F 'panic' -C 3 | less

# Непрерывно показывать новые строки со словом "5-star"
$ logscli -F '5-star' -tailf

Enlaces

El código de la utilidad (sin documentación) está disponible en github en la dirección https://github.com/YuriyNasretdinov/logscli. Estaré encantado de escuchar sus pensamientos sobre mi idea para una interfaz de consola para ver registros basada en ClickHouse.

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster