# Collector FE — Instrumentação Frontend com Grafana Faro

> **Este serviço é implantado na infraestrutura do cliente**, não na Elven. A Elven fornece as credenciais do Loki (`LOKI_API_TOKEN`) e realiza o deploy em conjunto com o cliente.

***

### Índice

* Visão geral
* Arquitetura
* Dados coletados
* Autenticação e multi-tenancy
* Variáveis de ambiente
* Deploy — Linux (VM / EC2)
* Deploy — Docker
* Deploy — Kubernetes (Helm)
* Configuração do Faro SDK no frontend
* CORS — Origens permitidas
* Geração do JWT
* Health check e monitoramento
* Troubleshooting
* FAQ

***

### Visão geral

O Collector FE é um serviço HTTP leve escrito em Go que atua como **bridge entre o Grafana Faro Web SDK e o Grafana Loki**:

1. O frontend (browser) envia payloads JSON via `POST /collect/:tenant/:token`
2. O serviço valida o JWT presente na URL
3. Transforma o payload Faro em streams Loki
4. Envia para o Loki com multi-tenancy (header `X-Scope-OrgID`)

O serviço **não fala OTLP** — ele é especializado em receber dados do Faro SDK e encaminhar para o Loki como logs estruturados.

***

### Arquitetura

```
┌─────────────────────┐
│   Browser / SPA     │
│   (Faro Web SDK)    │
└─────────┬───────────┘
          │ POST /collect/:tenant/:jwt
          │ JSON (Faro v2 payload)
          ▼
┌─────────────────────┐
│   Collector FE      │    ← Deploy na infra do cliente
│   (Go / HTTP)       │
│                     │
│  ┌───────────────┐  │
│  │ Validação JWT │  │
│  └───────┬───────┘  │
│  ┌───────▼───────┐  │
│  │ Faro → Loki   │  │
│  │ Transform     │  │
│  └───────┬───────┘  │
└──────────┼──────────┘
           │ POST /loki/api/v1/push
           │ Headers: X-Scope-OrgID, Authorization
           ▼
┌─────────────────────┐
│   Grafana Loki      │    ← Gerenciado pela Elven
│   (Multi-tenant)    │
└─────────────────────┘
```

***

### Dados coletados

O Faro SDK envia e o Collector FE processa quatro tipos de dados:

| Tipo             | Descrição                                                   | Label `kind` no Loki                |
| ---------------- | ----------------------------------------------------------- | ----------------------------------- |
| **Logs**         | `console.log`, `console.error`, etc. interceptados pelo SDK | `info`, `error`, `warning`, `debug` |
| **Events**       | Eventos customizados (cliques, navegação, interações)       | `event`                             |
| **Measurements** | Métricas de performance (Web Vitals: LCP, FID, CLS, etc.)   | `measurement`                       |
| **Exceptions**   | Erros JavaScript capturados (com stacktrace)                | `exception`                         |

#### Metadados incluídos em cada registro

Cada registro enviado ao Loki inclui automaticamente:

* **App**: nome, versão, ambiente
* **Browser**: nome, versão, OS, mobile
* **Session**: ID da sessão
* **Page**: URL da página
* **User**: username e atributos customizados
* **SDK**: versão do Faro SDK

#### Labels Loki

Cada stream no Loki recebe os seguintes labels:

| Label         | Origem                                                              |
| ------------- | ------------------------------------------------------------------- |
| `app`         | `meta.app.name` do payload                                          |
| `kind`        | Tipo do dado (`info`, `error`, `event`, `measurement`, `exception`) |
| `level`       | Nível do log                                                        |
| `environment` | `meta.app.environment`                                              |
| `browser`     | `meta.browser.name`                                                 |
| `session_id`  | `meta.session.id`                                                   |

***

### Autenticação e multi-tenancy

#### Tenant (`:tenant`)

* Primeiro segmento do path: `POST /collect/{tenant}/{token}`
* Identifica o cliente/produto no Loki
* Enviado como header `X-Scope-OrgID` para o Loki (multi-tenancy nativa)
* Validação: aceita letras, números, espaços, `-`, `.`, `_`, `@`

#### Token JWT (`:token`)

