Projeto base para novas aplicações Spring Boot 4 + Java 21. Fornece a estrutura e os componentes transversais prontos para uso, para que cada novo serviço comece com qualidade e observabilidade já configuradas.
| Componente | Tecnologia | Finalidade |
|---|---|---|
| Actuator | Spring Boot Actuator | Health check e métricas expostas |
| Observabilidade | OpenTelemetry + Micrometer | Traces, métricas e logs via OTLP |
| Logs estruturados | Logback + OTel Appender | Logs correlacionados com traces |
| Testes | JUnit 5 + Mockito + Testcontainers | Unitários e integração |
| Qualidade | JaCoCo + SpotBugs + PMD | Cobertura e análise estática |
| CI | GitHub Actions | Build e quality gates automáticos |
- Rodando em desenvolvimento
- Observabilidade
- Logs
- Testes e qualidade de código
- Como escrever testes
- CI — GitHub Actions
- Java 21
- Docker Desktop — para a infra de observabilidade e testes de integração
docker compose up -dInicia: OTel Collector, Grafana Tempo, Loki, Prometheus e Grafana.
# Logs em texto legível (padrão para dev)
./mvnw spring-boot:run
# Logs em JSON estruturado (ECS format)
./mvnw spring-boot:run -Dspring-boot.run.profiles=prodActuator disponível em http://localhost:8081:
| Endpoint | Descrição |
|---|---|
GET /actuator/health |
Status da aplicação e subsistemas |
GET /actuator/info |
Metadados da aplicação |
GET /actuator/metrics |
Lista todas as métricas disponíveis |
A aplicação envia os três sinais via OTLP HTTP para um OTel Collector centralizado:
App (:8081)
│
└─ OTLP HTTP (:4318) ──► OTel Collector
├─ Traces ──► Grafana Tempo (:3200)
├─ Métricas ──► Prometheus (scrape :8889)
└─ Logs ──► Loki (:3100)
│
Grafana (:3000)
| Serviço | URL | Credenciais |
|---|---|---|
| Grafana | http://localhost:3000 | admin / admin |
| Prometheus | http://localhost:9090 | — |
| Sinal | O que é capturado |
|---|---|
| Traces | Spans gerados automaticamente pelo Spring para cada operação instrumentada |
| Métricas | JVM, pool de threads e métricas customizadas via Micrometer |
| Logs | Todos os logs do Logback com traceId e spanId no contexto |
# application.yaml
management:
tracing:
sampling:
probability: 1.0 # 100% em dev — reduza em produção (ex: 0.1)
otlp:
metrics:
export:
url: http://localhost:4318/v1/metrics
opentelemetry:
tracing:
export:
otlp:
endpoint: http://localhost:4318/v1/traces
logging:
export:
otlp:
endpoint: http://localhost:4318/v1/logs
logging:
export:
otlp:
enabled: trueImportant
management.logging.export.otlp.enabled: true é obrigatório no Spring Boot 4. Sem essa propriedade o bean OtlpHttpLogRecordExporter não é criado e os logs são descartados silenciosamente.
Os logs são exportados via OpenTelemetryAppender (logback-spring.xml). Cada linha carrega automaticamente o traceId e spanId do contexto em curso, permitindo navegar de um trace no Grafana Tempo diretamente para os logs no Loki.
Em desenvolvimento os logs são impressos em texto plano. Com o profile prod o formato muda para JSON estruturado (ECS):
SPRING_PROFILES_ACTIVE=prod java -jar app.jar@Service
public class PedidoService {
private static final Logger log = LoggerFactory.getLogger(PedidoService.class);
public PedidoResponse buscar(Long id) {
log.info("Buscando pedido id={}", id);
// ...
}
}Campos adicionados ao MDC aparecem como atributos no Loki e ficam disponíveis como filtros no Grafana:
MDC.put("pedido.id", id.toString());
MDC.put("pedido.status", status);
try {
log.info("Processando pagamento");
} finally {
MDC.remove("pedido.id");
MDC.remove("pedido.status");
}Como adicionar spans de queries SQL ao trace
Ao integrar um banco de dados, adicione datasource-micrometer-spring-boot para obter spans automáticos de cada query SQL como filhos do trace:
<!-- pom.xml -->
<dependency>
<groupId>net.ttddyy.observation</groupId>
<artifactId>datasource-micrometer-spring-boot</artifactId>
<version>2.2.1</version>
</dependency>Nenhuma configuração extra é necessária. O Spring Boot auto-configura um proxy na DataSource que instrumenta qualquer tecnologia JDBC: JPA, Spring Data JDBC, JdbcTemplate, jOOQ, Flyway, etc.
O trace no Grafana Tempo ganha a estrutura completa:
HTTP POST /pedidos (200ms)
└─ PedidoService.criar (180ms)
├─ SELECT * FROM produtos WHERE ... (12ms)
└─ INSERT INTO pedidos ... (8ms)
| Sufixo | Executor | Contexto Spring | Docker |
|---|---|---|---|
*Test |
Surefire — ./mvnw test |
Não | Não |
*IT |
Failsafe — ./mvnw verify |
Sim (@SpringBootTest) |
Sim |
O Surefire executa apenas *Test — rápido, sem infraestrutura. O Failsafe executa *IT após o empacotamento e garante teardown mesmo em caso de falha.
# Apenas testes unitários (sem Docker)
./mvnw test
# Build completo: unitários + integração + cobertura + análise estática
./mvnw verify
# Relatório de cobertura sem forçar threshold
./mvnw test jacoco:report
# → target/site/jacoco/index.html
# Apenas análise estática
./mvnw spotbugs:check pmd:checkTodos executados automaticamente em ./mvnw verify:
| Ferramenta | O que verifica | Falha o build se... |
|---|---|---|
| JaCoCo | Cobertura de testes | LINE ou BRANCH < 80% |
| SpotBugs | Bugs em bytecode | Qualquer bug encontrado |
| PMD | Qualidade do código-fonte | Qualquer violação |
Note
Classes excluídas do JaCoCo: *Application, *Configuration, *Properties.
Para suprimir um falso positivo pontual:
// SpotBugs
@SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH", justification = "valor nunca é null por invariante do domínio")
// PMD
@SuppressWarnings("PMD.NomeDaRegra")Use quando a classe não tem dependências externas. Não sobe contexto Spring.
class PedidoCalculadorTest {
private final PedidoCalculador calculador = new PedidoCalculador();
@Test
void deveCalcularTotalComDesconto() {
BigDecimal total = calculador.calcular(new BigDecimal("100.00"), 10);
assertThat(total).isEqualByComparingTo("90.00");
}
}Use quando a classe tem dependências que precisam ser isoladas. Sem Spring, sem Docker — o mais rápido.
@ExtendWith(MockitoExtension.class)
class PedidoServiceTest {
@Mock
private PedidoRepository repository;
@InjectMocks
private PedidoService service;
@Test
void deveBuscarPedidoPorId() {
when(repository.findById(1L)).thenReturn(Optional.of(new Pedido(1L, "Em andamento")));
var resultado = service.buscar(1L);
assertThat(resultado.status()).isEqualTo("Em andamento");
verify(repository).findById(1L);
}
@Test
void deveLancarExcecaoQuandoNaoEncontrado() {
when(repository.findById(99L)).thenReturn(Optional.empty());
assertThatThrownBy(() -> service.buscar(99L))
.isInstanceOf(PedidoNaoEncontradoException.class);
}
}Use quando precisa do contexto Spring mas quer substituir um bean por um mock — útil para isolar dependências externas como clientes HTTP ou gateways.
Declare os mocks em MockConfiguration (já presente em src/test):
@TestConfiguration(proxyBeanMethods = false)
public class MockConfiguration {
@Bean
PagamentoGateway pagamentoGateway() {
return Mockito.mock(PagamentoGateway.class);
}
}@SpringBootTest
@Import(MockConfiguration.class)
class PedidoServiceSpringTest {
@Autowired
private PedidoService service;
@MockitoBean
private PedidoRepository repository;
@Test
void deveBuscarPedidoComContextoSpring() {
when(repository.findById(1L)).thenReturn(Optional.of(new Pedido(1L, "Aprovado")));
assertThat(service.buscar(1L).status()).isEqualTo("Aprovado");
}
}Use para testar o fluxo completo com infraestrutura real em Docker. O sufixo IT garante execução no Failsafe, onde o Docker está disponível.
Passo 1 — Configure o container em TestcontainersConfiguration (já presente em src/test):
@TestConfiguration(proxyBeanMethods = false)
@Testcontainers
public class TestcontainersConfiguration {
@Bean
@ServiceConnection
PostgreSQLContainer<?> postgresContainer() {
return new PostgreSQLContainer<>(DockerImageName.parse("postgres:17"));
}
}Tip
@ServiceConnection configura automaticamente spring.datasource.* apontando para o container — sem properties manuais. Adicione org.testcontainers:postgresql no pom.xml ao usar PostgreSQL.
Passo 2 — Escreva o teste:
@SpringBootTest
@Import(TestcontainersConfiguration.class)
class PedidoRepositoryIT {
@Autowired
private PedidoRepository repository;
@Test
@Transactional
void devePersistirEBuscarPedido() {
var pedido = repository.save(new Pedido("Novo"));
var encontrado = repository.findById(pedido.getId());
assertThat(encontrado).isPresent();
assertThat(encontrado.get().getStatus()).isEqualTo("Novo");
}
}O workflow .github/workflows/quality.yml roda automaticamente em todo push e pull request.
push / pull_request
│
▼
Checkout + Java 21 (Temurin, cache Maven)
│
▼
./mvnw verify
├── Compila
├── Testes unitários (Surefire)
├── Testes de integração (Failsafe + Docker)
├── Cobertura JaCoCo ≥ 80%
├── SpotBugs
└── PMD
│
▼
Upload relatório JaCoCo (artefato, 1 dia)
Note
O runner ubuntu-latest já possui Docker instalado e em execução — por isso os testes de integração com Testcontainers funcionam no CI sem configuração adicional.