Skip to main content

Auditoria i Logging

Tipus: Document d'Arquitectura
Versió: 1.0
Actualitzat: 2024-12
Estat: Normatiu


Resum

Aquest document defineix l'estratègia de logging i auditoria per a LDP. Cobreix:

  1. Què loguejar (i què NO)
  2. Estructura de logs
  3. Correlation IDs per traçabilitat
  4. Auditoria d'operacions sensibles

1. Principis de Logging

1.1. Logging ≠ Debugging

PropòsitDescripcióNivell
ObservabilitatEntendre què passa al sistemaINFO
AuditoriaRegistrar QUI va fer QUÈ QUANINFO/WARN
TroubleshootingDiagnosticar problemesWARN/ERROR
DebuggingDetalls per desenvolupamentDEBUG (mai en prod)

1.2. Regla d'Or de Seguretat

Mai Loguejar Secrets
  • ❌ Passwords
  • ❌ Tokens (JWT, refresh, API keys)
  • ❌ Credencials de BD
  • ❌ PII complet (email, telèfon, DNI)
  • ❌ Dades de targetes
  • ❌ Session IDs complets

2. Què Loguejar

2.1. Operacions Obligatòries a Loguejar

OperacióNivellDades a Incloure
Login exitósINFOuserId (no password)
Login fallitWARNintent (email parcialment emmascarada), IP, motiu
LogoutINFOuserId
Token refreshDEBUGuserId
Accés denegatWARNuserId, recurs, motiu
Creació d'entitatINFOentityType, entityId, userId
Modificació d'entitatINFOentityType, entityId, userId, camps canviats (noms, no valors sensibles)
Eliminació d'entitatWARNentityType, entityId, userId
Canvi de rolWARNtargetUserId, oldRole, newRole, byUserId
Ownership claim/transferWARNresourceType, resourceId, fromUserId, toUserId
Error 5xxERRORexception, stack trace, context

2.2. Operacions Opcionals (DEBUG)

  • Queries a BD (amb paràmetres emmascarats)
  • Crides a APIs externes
  • Temps d'execució de mètodes

2.3. Què NO Loguejar

// ❌ MAI
log.info("User logged in with password: {}", password);
log.debug("JWT token: {}", token);
log.info("Processing user email: {}", email); // PII

// ✅ CORRECTE
log.info("User logged in: userId={}", userId);
log.debug("JWT token issued for userId={}", userId);
log.info("Processing user: userId={}", userId);

3. Estructura de Logs

3.1. Format JSON Estructurat

Configuració Logback per producció:

<!-- logback-spring.xml -->
<configuration>
<springProfile name="prod">
<appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<includeMdcKeyName>correlationId</includeMdcKeyName>
<includeMdcKeyName>userId</includeMdcKeyName>
<includeMdcKeyName>requestPath</includeMdcKeyName>
</encoder>
</appender>

<root level="INFO">
<appender-ref ref="JSON" />
</root>
</springProfile>

<springProfile name="dev">
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - [%X{correlationId}] %msg%n</pattern>
</encoder>
</appender>

<root level="DEBUG">
<appender-ref ref="CONSOLE" />
</root>
</springProfile>
</configuration>

3.2. Exemple de Log JSON

{
"@timestamp": "2024-12-15T10:30:45.123Z",
"level": "INFO",
"logger": "c.l.s.ChoreographerService",
"message": "Choreographer created",
"correlationId": "abc123-def456-789",
"userId": 42,
"entityType": "Choreographer",
"entityId": 156,
"requestPath": "/api/choreographers",
"duration_ms": 45
}

4. Correlation ID

4.1. Propòsit

El Correlation ID permet traçar una petició a través de totes les capes i serveis:

Client Request

▼ correlationId: abc-123
┌─────────────────────┐
│ Controller │ log: [abc-123] Received request
├─────────────────────┤
│ Service │ log: [abc-123] Processing...
├─────────────────────┤
│ Repository │ log: [abc-123] Query executed
├─────────────────────┤
│ External API │ header: X-Correlation-Id: abc-123
└─────────────────────┘

