Skip to content

Commit

Permalink
feat: implement listLogs API and provide sample snippet (#602)
Browse files Browse the repository at this point in the history
* feat: add listLogs and listLogsAsync to Logging

Add listLogs API support to hand-written layer of google-cloud-logging.
Add unit testing for the new listLogs API.

Fixes #593

* feat: add listLogs snippet example

Add a sample snippet to demonstrate use of listLogs API.
Refactor ListLogs to include snippets for listLogs and listLogEntries.
Format all snippets.

Fixes #358.

* chore: set generated serialVersionUID for LogNamePageFetcher

* chore(code): fixing errors

* chore(deps): fix clirr plugin 7012 error

Because of JDK 1.7 it is impossible to provide default implementation
for new interface methods.
File with exclusions is added instead. The file should be removed once
JDK version is upgraded.

* chore(tests): fix samples' tests

Fix printed string in LogEntryWriteHttpRequest.createLogEntryRequest().
Fix loops to wait for any data in STDOUT.
Add test for listLogs snippet.

* chore(test): forward exception throwing

Update testListLogNames() signature to throw exceptions

* chore(tests): refactoring tests

Test ListLogs.printLogNames vs audit logs to save time.
Restore retrieval of log entries in the wait loop to ensure printing to STDOUT

* chore(tests): fine tune clirr exceptions

Provide method level exception configuration in clirr-ignored-differences.
Implement default methods for new methods in Logging and LoggingRpc interfaces.
Following guidelines, remove serialVersionUID from LogNamePageFetcher.

* chore: refactoring method naming and sample snippets

Make more verbose naming for methods.
Refactor testing after renaming interface method(s).
Split ListLogs sample into two: ListLogEntries and ListLogs.

* chore(fix): fix formatting

* chore(comment): fix copyright year of the new file

* chore(fix): restore sample filter to list log entries

update the list log entries filter to bring results only for the last hour.

* chore(fix): fix snippet-bot errors

adding empty region tag logging_list_log_entries to ListLogs.java
  • Loading branch information
minherz committed Aug 12, 2021
1 parent 63a79ab commit 9359569
Show file tree
Hide file tree
Showing 11 changed files with 398 additions and 54 deletions.
17 changes: 17 additions & 0 deletions google-cloud-logging/clirr-ignored-differences.xml
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- see http://www.mojohaus.org/clirr-maven-plugin/examples/ignored-differences.html -->
<!-- added to resolve breaking changes until JDK will be upgraded to ≥1.8 -->
<differences>
<!-- Added methods to com.google.cloud.logging.Logging interface with implementation in LoggingImpl is always okay -->
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/logging/Logging</className>
<method>* listLogs*(com.google.cloud.logging.Logging$ListOption[])</method>
</difference>
<!-- Added methods to com.google.cloud.logging.spi.v2.LoggingRpc interface with implementation in GrpcLoggingRpc is always okay -->
<difference>
<differenceType>7012</differenceType>
<className>com/google/cloud/logging/spi/v2/LoggingRpc</className>
<method>* listLogs(com.google.logging.v2.ListLogsRequest)</method>
</difference>
</differences>
Expand Up @@ -414,6 +414,53 @@ public static EntryListOption folder(String folder) {
*/
ApiFuture<Boolean> deleteSinkAsync(String sink);

/**
* Lists the log names. This method returns a {@link Page} object that can be used to consume
* paginated results. Use {@link ListOption} to specify the page size or the page token from which
* to start listing logs.
*
* <p>Example of listing log names, specifying the page size.
*
* <pre>{@code
* Page<Log> logNames = logging.listLogs(ListOption.pageSize(100));
* Iterator<Log> logIterator = logNames.iterateAll().iterator();
* while (logIterator.hasNext()) {
* String logName = logIterator.next();
* // do something with the log name
* }
* }</pre>
*
* @throws LoggingException upon failure
*/
default Page<String> listLogs(ListOption... options) {
throw new UnsupportedOperationException(
"method listLogs() does not have default implementation");
}

/**
* Sends a request for listing log names. This method returns a {@code ApiFuture} object to
* consume the result. {@link ApiFuture#get()} returns an {@link AsyncPage} object that can be
* used to asynchronously handle paginated results. Use {@link ListOption} to specify the page
* size or the page token from which to start listing log names.
*
* <p>Example of asynchronously listing log names, specifying the page size.
*
* <pre>{@code
* ApiFuture<AsyncPage<Log>> future = logging.listLogsAsync(ListOption.pageSize(100));
* // ...
* AsyncPage<Sink> logNames = future.get();
* Iterator<Sink> logIterator = logNames.iterateAll().iterator();
* while (logIterator.hasNext()) {
* String logName = logIterator.next();
* // do something with the log name
* }
* }</pre>
*/
default ApiFuture<AsyncPage<String>> listLogsAsync(ListOption... options) {
throw new UnsupportedOperationException(
"method listLogsAsync() does not have default implementation");
}

/**
* Deletes a log and all its log entries. The log will reappear if new entries are written to it.
*
Expand Down
Expand Up @@ -65,6 +65,8 @@
import com.google.logging.v2.ListLogEntriesResponse;
import com.google.logging.v2.ListLogMetricsRequest;
import com.google.logging.v2.ListLogMetricsResponse;
import com.google.logging.v2.ListLogsRequest;
import com.google.logging.v2.ListLogsResponse;
import com.google.logging.v2.ListMonitoredResourceDescriptorsRequest;
import com.google.logging.v2.ListMonitoredResourceDescriptorsResponse;
import com.google.logging.v2.ListSinksRequest;
Expand Down Expand Up @@ -195,6 +197,19 @@ public ApiFuture<AsyncPage<Sink>> getNextPage() {
}
}

private static class LogNamePageFetcher extends BasePageFetcher<String> {

LogNamePageFetcher(
LoggingOptions serviceOptions, String cursor, Map<Option.OptionType, ?> requestOptions) {
super(serviceOptions, cursor, requestOptions);
}

@Override
public ApiFuture<AsyncPage<String>> getNextPage() {
return listLogsAsync(serviceOptions(), requestOptions());
}
}

private static class MonitoredResourceDescriptorPageFetcher
extends BasePageFetcher<MonitoredResourceDescriptor> {

Expand Down Expand Up @@ -366,6 +381,63 @@ public ApiFuture<Boolean> deleteSinkAsync(String sink) {
return transform(rpc.delete(request), EMPTY_TO_BOOLEAN_FUNCTION);
}

/**
* Creates a new {@code ListLogsRequest} object.
*
* <p>Builds an instance of {@code ListLogsRequest} using page size, page token and project id
* from the {@code LoggingOptions}. The project id is used as the request's parent parameter.
*
* @see com.google.logging.v2.ListLogEntriesRequest
* @return the created {@code ListLogsRequest} object
*/
private static ListLogsRequest listLogsRequest(
LoggingOptions serviceOptions, Map<Option.OptionType, ?> options) {
ListLogsRequest.Builder builder = ListLogsRequest.newBuilder();
builder.setParent(ProjectName.of(serviceOptions.getProjectId()).toString());
Integer pageSize = PAGE_SIZE.get(options);
String pageToken = PAGE_TOKEN.get(options);
if (pageSize != null) {
builder.setPageSize(pageSize);
}
if (pageToken != null) {
builder.setPageToken(pageToken);
}
return builder.build();
}

private static ApiFuture<AsyncPage<String>> listLogsAsync(
final LoggingOptions serviceOptions, final Map<Option.OptionType, ?> options) {
final ListLogsRequest request = listLogsRequest(serviceOptions, options);
ApiFuture<ListLogsResponse> list = serviceOptions.getLoggingRpcV2().listLogs(request);
return transform(
list,
new Function<ListLogsResponse, AsyncPage<String>>() {
@Override
public AsyncPage<String> apply(ListLogsResponse listLogsResponse) {
List<String> logNames =
listLogsResponse.getLogNamesList() == null
? ImmutableList.<String>of()
: listLogsResponse.getLogNamesList();
String cursor =
listLogsResponse.getNextPageToken().equals("")
? null
: listLogsResponse.getNextPageToken();
return new AsyncPageImpl<>(
new LogNamePageFetcher(serviceOptions, cursor, options), cursor, logNames);
}
});
}

@Override
public Page<String> listLogs(ListOption... options) {
return get(listLogsAsync(options));
}

@Override
public ApiFuture<AsyncPage<String>> listLogsAsync(ListOption... options) {
return listLogsAsync(getOptions(), optionMap(options));
}

public boolean deleteLog(String log) {
return get(deleteLogAsync(log));
}
Expand Down
Expand Up @@ -61,6 +61,8 @@
import com.google.logging.v2.ListLogEntriesResponse;
import com.google.logging.v2.ListLogMetricsRequest;
import com.google.logging.v2.ListLogMetricsResponse;
import com.google.logging.v2.ListLogsRequest;
import com.google.logging.v2.ListLogsResponse;
import com.google.logging.v2.ListMonitoredResourceDescriptorsRequest;
import com.google.logging.v2.ListMonitoredResourceDescriptorsResponse;
import com.google.logging.v2.ListSinksRequest;
Expand Down Expand Up @@ -260,6 +262,11 @@ public ApiFuture<Empty> delete(DeleteExclusionRequest request) {
StatusCode.Code.NOT_FOUND);
}

@Override
public ApiFuture<ListLogsResponse> listLogs(ListLogsRequest request) {
return translate(loggingClient.listLogsCallable().futureCall(request), true);
}

@Override
public ApiFuture<Empty> delete(DeleteLogRequest request) {
return translate(
Expand Down
Expand Up @@ -34,6 +34,8 @@
import com.google.logging.v2.ListLogEntriesResponse;
import com.google.logging.v2.ListLogMetricsRequest;
import com.google.logging.v2.ListLogMetricsResponse;
import com.google.logging.v2.ListLogsRequest;
import com.google.logging.v2.ListLogsResponse;
import com.google.logging.v2.ListMonitoredResourceDescriptorsRequest;
import com.google.logging.v2.ListMonitoredResourceDescriptorsResponse;
import com.google.logging.v2.ListSinksRequest;
Expand Down Expand Up @@ -93,6 +95,18 @@ public interface LoggingRpc extends AutoCloseable, ServiceRpc {
*/
ApiFuture<Empty> delete(DeleteSinkRequest request);

/**
* Sends a request to list the log names in a project. This method returns a {@code ApiFuture}
* object to consume the result. {@link ApiFuture#get()} returns a response object containing the
* listing result.
*
* @param request the request object containing all of the parameters for the API call
*/
default ApiFuture<ListLogsResponse> listLogs(ListLogsRequest request) {
throw new UnsupportedOperationException(
"method list(ListLogsRequest request) does not have default implementation");
}

/**
* Sends a request to deletes a log. This method returns a {@code ApiFuture} object to consume the
* result. {@link ApiFuture#get()} returns {@link Empty#getDefaultInstance()} or {@code null} if
Expand Down
Expand Up @@ -63,6 +63,8 @@
import com.google.logging.v2.ListLogEntriesResponse;
import com.google.logging.v2.ListLogMetricsRequest;
import com.google.logging.v2.ListLogMetricsResponse;
import com.google.logging.v2.ListLogsRequest;
import com.google.logging.v2.ListLogsResponse;
import com.google.logging.v2.ListMonitoredResourceDescriptorsRequest;
import com.google.logging.v2.ListMonitoredResourceDescriptorsResponse;
import com.google.logging.v2.ListSinksRequest;
Expand Down Expand Up @@ -106,6 +108,9 @@ public class LoggingImplTest {
com.google.api.MonitoredResourceDescriptor.getDefaultInstance();
private static final MonitoredResourceDescriptor DESCRIPTOR =
MonitoredResourceDescriptor.fromPb(DESCRIPTOR_PB);
private static final String LOG_NAME1 = "test-list-log-name-1";
private static final String LOG_NAME2 = "test-list-log-name-2";
private static final String LOG_NAMES_CURSOR = "cursor";
private static final String LOG_NAME = "log";
private static final String LOG_NAME_PB = "projects/" + PROJECT + "/logs/" + LOG_NAME;
private static final MonitoredResource MONITORED_RESOURCE =
Expand Down Expand Up @@ -173,6 +178,40 @@ public com.google.api.MonitoredResourceDescriptor apply(
private LoggingRpc loggingRpcMock;
private Logging logging;

private void configureListLogsTests(List<String> returnedList, String cursor) {
ListLogsRequest request = ListLogsRequest.newBuilder().setParent(PROJECT_PB).build();
ListLogsResponse response =
ListLogsResponse.newBuilder().setNextPageToken(cursor).addAllLogNames(returnedList).build();
ApiFuture<ListLogsResponse> futureResponse = ApiFutures.immediateFuture(response);
EasyMock.expect(loggingRpcMock.listLogs(request)).andReturn(futureResponse);
EasyMock.replay(loggingRpcMock);
}

private void configureListLogsTests(
List<String> page1ReturnedList,
List<String> page2ReturnedList,
String page1Cursor,
String page2Cursor) {
ListLogsRequest request1 = ListLogsRequest.newBuilder().setParent(PROJECT_PB).build();
ListLogsRequest request2 =
ListLogsRequest.newBuilder().setParent(PROJECT_PB).setPageToken(page1Cursor).build();
ListLogsResponse response1 =
ListLogsResponse.newBuilder()
.setNextPageToken(page1Cursor)
.addAllLogNames(page1ReturnedList)
.build();
ListLogsResponse response2 =
ListLogsResponse.newBuilder()
.setNextPageToken(page2Cursor)
.addAllLogNames(page2ReturnedList)
.build();
ApiFuture<ListLogsResponse> futureResponse1 = ApiFutures.immediateFuture(response1);
ApiFuture<ListLogsResponse> futureResponse2 = ApiFutures.immediateFuture(response2);
EasyMock.expect(loggingRpcMock.listLogs(request1)).andReturn(futureResponse1);
EasyMock.expect(loggingRpcMock.listLogs(request2)).andReturn(futureResponse2);
EasyMock.replay(loggingRpcMock);
}

@Before
public void setUp() {
rpcFactoryMock = EasyMock.createStrictMock(LoggingRpcFactory.class);
Expand All @@ -187,8 +226,10 @@ public void setUp() {
.build();

// By default when calling ListLogEntries, we append a filter of last 24 hours.
// However when testing, the time when it was called by the test and by the method
// implementation might differ by microseconds so we use the same time filter implementation
// However when testing, the time when it was called by the test and by the
// method
// implementation might differ by microseconds so we use the same time filter
// implementation
// for test and in "real" method
LoggingImpl.defaultTimestampFilterCreator =
new ITimestampDefaultFilter() {
Expand Down Expand Up @@ -1576,6 +1617,89 @@ public void testListResourceDescriptorAsyncWithOptions()
Iterables.toArray(page.getValues(), MonitoredResourceDescriptor.class));
}

@Test
public void testListLogsWithLogNames() {
EasyMock.replay(rpcFactoryMock);
logging = options.getService();
List<String> logNames = ImmutableList.of(LOG_NAME1, LOG_NAME2);
configureListLogsTests(logNames, LOG_NAMES_CURSOR);

Page<String> page = logging.listLogs();
assertEquals(LOG_NAMES_CURSOR, page.getNextPageToken());
assertArrayEquals(logNames.toArray(), Iterables.toArray(page.getValues(), String.class));
}

@Test
public void testListLogsWithEmptySet() {
EasyMock.replay(rpcFactoryMock);
logging = options.getService();
List<String> emptyList = ImmutableList.of();
configureListLogsTests(emptyList, LOG_NAMES_CURSOR);

Page<String> page = logging.listLogs();
assertEquals(LOG_NAMES_CURSOR, page.getNextPageToken());
assertArrayEquals(emptyList.toArray(), Iterables.toArray(page.getValues(), String.class));
}

@Test
public void testListLogsNextPageWithLogNames() throws ExecutionException, InterruptedException {
EasyMock.replay(rpcFactoryMock);
logging = options.getService();
List<String> logNames1 = ImmutableList.of(LOG_NAME1, LOG_NAME2);
List<String> logNames2 = ImmutableList.of(LOG_NAME1);
String nextPageCursor = "nextCursor";
configureListLogsTests(logNames1, logNames2, LOG_NAMES_CURSOR, nextPageCursor);

Page<String> page = logging.listLogs();
assertEquals(LOG_NAMES_CURSOR, page.getNextPageToken());
assertArrayEquals(logNames1.toArray(), Iterables.toArray(page.getValues(), String.class));
page = page.getNextPage();
assertEquals(nextPageCursor, page.getNextPageToken());
assertArrayEquals(logNames2.toArray(), Iterables.toArray(page.getValues(), String.class));
}

@Test
public void testListLogsAsyncWithLogNames() throws ExecutionException, InterruptedException {
EasyMock.replay(rpcFactoryMock);
logging = options.getService();
List<String> logNames = ImmutableList.of(LOG_NAME1, LOG_NAME2);
configureListLogsTests(logNames, LOG_NAMES_CURSOR);

AsyncPage<String> page = logging.listLogsAsync().get();
assertEquals(LOG_NAMES_CURSOR, page.getNextPageToken());
assertArrayEquals(logNames.toArray(), Iterables.toArray(page.getValues(), String.class));
}

@Test
public void testListLogsAsyncWithEmptySet() throws ExecutionException, InterruptedException {
EasyMock.replay(rpcFactoryMock);
logging = options.getService();
List<String> emptyList = ImmutableList.of();
configureListLogsTests(emptyList, LOG_NAMES_CURSOR);

AsyncPage<String> page = logging.listLogsAsync().get();
assertEquals(LOG_NAMES_CURSOR, page.getNextPageToken());
assertArrayEquals(emptyList.toArray(), Iterables.toArray(page.getValues(), String.class));
}

@Test
public void testListLogsAsyncNextPageWithLogNames()
throws ExecutionException, InterruptedException {
EasyMock.replay(rpcFactoryMock);
logging = options.getService();
List<String> logNames1 = ImmutableList.of(LOG_NAME1, LOG_NAME2);
List<String> logNames2 = ImmutableList.of(LOG_NAME1);
String nextPageCursor = "nextCursor";
configureListLogsTests(logNames1, logNames2, LOG_NAMES_CURSOR, nextPageCursor);

AsyncPage<String> page = logging.listLogsAsync().get();
assertEquals(LOG_NAMES_CURSOR, page.getNextPageToken());
assertArrayEquals(logNames1.toArray(), Iterables.toArray(page.getValues(), String.class));
page = page.getNextPageAsync().get();
assertEquals(nextPageCursor, page.getNextPageToken());
assertArrayEquals(logNames2.toArray(), Iterables.toArray(page.getValues(), String.class));
}

@Test
public void testDeleteLog() {
DeleteLogRequest request = DeleteLogRequest.newBuilder().setLogName(LOG_NAME_PB).build();
Expand Down Expand Up @@ -2034,7 +2158,8 @@ public void testFlushStress() throws InterruptedException {
EasyMock.expect(loggingRpcMock.write(request)).andReturn(mockRpcResponse).times(threads.length);
EasyMock.replay(loggingRpcMock);

// log and flush concurrently in many threads to trigger a ConcurrentModificationException
// log and flush concurrently in many threads to trigger a
// ConcurrentModificationException
final AtomicInteger exceptions = new AtomicInteger(0);
for (int i = 0; i < threads.length; i++) {
threads[i] =
Expand Down

0 comments on commit 9359569

Please sign in to comment.