Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Apollo 3.0.0
* [Fix: handle encoded and literal-percent keys, preserve item types and audit display names, enforce full namespace name limits, report partial import failures and honor overwrite selections, restore token actions and audit filters, show gray-rule labels, and correct restricted namespace visibility, API error responses and namespace views](https://github.com/apolloconfig/apollo/pull/5681)
* [Fix: restore service startup on JDK 26, recover background console logs, and consolidate startup log configuration](https://github.com/apolloconfig/apollo/pull/5682)
* [Fix: snapshot AccessKey cache under lock to avoid concurrent iteration failures](https://github.com/apolloconfig/apollo/pull/5678)
* [Fix: support open-ended audit date filters and align release regression checks with supported Portal behavior](https://github.com/apolloconfig/apollo/pull/5684)

------------------
All issues and pull requests are [here](https://github.com/apolloconfig/apollo/milestone/18?closed=1)
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ public interface ApolloAuditLogQueryApi {
* Query AuditLogs by operator name and time limit and page
*
* @param opName operation name of querying
* @param startDate expect result after or equal this time
* @param endDate expect result before or equal this time
* @param startDate expect result after or equal this time, or null for no lower bound
* @param endDate expect result before or equal this time, or null for no upper bound
* @param page index from 0
* @param size size of a page
* @return List of ApolloAuditLogDTO
Expand Down
6 changes: 6 additions & 0 deletions apollo-audit/apollo-audit-impl/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>

</dependencies>

</project>
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ public interface ApolloAuditLogRepository extends JpaRepository<ApolloAuditLog,

List<ApolloAuditLog> findByOpName(String opName, Pageable page);

List<ApolloAuditLog> findByOpNameAndDataChangeCreatedTimeGreaterThanEqual(String opName,
Date startDate, Pageable pageable);

List<ApolloAuditLog> findByOpNameAndDataChangeCreatedTimeLessThanEqual(String opName,
Date endDate, Pageable pageable);

List<ApolloAuditLog> findByOpNameAndDataChangeCreatedTimeGreaterThanEqualAndDataChangeCreatedTimeLessThanEqual(
String opName, Date startDate, Date endDate, Pageable pageable);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,17 @@ public List<ApolloAuditLog> findByOpName(String opName, int page, int size) {
public List<ApolloAuditLog> findByOpNameAndTime(String opName, Date startDate, Date endDate,
int page, int size) {
Pageable pageable = pageSortByTime(page, size);
if (startDate == null && endDate == null) {
return logRepository.findByOpName(opName, pageable);
}
if (endDate == null) {
return logRepository.findByOpNameAndDataChangeCreatedTimeGreaterThanEqual(opName, startDate,
pageable);
}
if (startDate == null) {
return logRepository.findByOpNameAndDataChangeCreatedTimeLessThanEqual(opName, endDate,
pageable);
}
return logRepository
.findByOpNameAndDataChangeCreatedTimeGreaterThanEqualAndDataChangeCreatedTimeLessThanEqual(
opName, startDate, endDate, pageable);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* Copyright 2025 Apollo Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.ctrip.framework.apollo.audit.service;

import static org.assertj.core.api.Assertions.assertThat;

import com.ctrip.framework.apollo.audit.entity.ApolloAuditLog;
import com.ctrip.framework.apollo.audit.repository.ApolloAuditLogRepository;
import jakarta.persistence.EntityManagerFactory;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import javax.sql.DataSource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.transaction.annotation.Transactional;

/**
* Verifies audit date filtering and pagination against a real JPA repository.
*/
@SpringJUnitConfig(ApolloAuditLogServiceTest.TestConfiguration.class)
@Transactional
class ApolloAuditLogServiceTest {

private static final String OP_NAME = "App.create";

@Autowired
private ApolloAuditLogRepository repository;

@Autowired
private ApolloAuditLogService service;

@BeforeEach
void setUp() {
for (int minute = 0; minute < 6; minute++) {
repository.save(log(OP_NAME, minute));
}
repository.save(log("App.update", 3));
repository.flush();
}

@ParameterizedTest
@MethodSource("dateRanges")
void filtersInclusiveDateBoundsAndPreservesThemAcrossPages(Date startDate, Date endDate,
List<String> expected) {
assertThat(service.findByOpNameAndTime(OP_NAME, startDate, endDate, 0, 10))
.extracting(ApolloAuditLog::getSpanId).containsExactlyElementsOf(expected);

List<ApolloAuditLog> pages = new ArrayList<>();
for (int page = 0; page < 3; page++) {
pages.addAll(service.findByOpNameAndTime(OP_NAME, startDate, endDate, page, 2));
}
assertThat(pages).extracting(ApolloAuditLog::getSpanId).containsExactlyElementsOf(expected);
assertThat(service.findByOpNameAndTime(OP_NAME, startDate, endDate, 3, 2)).isEmpty();
}

static Stream<Arguments> dateRanges() {
return Stream.of(Arguments.of(date(2), date(4), List.of("minute-4", "minute-3", "minute-2")),
Arguments.of(date(2), null, List.of("minute-5", "minute-4", "minute-3", "minute-2")),
Arguments.of(null, date(4),
List.of("minute-4", "minute-3", "minute-2", "minute-1", "minute-0")),
Arguments.of(null, null,
List.of("minute-5", "minute-4", "minute-3", "minute-2", "minute-1", "minute-0")));
}

@Test
void returnsNoLogsWhenTheOperationOrDatesDoNotMatch() {
assertThat(service.findByOpNameAndTime("App.missing", date(2), null, 0, 10)).isEmpty();
assertThat(service.findByOpNameAndTime(OP_NAME, date(6), null, 0, 10)).isEmpty();
assertThat(service.findByOpNameAndTime(OP_NAME, null, date(-1), 0, 10)).isEmpty();
}

private static ApolloAuditLog log(String opName, int minute) {
return ApolloAuditLog.builder().traceId("test-trace").spanId("minute-" + minute).opName(opName)
.happenedTime(date(minute)).build();
}

private static Date date(int minute) {
return Date.from(Instant.parse("2026-09-27T13:30:00Z").plusSeconds(minute * 60L));
}

@Configuration(proxyBeanMethods = false)
@EnableJpaRepositories(basePackageClasses = ApolloAuditLogRepository.class)
@Import(ApolloAuditLogService.class)
static class TestConfiguration {

@Bean
DataSource dataSource() {
return new EmbeddedDatabaseBuilder().generateUniqueName(true).setType(EmbeddedDatabaseType.H2)
.build();
}

@Bean
LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setDataSource(dataSource);
factory.setPackagesToScan(ApolloAuditLog.class.getPackage().getName());
factory.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
factory.setJpaPropertyMap(Map.of("hibernate.hbm2ddl.auto", "create-drop"));
return factory;
}

@Bean
JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,24 @@ for (const cleared of [null, undefined, '']) {
});
}

test('clearing one bound preserves the other bound', () => {
const { scope, requests } = controller();
const date = '2026-09-26T15:15:00.000Z';
scope.searchByOpNameAndDate('operation', date, date);
scope.searchByOpNameAndDate('operation', null, date);
assert.deepEqual(requests.at(-1), ['operation', null, date, 0, 10]);
scope.searchByOpNameAndDate('operation', date, null);
assert.deepEqual(requests.at(-1), ['operation', date, null, 0, 10]);
});
for (const cleared of [null, undefined, '']) {
for (const bound of ['start', 'end']) {
test('clearing ' + bound + ' preserves the other bound when paging: ' + String(cleared), () => {
const { scope, requests, respond } = controller();
const date = '2026-09-26T15:15:00.000Z';
scope.searchByOpNameAndDate('operation', date, date);
respond([{ id: 1 }]);
assert.equal(scope.hasLoadAll, true);
scope.searchByOpNameAndDate('operation',
bound === 'start' ? cleared : date, bound === 'end' ? cleared : date);
const start = bound === 'start' ? null : date;
const end = bound === 'end' ? null : date;
assert.deepEqual(requests.at(-1), ['operation', start, end, 0, 10]);
assert.equal(scope.auditLogList.length, 0);
assert.equal(scope.hasLoadAll, false);
respond(Array.from({ length: 10 }, (_, id) => ({ id })));
scope.getMoreAuditLogs();
assert.deepEqual(requests.at(-1), ['operation', start, end, 1, 10]);
});
}
}
Loading
Loading