* Segundo segmento do path: `POST /collect/{tenant}/{token}`
* É um **JWT (HS256)** assinado com a `SECRET_KEY` configurada no servidor
* **Claims obrigatórias:**
  * `role`: deve ser `admin` ou `user`
  * `iss`: deve coincidir com `JWT_ISSUER` (padrão: `trusted-issuer`)
* **Claim opcional:**
  * `exp`: verificada apenas se `JWT_VALIDATE_EXP=true`

**Exemplo de payload JWT:**

```json
{
  "role": "user",
  "iss": "trusted-issuer",
  "exp": 1735689600,
  "sub": "minha-app-frontend"
}
```

> **Importante:** O token fica exposto na URL do SDK Faro (visível no browser). Por isso, use `role: user` (menor privilégio) e considere habilitar `JWT_VALIDATE_EXP=true` com rotação de tokens.

***

### Variáveis de ambiente

#### Obrigatórias

| Variável         | Descrição                                                     | Exemplo                                         |
| ---------------- | ------------------------------------------------------------- | ----------------------------------------------- |
| `SECRET_KEY`     | Chave HMAC para validar o JWT (mín. **64 caracteres**)        | `a1b2c3d4...` (64+ chars)                       |
| `LOKI_URL`       | URL base do Loki (sem `/loki/api/v1/push`)                    | `https://loki.elvenobservability.com`           |
| `LOKI_API_TOKEN` | Token Bearer para autenticação no Loki (fornecido pela Elven) | `eyJhbGci...`                                   |
| `ALLOW_ORIGINS`  | Origens CORS permitidas (separadas por vírgula, ou `*`)       | `https://app.example.com,https://*.example.com` |

#### Opcionais

| Variável           | Default          | Descrição                                    |
| ------------------ | ---------------- | -------------------------------------------- |
| `PORT`             | `3000`           | Porta HTTP do serviço                        |
| `JWT_ISSUER`       | `trusted-issuer` | Valor esperado na claim `iss` do JWT         |
| `JWT_VALIDATE_EXP` | `false`          | Se `true`, valida a expiração (`exp`) do JWT |

#### Variáveis do instalador (apenas no script `install.sh`)

| Variável            | Descrição                                                                       |
| ------------------- | ------------------------------------------------------------------------------- |
| `GITHUB_TOKEN`      | Token para download de repositório privado                                      |
| `LOCAL_BINARY`      | Caminho de um binário local (pula o download)                                   |
| `BINARY_URL`        | URL direta de download do binário                                               |
| `GITHUB_REPO`       | Repositório GitHub (padrão: `elven-observability/collector-fe-instrumentation`) |
| `COLLECTOR_VERSION` | Tag da release (padrão: `latest`)                                               |
| `INSTALL_CADDY`     | Se `true`, instala o Caddy como reverse proxy com auto-SSL                      |
| `CADDY_DOMAIN`      | Domínio para o Caddy (obrigatório se `INSTALL_CADDY=true`)                      |

***

### Deploy — Linux (VM / EC2)

O instalador configura o serviço como **systemd** em qualquer distribuição Linux suportada (Ubuntu, Debian, RHEL, CentOS, Rocky, AlmaLinux, Fedora, Amazon Linux).

#### One-liner (repositório público)

```bash
curl -sSL https://raw.githubusercontent.com/elven-observability/collector-fe-instrumentation/main/scripts/install.sh | sudo bash
```

O script entra em modo **interativo** e solicita as variáveis obrigatórias.

#### Instalação automatizada (CI / scripts)

```bash
sudo SECRET_KEY="sua-chave-com-pelo-menos-64-caracteres-aqui-coloque-algo-bem-longo" \
     LOKI_URL="https://loki.elvenobservability.com" \
     LOKI_API_TOKEN="seu-token-loki-fornecido-pela-elven" \
     ALLOW_ORIGINS="https://app.meusite.com.br,https://*.meusite.com.br" \
     PORT=3000 \
     bash <(curl -sSL https://raw.githubusercontent.com/elven-observability/collector-fe-instrumentation/main/scripts/install.sh)
```

#### Instalação com Caddy (HTTPS automático)

