# Laravel Horizon Job Tagging System

## Overview

Este documento describe el sistema de tags (etiquetas) para jobs en Laravel Horizon, implementado a través del trait `HorizonTaggable`. Este sistema permite identificar, filtrar y depurar jobs de manera eficiente en el dashboard de Horizon.

## Problema que Resuelve

### Antes: Debugging Difícil
- ❌ Jobs mezclados de múltiples tenants sin identificación clara
- ❌ Imposible filtrar por cliente o tipo de job en Horizon
- ❌ Dificultad para rastrear jobs de un tenant específico
- ❌ No hay manera de agrupar jobs relacionados

### Después: Debugging Eficiente
- ✅ Tags automáticos para cada job: `job:ProcessNode`, `tenant:7675435`, `db:1747675435`
- ✅ Búsqueda fácil en Horizon: buscar `tenant:7675435` muestra todos los jobs de ese cliente
- ✅ Identificación rápida de jobs fallidos por tipo o tenant
- ✅ Normalización automática usando la tabla `instances`

## Implementación

### Trait: `App\Traits\HorizonTaggable`

El trait proporciona el método `tags()` requerido por Horizon y normaliza la información del tenant consultando la tabla `instances`.

```php
<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Traits\HorizonTaggable;

class ProcessNode implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
    use HorizonTaggable; // ← Agregar este trait
    
    protected $tenantDatabase; // o $database, o $tenantId
    protected $node_id;
    protected $flow_id;
    
    public function __construct($node_id, $node_type, $node_data_id, $tenantDatabase, $flow_id)
    {
        $this->node_id = $node_id;
        $this->tenantDatabase = $tenantDatabase;
        $this->flow_id = $flow_id;
    }
    
    // Opcionalmente agregar tags adicionales específicos del job
    protected function additionalTags(): array
    {
        return [
            'node:' . $this->node_id,
            'flow:' . $this->flow_id
        ];
    }
    
    public function handle()
    {
        // ... lógica del job
    }
}
```

## Tags Generados Automáticamente

### Tags Principales (Siempre Presentes)

| Tag | Ejemplo | Descripción |
|-----|---------|-------------|
| `job:{ClassName}` | `job:ProcessNode` | Nombre de la clase del job (sin namespace) |
| `tenant:{id}` | `tenant:7675435` | ID del tenant (desde tabla instances) |
| `db:{database}` | `db:1747675435` | Nombre de la base de datos del tenant |
| `domain:{id}` | `domain:8537646` | ID extraído del dominio (de `8537646.app.suitex.io`) |

### Tags Condicionales

| Tag | Ejemplo | Condición | Descripción |
|-----|---------|-----------|-------------|
| `instance:{name}` | `instance:Staging` | Solo si `name ≠ "Production"` | Nombre de la instancia |
| `account:{id}` | `account:9999999` | Solo si `account_id ≠ tenant_id` | ID de la cuenta si difiere |

## Normalización de Tenant

El trait acepta **cualquiera de estos formatos** en el job:

```php
// Formato 1: tenantDatabase (Prioridad 1)
protected $tenantDatabase = 'sx_db_1747675435';

// Formato 2: database (Prioridad 2)
protected $database = '1759781490';
protected $database = 'sx_db_1759781490'; // También funciona

// Formato 3: tenantId (Prioridad 3)
protected $tenantId = 7675435;
```

Todos se **normalizan automáticamente** consultando la tabla `instances`:

```sql
SELECT * FROM instances WHERE database = '1747675435';
-- o
SELECT * FROM instances WHERE id = 7675435;
```

## Uso en Horizon

### Buscar Jobs por Tenant

En el dashboard de Horizon, puedes buscar:

```
tenant:7675435
```

Resultado: Todos los jobs de ese cliente.

### Buscar Jobs por Tipo

```
job:ImportJobCoordinator
```

Resultado: Todos los jobs de ese tipo.

### Búsquedas Combinadas

```
job:ProcessNode AND tenant:7675435
```

Resultado: Solo los ProcessNode de ese tenant.

```
job:ImportJobCoordinator AND domain:8537646
```

Resultado: ImportJobCoordinator del dominio específico.

### Filtrar Jobs Fallidos

En la pestaña "Failed Jobs" de Horizon:

```
tenant:9781490
```

Resultado: Solo los jobs fallidos de ese tenant.

## Características Técnicas

### 1. Uso Explícito de Conexión MySQL

El trait **siempre** usa la conexión `mysql` explícitamente para consultar la tabla `instances`:

```php
Instance::on('mysql')->find($identifier['value']);
Instance::on('mysql')->where('database', $identifier['value'])->first();
```

**¿Por qué?**
- La tabla `instances` solo existe en la base de datos principal
- Los jobs cambian entre conexiones durante su ejecución
- Esto garantiza que el trait funcione sin importar la conexión activa

### 2. Cacheo Inteligente

Las consultas a `instances` se cachean por **1 hora**:

```php
Cache::remember("horizon_tag:tenant_info:database:1747675435", 3600, function() {
    return Instance::on('mysql')->where('database', '1747675435')->first();
});
```

**Beneficios:**
- Evita queries repetidas para el mismo tenant
- Mejora performance en jobs frecuentes
- Cache se limpia automáticamente

### 3. Manejo Robusto de Errores

Si el trait **no puede** resolver el tenant:
- ✅ No falla el job
- ✅ Solo genera el tag básico `job:{ClassName}`
- ✅ Logea en nivel `debug` (no contamina logs)

```php
// Job sin tenant identificable
['job:TestJobWithoutTenant']

// Job con tenant válido
['job:ProcessNode', 'tenant:7675435', 'db:1747675435', 'domain:8537646']
```