4.2. Implementació

Filter per Generar/Propagar

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CorrelationIdFilter extends OncePerRequestFilter {

private static final String CORRELATION_ID_HEADER = "X-Correlation-Id";
private static final String CORRELATION_ID_MDC_KEY = "correlationId";

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
try {
String correlationId = request.getHeader(CORRELATION_ID_HEADER);
if (correlationId == null || correlationId.isBlank()) {
correlationId = UUID.randomUUID().toString();
}

MDC.put(CORRELATION_ID_MDC_KEY, correlationId);
response.setHeader(CORRELATION_ID_HEADER, correlationId);

chain.doFilter(request, response);
} finally {
MDC.remove(CORRELATION_ID_MDC_KEY);
}
}
}

Propagació a APIs Externes

@Component
public class ExternalApiClient {

private final RestTemplate restTemplate;

public SpotifyTrack fetchTrack(String trackId) {
String correlationId = MDC.get("correlationId");

HttpHeaders headers = new HttpHeaders();
headers.set("X-Correlation-Id", correlationId);

HttpEntity<?> entity = new HttpEntity<>(headers);

return restTemplate.exchange(
spotifyApiUrl + "/tracks/" + trackId,
HttpMethod.GET,
entity,
SpotifyTrack.class
).getBody();
}
}

Propagació a Threads Asíncrons

@Configuration
@EnableAsync
public class AsyncConfig {

@Bean
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setTaskDecorator(new MdcTaskDecorator());
executor.initialize();
return executor;
}
}

public class MdcTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
Map<String, String> contextMap = MDC.getCopyOfContextMap();
return () -> {
try {
if (contextMap != null) {
MDC.setContextMap(contextMap);
}
runnable.run();
} finally {
MDC.clear();
}
};
}
}

5. Context de Request

5.1. Afegir Informació d'Usuari

@Component
public class UserContextFilter extends OncePerRequestFilter {

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
try {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.getPrincipal() instanceof UserDetails user) {
MDC.put("userId", String.valueOf(((User) user).getId()));
}
MDC.put("requestPath", request.getRequestURI());
MDC.put("requestMethod", request.getMethod());
MDC.put("clientIp", getClientIp(request));

chain.doFilter(request, response);
} finally {
MDC.remove("userId");
MDC.remove("requestPath");
MDC.remove("requestMethod");
MDC.remove("clientIp");
}
}

private String getClientIp(HttpServletRequest request) {
String xff = request.getHeader("X-Forwarded-For");
if (xff != null && !xff.isBlank()) {
return xff.split(",")[0].trim();
}
return request.getRemoteAddr();
}
}

6. Auditoria d'Operacions Sensibles

6.1. Servei d'Auditoria

@Service
@RequiredArgsConstructor
public class AuditService {

private static final Logger auditLog = LoggerFactory.getLogger("AUDIT");

public void logSecurityEvent(SecurityEventType type, Long userId, String details) {
auditLog.info("type={} userId={} details={}",
type, userId, sanitize(details));
}

public void logEntityChange(String entityType, Long entityId,
ChangeType changeType, Long userId) {
auditLog.info("entityType={} entityId={} changeType={} userId={}",
entityType, entityId, changeType, userId);
}

public void logAccessDenied(Long userId, String resource, String reason) {
auditLog.warn("ACCESS_DENIED userId={} resource={} reason={}",
userId, resource, reason);
}

private String sanitize(String input) {
if (input == null) return null;
// Eliminar newlines per evitar log injection
return input.replaceAll("[\r\n]", " ");
}
}

public enum SecurityEventType {
LOGIN_SUCCESS,
LOGIN_FAILED,
LOGOUT,
PASSWORD_CHANGED,
ROLE_CHANGED,
ACCOUNT_LOCKED,
ACCOUNT_UNLOCKED,
TOKEN_REVOKED
}

public enum ChangeType {
CREATE,
UPDATE,
DELETE,
SOFT_DELETE,
RESTORE
}

6.2. Ús a Services