```bash
sudo SECRET_KEY="sua-chave-com-pelo-menos-64-caracteres-aqui-coloque-algo-bem-longo" \
     LOKI_URL="https://loki.elvenobservability.com" \
     LOKI_API_TOKEN="seu-token-loki" \
     ALLOW_ORIGINS="https://app.meusite.com.br" \
     INSTALL_CADDY=true \
     CADDY_DOMAIN="collector.meusite.com.br" \
     bash <(curl -sSL https://raw.githubusercontent.com/elven-observability/collector-fe-instrumentation/main/scripts/install.sh)
```

> **Pré-requisito:** O DNS do domínio `CADDY_DOMAIN` deve apontar para o IP público da VM **antes** da instalação. O Caddy obtém o certificado SSL automaticamente via Let's Encrypt.

#### Instalação com repositório privado

```bash
export GITHUB_TOKEN=$(gh auth token)
curl -sSL https://raw.githubusercontent.com/elven-observability/collector-fe-instrumentation/main/scripts/install.sh | sudo -E bash
```

> Requer `jq` ou `python3` na VM para parsing da API do GitHub.

#### Instalação com binário local (air-gap)

```bash
sudo SECRET_KEY="sua-chave..." \
     LOKI_URL="https://loki.elvenobservability.com" \
     LOKI_API_TOKEN="seu-token" \
     ALLOW_ORIGINS="*" \
     LOCAL_BINARY=/tmp/collector-fe-instrumentation-linux-amd64 \
     bash install.sh
```

#### Onde fica instalado

| Item               | Caminho                                                          |
| ------------------ | ---------------------------------------------------------------- |
| Binário            | `/opt/collector-fe-instrumentation/collector-fe-instrumentation` |
| Configuração (env) | `/etc/collector-fe-instrumentation/env`                          |
| Serviço systemd    | `collector-fe-instrumentation`                                   |

#### Comandos úteis

```bash
# Status do serviço
systemctl status collector-fe-instrumentation

# Reiniciar
systemctl restart collector-fe-instrumentation

# Logs em tempo real
journalctl -u collector-fe-instrumentation -f

# Health check
curl http://localhost:3000/health
```

#### Atualizando a configuração

Edite o arquivo de env e reinicie o serviço:

```bash
sudo nano /etc/collector-fe-instrumentation/env
sudo systemctl restart collector-fe-instrumentation
```

#### Atualizando o binário

Re-execute o instalador. Ele para o serviço, baixa a nova versão e reinicia:

```bash
sudo COLLECTOR_VERSION="v0.2.0" \
     SECRET_KEY="..." LOKI_API_TOKEN="..." \
     bash <(curl -sSL https://raw.githubusercontent.com/elven-observability/collector-fe-instrumentation/main/scripts/install.sh)
```

***

### Deploy — Docker

A imagem está no Docker Hub: `elvenobservability/collector-fe-instrumentation`

#### docker run

```bash
docker run -d \
  --name collector-fe \
  --restart unless-stopped \
  -p 3000:3000 \
  -e SECRET_KEY="sua-chave-com-pelo-menos-64-caracteres-aqui-coloque-algo-bem-longo" \
  -e LOKI_URL="https://loki.elvenobservability.com" \
  -e LOKI_API_TOKEN="seu-token-loki" \
  -e ALLOW_ORIGINS="https://app.meusite.com.br,https://*.meusite.com.br" \
  -e PORT=3000 \
  -e JWT_ISSUER="trusted-issuer" \
  -e JWT_VALIDATE_EXP="false" \
  elvenobservability/collector-fe-instrumentation:latest
```

#### docker-compose.yml

```yaml
services:
  collector-fe:
    image: elvenobservability/collector-fe-instrumentation:latest
    container_name: collector-fe
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      SECRET_KEY: "sua-chave-com-pelo-menos-64-caracteres-aqui-coloque-algo-bem-longo"
      LOKI_URL: "https://loki.elvenobservability.com"
      LOKI_API_TOKEN: "seu-token-loki"
      ALLOW_ORIGINS: "https://app.meusite.com.br,https://*.meusite.com.br"
      PORT: "3000"
      JWT_ISSUER: "trusted-issuer"
      JWT_VALIDATE_EXP: "false"
```

#### Docker Compose com Caddy (HTTPS)