### 4. Tags Adicionales Personalizados

Los jobs pueden agregar **tags específicos** implementando `additionalTags()`:

```php
class ProcessNode implements ShouldQueue
{
    use HorizonTaggable;
    
    protected $node_id;
    protected $flow_id;
    
    protected function additionalTags(): array
    {
        return [
            'node:' . $this->node_id,
            'flow:' . $this->flow_id,
            'queue:' . $this->queueName
        ];
    }
}
```

**Resultado en Horizon:**
```
job:ProcessNode
tenant:7675435
db:1747675435
domain:8537646
node:42
flow:123
queue:default
```

## Patrones de Uso

### Patrón 1: Job Simple (Solo Tenant)

```php
class ProcessQueuedJobs implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
    use HorizonTaggable;
    
    protected $database;
    
    public function __construct($database)
    {
        $this->database = $database;
    }
}
```

### Patrón 2: Job con Contexto Adicional

```php
class ImportJobCoordinator implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
    use HorizonTaggable;
    
    protected $tenantDatabase;
    protected $jobId;
    protected $integrationId;
    
    protected function additionalTags(): array
    {
        return [
            'import_job:' . $this->jobId,
            'integration:' . $this->integrationId
        ];
    }
}
```

### Patrón 3: Job sin Tenant (Tarea Global)

```php
class CleanupOldLogsJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
    use HorizonTaggable;
    
    // Sin propiedades de tenant - solo generará tag:job
}
```

## Jobs Actuales con el Trait

Los siguientes jobs ya implementan `HorizonTaggable`:

| Job | Tenant Property | Tags Adicionales |
|-----|----------------|------------------|
| `ProcessNode` | `$tenantDatabase` | `node:*`, `flow:*` |
| `ProcessFlow` | `$tenantDatabase` | `flow:*` |
| `ProcessApiRequest` | `$tenantDatabase` | `flow:*`, `node_metric:*` |
| `ImportJobCoordinator` | `$tenantDatabase` | `import_job:*`, `integration:*` |

## Testing

### Cobertura de Tests

El trait incluye **15 tests con 34 aserciones** que verifican:

✅ Generación de tags básicos
✅ Normalización de formatos de tenant
✅ Cacheo de información
✅ Manejo de tenants inexistentes
✅ Tags condicionales (instance, account)
✅ Prioridad de propiedades
✅ Limpieza de dominios
✅ Conexión explícita a MySQL
✅ Merge de tags adicionales

### Ejecutar Tests

```bash
./vendor/bin/pest tests/Unit/Traits/HorizonTaggableTest.php
```

**Resultado esperado:**
```
Tests:    15 passed (34 assertions)
Duration: 1.11s
```

## Migración de Jobs Existentes

### Paso 1: Agregar el Trait

```php
use App\Traits\HorizonTaggable;

class YourJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, HorizonTaggable;
    //                                                                   ↑↑↑ Agregar aquí
}
```

### Paso 2: Verificar Propiedad de Tenant

Asegúrate de que el job tenga **al menos una** de estas propiedades:

```php
protected $tenantDatabase; // Preferido
protected $database;       // Alternativa
protected $tenantId;       // Alternativa
```

### Paso 3: (Opcional) Agregar Tags Personalizados

```php
protected function additionalTags(): array
{
    return ['custom:tag1', 'custom:tag2'];
}
```

### Paso 4: Probar en Development

Ejecuta el job y verifica los tags en Horizon:

```bash
php artisan horizon
```

Navega a `http://localhost/horizon` y busca el job recién ejecutado.

## Troubleshooting

### Problema: Tags no Aparecen en Horizon

**Causa:** El método `tags()` no se está llamando.

**Solución:** Verifica que:
1. El trait está agregado a la clase
2. Horizon está corriendo (`php artisan horizon`)
3. Los jobs se están encolando correctamente

### Problema: Tag `tenant:*` No Aparece

**Causa:** No se puede resolver el tenant desde `instances`.

**Solución:** Verifica que:
1. El job tiene una propiedad de tenant válida
2. El registro existe en la tabla `instances`
3. El valor coincide (ej: `1747675435` existe en `instances.database`)

**Debug:**
```php
// Ver qué está pasando
Log::debug('Tenant identifier', [
    'tenantDatabase' => $this->tenantDatabase ?? 'not set',
    'database' => $this->database ?? 'not set',
    'tenantId' => $this->tenantId ?? 'not set'
]);
```

### Problema: Queries Lentos

**Causa:** Cache no está funcionando.

**Solución:** Verifica que:
1. Laravel cache está configurado
2. Cache driver está funcionando (file, redis, etc.)
3. No hay errores en `storage/logs/laravel.log`

## Mejoras Futuras

### Posibles Extensiones

1. **Tags de Queue**: Agregar automáticamente `queue:import`, `queue:default`
2. **Tags de Ambiente**: `env:production`, `env:staging`
3. **Tags de Prioridad**: `priority:high`, `priority:low`
4. **Tags de Retry**: `retry:1`, `retry:2` (número de intento)

### Integración con Métricas

```php
protected function additionalTags(): array
{
    return [
        'duration_estimate:' . $this->estimatedDuration,
        'batch_size:' . $this->batchSize
    ];
}
```

## Referencias

- [Laravel Horizon Documentation](https://laravel.com/docs/horizon#tags)
- [Job Tagging in Horizon](https://laravel.com/docs/horizon#tags)
- Tests: `tests/Unit/Traits/HorizonTaggableTest.php`
- Trait: `src/App/Traits/HorizonTaggable.php`

---

**Última actualización:** 2025-10-24
**Versión:** 1.0.0
**Autor:** SuiteX Development Team