@Service
@RequiredArgsConstructor
public class ChoreographerService {

private final AuditService auditService;

@Transactional
public void delete(Long id) {
Choreographer entity = repo.findById(id).orElseThrow();
Long userId = authUtil.getCurrentUserId();

// Soft delete
entity.setDeletedAt(Instant.now());
repo.save(entity);

// Audit
auditService.logEntityChange("Choreographer", id, ChangeType.SOFT_DELETE, userId);
}
}

7. Logging d'Errors

7.1. Exception Handler amb Context

@RestControllerAdvice
@RequiredArgsConstructor
public class GlobalExceptionHandler {

private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
private final AuditService auditService;

@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<ErrorResponse> handleAccessDenied(
AccessDeniedException ex, HttpServletRequest request) {

Long userId = getCurrentUserId();
String resource = request.getRequestURI();

auditService.logAccessDenied(userId, resource, ex.getMessage());

return ResponseEntity
.status(HttpStatus.FORBIDDEN)
.body(new ErrorResponse("ACCESS_DENIED", "No tens permís per aquesta acció"));
}

@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleUnexpected(Exception ex) {
String correlationId = MDC.get("correlationId");

log.error("Unexpected error correlationId={}", correlationId, ex);

return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("INTERNAL_ERROR",
"Error intern. Referència: " + correlationId));
}
}

7.2. No Exposar Stack Traces

// ❌ MAI en producció
return ResponseEntity
.status(500)
.body(new ErrorResponse("ERROR", ex.getMessage()));

// ✅ CORRECTE
return ResponseEntity
.status(500)
.body(new ErrorResponse("INTERNAL_ERROR",
"Error intern. Contacta suport amb referència: " + correlationId));

8. Nivells de Log per Entorn

PackageDevStagingProd
com.linedanceDEBUGINFOINFO
org.springframework.securityDEBUGINFOWARN
org.hibernate.SQLDEBUGOFFOFF
org.hibernate.typeTRACEOFFOFF
AUDIT (logger especial)INFOINFOINFO

Configuració

# application.yml
logging:
level:
com.linedance: ${LOG_LEVEL_APP:INFO}
org.springframework.security: ${LOG_LEVEL_SECURITY:WARN}
AUDIT: INFO # Sempre actiu

9. Emmascarament de Dades

9.1. Utilitat d'Emmascarament

public class LogMasker {

public static String maskEmail(String email) {
if (email == null || !email.contains("@")) return "***";
String[] parts = email.split("@");
String local = parts[0];
if (local.length() <= 2) return "**@" + parts[1];
return local.charAt(0) + "***" + local.charAt(local.length() - 1) + "@" + parts[1];
}

public static String maskToken(String token) {
if (token == null || token.length() < 10) return "***";
return token.substring(0, 6) + "..." + token.substring(token.length() - 4);
}

public static String maskIp(String ip) {
// Mantenir subnet per debugging, ocultar host
if (ip == null) return "***";
int lastDot = ip.lastIndexOf('.');
if (lastDot > 0) {
return ip.substring(0, lastDot) + ".***";
}
return "***";
}
}

9.2. Ús

log.info("Login attempt for email={}", LogMasker.maskEmail(email));
// Output: Login attempt for email=j***n@example.com

log.warn("Token validation failed: {}", LogMasker.maskToken(token));
// Output: Token validation failed: eyJhbG...xyz1

10. Checklist de Logging

Per Operació Nova

- [ ] Log INFO per operacions importants
- [ ] Log WARN per situacions anòmales però recuperables
- [ ] Log ERROR només per errors inesperats
- [ ] Correlation ID inclòs (via MDC)
- [ ] Sense secrets ni PII
- [ ] Emmascarament aplicat si cal

Per Error Handler Nou

- [ ] Log de l'error amb context
- [ ] Correlation ID a la resposta d'error
- [ ] Missatge d'usuari genèric
- [ ] Stack trace NO exposat al client

11. Referències


Historial de Canvis

DataVersióCanvis
2024-121.0Document inicial