```yaml
services:
  collector-fe:
    image: elvenobservability/collector-fe-instrumentation:latest
    container_name: collector-fe
    restart: unless-stopped
    expose:
      - "3000"
    environment:
      SECRET_KEY: "sua-chave-com-pelo-menos-64-caracteres-aqui-coloque-algo-bem-longo"
      LOKI_URL: "https://loki.elvenobservability.com"
      LOKI_API_TOKEN: "seu-token-loki"
      ALLOW_ORIGINS: "https://app.meusite.com.br"
      PORT: "3000"

  caddy:
    image: caddy:2-alpine
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config

volumes:
  caddy_data:
  caddy_config:
```

**Caddyfile:**

```
collector.meusite.com.br {
    reverse_proxy collector-fe:3000
}
```

***

### Deploy — Kubernetes (Helm)

O deploy em Kubernetes usa **Helmfile** com o chart `faro-collector`. A estrutura de arquivos segue o padrão:

```
stack-observability-k8s/
├── helmfile.yaml
├── kustomization.yaml
└── collector-fe/
    ├── values.yaml
    └── collector-fe-env-secret.yaml
```

#### 1. Criar o Secret com as variáveis

O Secret contém as credenciais e configurações sensíveis. Preencha os valores antes de aplicar:

```yaml
# collector-fe/collector-fe-env-secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: collector-fe-env-secret
  namespace: default
type: Opaque
stringData:
  SECRET_KEY: "sua-chave-com-pelo-menos-64-caracteres-aqui-coloque-algo-bem-longo"
  LOKI_URL: "https://loki.elvenobservability.com"
  LOKI_API_TOKEN: "seu-token-loki-fornecido-pela-elven"
  ALLOW_ORIGINS: "https://app.meusite.com.br,https://*.meusite.com.br"
  JWT_ISSUER: "trusted-issuer"
```

> **Atenção:** O `LOKI_URL` deve conter apenas a URL **base** (ex: `https://loki.elvenobservability.com`). O Collector FE appenda `/loki/api/v1/push` automaticamente. **Não** inclua o path na variável.

#### 2. Configurar o `kustomization.yaml`

O Kustomize aplica os Secrets e outros manifests estáticos **antes** do Helmfile:

```yaml
# kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - collector-fe/collector-fe-env-secret.yaml
```

#### 3. Configurar o `values.yaml`

```yaml
# collector-fe/values.yaml
replicaCount: 1

statefulset: false

UpdateStrategy:
  type: RollingUpdate

image:
  registry: docker.io
  repository: elvenobservability/collector-fe-instrumentation
  pullPolicy: Always
  tag: "latest"

envFrom:
  - secretRef:
      name: collector-fe-env-secret

containerPorts:
  - name: http
    containerPort: 3000
    protocol: TCP

service:
  servicePorts:
    - name: http
      port: 3000
      targetPort: 3000
  type: ClusterIP

Strategy:
  rollingUpdate:
    maxSurge: 1
    maxUnavailable: 0
  type: RollingUpdate

securityContext:
  privileged: false

ingress:
  enabled: false
```

Para expor o Collector FE externamente, habilite o Ingress:

```yaml
ingress:
  enabled: true
  ingressClassName: nginx
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
  hosts:
    - host: collector-fe.meusite.com.br
      paths:
        - path: /
          pathType: Prefix
          number: 3000
  tls:
    - secretName: collector-fe-tls
      hosts:
        - collector-fe.meusite.com.br
```

Para produção, recomenda-se definir resources e autoscaling:

```yaml
resources:
  requests:
    cpu: 50m
    memory: 64Mi
  limits:
    cpu: 200m
    memory: 128Mi

autoscaling:
  enabled: true
  minReplicas: 2
  maxReplicas: 10
  targetCPUUtilizationPercentage: 70
```

#### 4. Configurar o `helmfile.yaml`

```yaml
# helmfile.yaml
repositories:
  - name: faro-collector
    url: https://leozw.github.io/faro-collector

releases:
  - name: collector-fe
    namespace: default
    createNamespace: true
    chart: faro-collector/faro-collector
    values:
      - ./collector-fe/values.yaml

hooks:
  - events: ["prepare"]
    command: "kubectl"
    args: ["apply", "-k", "."]
```

O hook `prepare` aplica o Kustomize (que cria o Secret) **antes** de instalar o chart Helm.

