Posts

De print a Logger: registros útiles para depurar tus apps

Arturo Rivas Arias

Un print("Error al guardar") permite comprobar rápidamente si entramos en una rama del código. Pero cuando aparecen decenas de mensajes, cuesta reconstruir qué ocurrió y desde qué componente. Y si el fallo sucede en el dispositivo de otra persona, necesitamos algo más que la salida de una sesión de depuración.

Apple ofrece un sistema de registro unificado que incorpora niveles de severidad, categorías y controles de privacidad. Su API Logger, disponible desde iOS 14 y macOS 11, permite registrar información que después podemos consultar con herramientas del sistema.

Organizar los mensajes desde el principio

Un Logger se configura con un subsistema, normalmente el identificador de la aplicación, y una categoría que identifica un área funcional. Podemos dedicar categorías al guardado, las notificaciones o la reproducción de audio. Así filtramos por responsabilidades sin buscar palabras sueltas entre todos los mensajes.

Imaginemos una app que guarda recordatorios para regar plantas. Queremos distinguir cuándo empieza un guardado, cuándo termina y cuándo falla:

import Foundation
import OSLog

struct WateringReminder: Codable {
    let plantName: String
    let nextWatering: Date
}

struct ReminderStore {
    private let logger = Logger(
        subsystem: "com.example.Riego",
        category: "Guardado"
    )

    func save(_ reminders: [WateringReminder], to url: URL) throws {
        let operationID = UUID().uuidString

        logger.notice("Inicio de guardado: \(operationID, privacy: .public)")
        logger.debug("Recordatorios pendientes: \(reminders.count)")

        do {
            let data = try JSONEncoder().encode(reminders)
            try data.write(to: url, options: .atomic)
            logger.notice("Guardado completado: \(operationID, privacy: .public)")
        } catch {
            logger.error("Guardado fallido: \(operationID, privacy: .public); detalle: \(error.localizedDescription, privacy: .private)")
            throw error
        }
    }
}

El identificador aleatorio permite relacionar los mensajes de una misma operación. Lo marcamos como público porque se genera para este diagnóstico y no contiene información del usuario. Además, registrar un error no lo resuelve: lo propagamos para que quien llama decida cómo reaccionar.

El nivel también determina qué podremos recuperar

Los niveles de registro expresan la importancia del mensaje y afectan a su conservación:

NivelUso habitualConservación predeterminada
debugDetalles para depurarNo se guarda en disco
infoContexto complementarioNormalmente en memoria; puede recogerse con log
noticeEventos relevantes para el diagnósticoEn disco, con límites
errorFallos durante la ejecuciónEn disco, con límites
faultPosibles defectos o supuestos internos incumplidosEn disco, con límites

No conviene utilizar error para que cualquier mensaje destaque. Tampoco debemos confiar en debug para investigar días después un fallo: los registros no son un historial permanente. La configuración y los límites de almacenamiento condicionan qué información sigue disponible.

La interpolación importa para el rendimiento

Como explica Apple en Explore logging in Swift, Logger conserva una representación optimizada del mensaje y sus argumentos, en lugar de construir inmediatamente una cadena completa. Esto permite aplazar el formato y evitar trabajo cuando un mensaje de depuración se descarta.

Para aprovecharlo, pasa el literal interpolado directamente al método. Si calculas antes una descripción costosa y la guardas en una variable, ese cálculo ya se habrá ejecutado aunque después el mensaje no se registre. Tampoco introduzcas lógica de negocio dentro de la interpolación: su evaluación puede omitirse.

Revisar la privacidad de cada valor

Por defecto, las cadenas y los objetos interpolados se ocultan, mientras que los valores numéricos son visibles. Por eso, un identificador personal representado como Int también necesita una decisión explícita de privacidad.

Las opciones de OSLogPrivacy permiten marcar valores como .private o .public. En el ejemplo ocultamos la descripción del error porque podría incorporar rutas o nombres de archivos. Si añadimos el nombre de una planta, también podemos protegerlo:

let logger = Logger(subsystem: "com.example.Riego", category: "Recordatorios")
let plantName = "Orquídea del despacho"
logger.debug("Recordatorio seleccionado: \(plantName, privacy: .private)")

Hacer público un valor solo para verlo durante una prueba puede exponerlo en diagnósticos posteriores. Conviene decidir qué información necesitamos antes de registrarla.

Consultar los registros con contexto

La consola de depuración de Xcode permite filtrar por nivel, subsistema y categoría, además de consultar metadatos y localizar la llamada en el código. La app Consola de macOS también permite observar mensajes de dispositivos conectados.

Para observar los del Mac desde Terminal:

log stream --level debug \
  --predicate 'subsystem == "com.example.Riego"' \
  --style compact

Este comando muestra eventos en directo; no recupera mensajes debug antiguos ni selecciona automáticamente un iPhone conectado.

Una migración gradual puede empezar por los fallos, continuar con los eventos relevantes y reservar debug para el detalle. print() sigue siendo adecuado para producir la salida de una herramienta de Terminal. Para diagnosticar una app, Logger aporta el contexto que necesitamos sin diseñar nuestro propio sistema de registros.