#### 5. Deploy

```bash
helmfile sync
```

Ou se preferir aplicar passo a passo:

```bash
# 1. Aplicar o Secret
kubectl apply -k .

# 2. Instalar o chart
helm repo add faro-collector https://leozw.github.io/faro-collector
helm upgrade --install collector-fe faro-collector/faro-collector \
  -n default \
  -f collector-fe/values.yaml
```

#### Verificação

```bash
# Pods rodando
kubectl get pods -l app.kubernetes.io/name=faro-collector

# Logs
kubectl logs -l app.kubernetes.io/name=faro-collector -f

# Health check via port-forward
kubectl port-forward svc/collector-fe-faro-collector 3000:3000
curl http://localhost:3000/health
```

***

### Configuração do Faro SDK no frontend

Após o Collector FE estar rodando, configure o Faro Web SDK na sua aplicação frontend.

#### Instalação

```bash
npm install @grafana/faro-web-sdk
```

#### Inicialização

```typescript
import { initializeFaro, getWebInstrumentations } from '@grafana/faro-web-sdk';

const faro = initializeFaro({
  url: 'https://collector.meusite.com.br/collect/meu-tenant/eyJhbGciOiJIUzI1NiIs...',
  //                                                ─────────── ─────────────────────
  //                                                  tenant          JWT token
  app: {
    name: 'minha-app-frontend',
    version: '1.0.0',
    environment: 'production',
  },
  instrumentations: [
    ...getWebInstrumentations(),
  ],
});
```

#### Formato da URL

```
https://<host-collector>/collect/<tenant>/<jwt-token>
```

| Parte            | Descrição                                   | Exemplo                    |
| ---------------- | ------------------------------------------- | -------------------------- |
| `host-collector` | Endereço público do Collector FE            | `collector.meusite.com.br` |
| `tenant`         | Identificador do tenant/produto no Loki     | `meu-produto`              |
| `jwt-token`      | JWT assinado com a `SECRET_KEY` do servidor | `eyJhbGciOiJIUzI1NiIs...`  |

#### Exemplo com React

```tsx
// src/faro.ts
import { initializeFaro, getWebInstrumentations } from '@grafana/faro-web-sdk';

export function setupFaro() {
  return initializeFaro({
    url: process.env.REACT_APP_FARO_URL!,
    app: {
      name: 'minha-app-react',
      version: process.env.REACT_APP_VERSION || '0.0.0',
      environment: process.env.NODE_ENV,
    },
    instrumentations: [
      ...getWebInstrumentations({
        captureConsole: true,
        captureConsoleDisabledLevels: [],
      }),
    ],
    sessionTracking: {
      enabled: true,
    },
  });
}
```

```tsx
// src/index.tsx
import { setupFaro } from './faro';

setupFaro();

// ... resto da inicialização do React
```

**.env:**

```bash
REACT_APP_FARO_URL=https://collector.meusite.com.br/collect/meu-tenant/eyJhbGciOiJIUzI1NiIs...
```

#### Exemplo com Next.js

```tsx
// src/lib/faro.ts
'use client';

import { initializeFaro, getWebInstrumentations } from '@grafana/faro-web-sdk';

let faroInstance: ReturnType<typeof initializeFaro> | null = null;

export function getFaro() {
  if (typeof window === 'undefined') return null;
  if (faroInstance) return faroInstance;

  faroInstance = initializeFaro({
    url: process.env.NEXT_PUBLIC_FARO_URL!,
    app: {
      name: 'minha-app-nextjs',
      version: process.env.NEXT_PUBLIC_APP_VERSION || '0.0.0',
      environment: process.env.NODE_ENV,
    },
    instrumentations: [...getWebInstrumentations()],
  });

  return faroInstance;
}
```

***

### CORS — Origens permitidas

A variável `ALLOW_ORIGINS` controla quais domínios podem enviar dados para o Collector FE.

#### Formatos aceitos

| Formato                | Exemplo                                                   | Descrição                                     |
| ---------------------- | --------------------------------------------------------- | --------------------------------------------- |
| URL exata              | `https://app.meusite.com.br`                              | Apenas esse domínio                           |
| Wildcard de subdomínio | `https://*.meusite.com.br`                                | Qualquer subdomínio                           |
| Múltiplas origens      | `https://app.meusite.com.br,https://admin.meusite.com.br` | Separadas por vírgula                         |
| Todas                  | `*`                                                       | Qualquer origem (apenas para desenvolvimento) |

#### Recomendações

* **Produção:** sempre listar os domínios explicitamente
* **Staging/Dev:** pode usar wildcard de subdomínio: `https://*.dev.meusite.com.br`
* **Nunca use `*` em produção** — permite que qualquer site envie dados para o seu collector

***

### Geração do JWT

O JWT deve ser gerado **server-side** (no backend da sua aplicação) e passado para o frontend. Ele deve ser assinado com a mesma `SECRET_KEY` configurada no Collector FE.

#### Exemplo em Python

```python
import jwt
from datetime import datetime, timedelta

SECRET_KEY = "mesma-chave-de-64-caracteres-configurada-no-collector-fe-instrumentation"

token = jwt.encode(
    {
        "role": "user",
        "iss": "trusted-issuer",
        "exp": datetime.utcnow() + timedelta(days=365),
        "sub": "minha-app-frontend",
    },
    SECRET_KEY,
    algorithm="HS256",
)

print(token)
```

#### Exemplo em Node.js

```javascript
const jwt = require('jsonwebtoken');

const SECRET_KEY = 'mesma-chave-de-64-caracteres-configurada-no-collector-fe-instrumentation';

const token = jwt.sign(
  {
    role: 'user',
    iss: 'trusted-issuer',
    sub: 'minha-app-frontend',
  },
  SECRET_KEY,
  { expiresIn: '365d' }
);

console.log(token);
```

#### Exemplo em Go

```go
package main

import (
    "fmt"
    "time"
    "github.com/golang-jwt/jwt/v5"
)

func main() {
    secretKey := []byte("mesma-chave-de-64-caracteres-configurada-no-collector-fe-instrumentation")

    token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
        "role": "user",
        "iss":  "trusted-issuer",
        "exp":  time.Now().Add(365 * 24 * time.Hour).Unix(),
        "sub":  "minha-app-frontend",
    })

    tokenString, _ := token.SignedString(secretKey)
    fmt.Println(tokenString)
}
```

> **Dica:** Se `JWT_VALIDATE_EXP=false` (padrão), o campo `exp` é ignorado e o token nunca expira. Útil para simplificar, mas menos seguro. Para produção, considere `JWT_VALIDATE_EXP=true` com rotação periódica do token.

***

### Health check e monitoramento

#### Endpoint

```
GET /health → {"status":"ok"}
```

#### Exemplos

```bash
# Local
curl http://localhost:3000/health

# Com Caddy/Ingress
curl https://collector.meusite.com.br/health
```

#### Monitoramento recomendado

* Configure um **health check** no seu load balancer / ECS / Kubernetes apontando para `/health`
* Monitore os logs do serviço para erros de push para o Loki:

  ```bash
  journalctl -u collector-fe-instrumentation -f | grep -i error
  ```
* No Kubernetes, configure liveness e readiness probes:

  ```yaml
  probe:
    livenessProbe:
      httpGet:
        path: /health
        port: 3000
      initialDelaySeconds: 5
      periodSeconds: 10
    readinessProbe:
      httpGet:
        path: /health
        port: 3000
      initialDelaySeconds: 3
      periodSeconds: 5
  ```

***

### Troubleshooting

#### Serviço não inicia

**Sintoma:** `systemctl status collector-fe-instrumentation` mostra `failed`.

**Verificações:**

1. Verifique os logs: `journalctl -u collector-fe-instrumentation -n 50`
2. Erros comuns:
   * `invalid config: SECRET_KEY is required` → SECRET\_KEY não está definida
   * `invalid config: SECRET_KEY must be at least 64 characters` → Chave muito curta
   * `invalid config: LOKI_URL is required` → LOKI\_URL vazia
   * `invalid config: LOKI_API_TOKEN is required` → Token Loki não definido
   * `invalid config: ALLOW_ORIGINS is required` → ALLOW\_ORIGINS vazia
3. Confirme o conteúdo do env: `cat /etc/collector-fe-instrumentation/env`

#### SDK Faro retorna erro CORS

**Sintoma:** Erro no console do browser: `Access to fetch blocked by CORS policy`.

**Verificações:**

1. Confirme que o domínio da sua aplicação está em `ALLOW_ORIGINS`
2. Para wildcard: `https://*.meusite.com.br` cobre `https://app.meusite.com.br`, mas **não** cobre `https://meusite.com.br` (raiz)
3. Reinicie o serviço após alterar `ALLOW_ORIGINS`

#### SDK Faro retorna 401

**Sintoma:** Erro 401 (Unauthorized) nas requests do Faro.

**Verificações:**

1. Confirme que o JWT na URL foi gerado com a mesma `SECRET_KEY` do servidor
2. Verifique que as claims `role` e `iss` estão corretas:
   * `role` deve ser `admin` ou `user`
   * `iss` deve ser igual a `JWT_ISSUER` (padrão: `trusted-issuer`)
3. Se `JWT_VALIDATE_EXP=true`, verifique se o token não expirou
4. Verifique nos logs do collector: `journalctl -u collector-fe-instrumentation -f | grep auth`

#### SDK Faro retorna 500

**Sintoma:** O Collector FE aceita o request, mas retorna 500.

**Verificações:**

1. O Loki está acessível: `curl -v https://loki.elvenobservability.com/ready`
2. O `LOKI_API_TOKEN` é válido
3. Verifique nos logs: `journalctl -u collector-fe-instrumentation -f | grep "loki push failed"`

#### Logs não aparecem no Grafana

**Sintoma:** O SDK não reporta erros, mas os logs não aparecem no Grafana.

**Verificações:**

1. Confirme que o tenant na URL do Faro SDK (`/collect/{tenant}/...`) é o mesmo que você está filtrando no Grafana
2. O Grafana deve estar configurado com o mesmo data source Loki que recebe os dados
3. Use o label `app` para filtrar: `{app="minha-app-frontend"}`
4. Verifique se o payload do Faro não está vazio (o collector retorna 400 para payloads sem dados)

***

### FAQ

**Preciso abrir portas especiais?**

O serviço escuta na `PORT` configurada (padrão 3000). Se usar Caddy, ele escuta nas portas 80 e 443. Garanta que o firewall/security group permite tráfego nessas portas.

**O JWT fica exposto no browser?**

Sim, o JWT é parte da URL do Faro SDK e é visível nas DevTools do browser. Por isso:

* Use `role: user` (menor privilégio)
* Considere habilitar `JWT_VALIDATE_EXP=true` com rotação periódica
* O JWT só autentica o envio de dados de instrumentação — ele não dá acesso a nenhum outro recurso

**Posso ter múltiplos tenants no mesmo Collector FE?**

Sim. O tenant é definido na URL do Faro SDK. Diferentes aplicações frontend podem apontar para o mesmo Collector FE usando tenants diferentes:

* App A: `POST /collect/produto-a/{jwt}`
* App B: `POST /collect/produto-b/{jwt}`

Cada um aparece separado no Loki pelo header `X-Scope-OrgID`.

**O Collector FE envia dados via OTLP?**

Não. O Collector FE é especializado em receber payloads do Faro Web SDK e enviar para o Loki. Para traces e métricas de backend, use a instrumentação com as libs `elven-unified-observability-py` ou `elven-unified-observability-js` e um collector OTLP.

**Qual o consumo de recursos?**

O binário Go é muito leve. Para a maioria dos casos:

* **CPU**: 50-200m (Kubernetes)
* **Memória**: 32-128 MB
* **Disco**: \~10 MB (binário estático)

**Posso usar o mesmo `SECRET_KEY` para múltiplos ambientes?**

Pode, mas não é recomendado. Usar chaves diferentes por ambiente (dev/staging/prod) isola a autenticação e permite revogar tokens de um ambiente sem afetar os outros.

**Como faço rotação do JWT?**

1. Gere um novo JWT com a mesma `SECRET_KEY`
2. Atualize a URL do Faro SDK no frontend com o novo token
3. Deploy da aplicação frontend

Se precisar revogar tokens antigos imediatamente, mude a `SECRET_KEY` no Collector FE e reinicie o serviço — todos os tokens antigos serão invalidados.
