Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Extend a set of options in WriteOption to allow defining the log location as custom project, folder, organization or billing account #727

Merged
merged 12 commits into from Nov 2, 2021
Merged
Expand Up @@ -69,7 +69,8 @@ final class WriteOption extends Option {
enum OptionType implements Option.OptionType {
LOG_NAME,
RESOURCE,
LABELS;
LABELS,
LOG_DESTINATION;

@SuppressWarnings("unchecked")
<T> T get(Map<Option.OptionType, ?> options) {
Expand Down Expand Up @@ -105,6 +106,14 @@ public static WriteOption resource(MonitoredResource resource) {
public static WriteOption labels(Map<String, String> labels) {
return new WriteOption(OptionType.LABELS, ImmutableMap.copyOf(labels));
}

/**
* Returns an option to specify a log destination resource path (see {@link LogDestinationName}
* for details)
*/
public static WriteOption destination(LogDestinationName destination) {
return new WriteOption(OptionType.LOG_DESTINATION, destination);
}
}

/** Fields according to which log entries can be sorted. */
Expand Down
Expand Up @@ -22,7 +22,9 @@
import com.google.cloud.logging.Logging.WriteOption;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
Expand Down Expand Up @@ -172,7 +174,7 @@ public LoggingHandler(String log, LoggingOptions options) {
* a default resource is created based on the project ID and deployment environment.
*/
public LoggingHandler(String log, LoggingOptions options, MonitoredResource monitoredResource) {
this(log, options, monitoredResource, null);
this(log, options, monitoredResource, null, null);
}

/**
Expand All @@ -190,6 +192,27 @@ public LoggingHandler(
LoggingOptions options,
MonitoredResource monitoredResource,
List<LoggingEnhancer> enhancers) {
this(log, options, monitoredResource, enhancers, null);
}

/**
* Creates a handler that publishes messages to Cloud Logging.
*
* @param log the name of the log to which log entries are written
* @param options options for the Cloud Logging service
* @param monitoredResource the monitored resource to which log entries refer. If it is null then
* a default resource is created based on the project ID and deployment environment.
* @param enhancers List of {@link LoggingEnhancer} instances used to enhance any{@link LogEntry}
* instances built by this handler.
* @param destination the log destination {@link LogDestinationName} (see 'logName' parameter in
* https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry)
*/
public LoggingHandler(
String log,
LoggingOptions options,
MonitoredResource monitoredResource,
List<LoggingEnhancer> enhancers,
LogDestinationName destination) {
try {
loggingOptions = options != null ? options : LoggingOptions.getDefaultInstance();
LoggingConfig config = new LoggingConfig(getClass().getName());
Expand All @@ -204,17 +227,20 @@ public LoggingHandler(
MonitoredResource resource =
firstNonNull(
monitoredResource, config.getMonitoredResource(loggingOptions.getProjectId()));
defaultWriteOptions =
new WriteOption[] {
WriteOption.logName(logName),
WriteOption.resource(resource),
WriteOption.labels(
ImmutableMap.of(
LEVEL_NAME_KEY,
baseLevel.getName(),
LEVEL_VALUE_KEY,
String.valueOf(baseLevel.intValue())))
};
List<WriteOption> writeOptions = new ArrayList<WriteOption>();
writeOptions.add(WriteOption.logName(logName));
writeOptions.add(WriteOption.resource(resource));
writeOptions.add(
WriteOption.labels(
ImmutableMap.of(
LEVEL_NAME_KEY,
baseLevel.getName(),
LEVEL_VALUE_KEY,
String.valueOf(baseLevel.intValue()))));
if (destination != null) {
writeOptions.add(WriteOption.destination(destination));
}
defaultWriteOptions = Iterables.toArray(writeOptions, WriteOption.class);

getLogging().setFlushSeverity(severityFor(flushLevel));
getLogging().setWriteSynchronicity(config.getSynchronicity());
Expand Down
Expand Up @@ -21,6 +21,7 @@
import static com.google.cloud.logging.Logging.ListOption.OptionType.PAGE_SIZE;
import static com.google.cloud.logging.Logging.ListOption.OptionType.PAGE_TOKEN;
import static com.google.cloud.logging.Logging.WriteOption.OptionType.LABELS;
import static com.google.cloud.logging.Logging.WriteOption.OptionType.LOG_DESTINATION;
import static com.google.cloud.logging.Logging.WriteOption.OptionType.LOG_NAME;
import static com.google.cloud.logging.Logging.WriteOption.OptionType.RESOURCE;

Expand Down Expand Up @@ -738,11 +739,16 @@ private static WriteLogEntriesRequest writeLogEntriesRequest(
LoggingOptions serviceOptions,
Iterable<LogEntry> logEntries,
Map<Option.OptionType, ?> options) {
String projectId = serviceOptions.getProjectId();
WriteLogEntriesRequest.Builder builder = WriteLogEntriesRequest.newBuilder();
String logName = LOG_NAME.get(options);
String projectId = null;

LogName logName =
getLogName(
serviceOptions.getProjectId(), LOG_NAME.get(options), LOG_DESTINATION.get(options));

losalex marked this conversation as resolved.
Show resolved Hide resolved
if (logName != null) {
builder.setLogName(LogName.ofProjectLogName(projectId, logName).toString());
builder.setLogName(logName.toString());
projectId = logName.getProject();
losalex marked this conversation as resolved.
Show resolved Hide resolved
}
MonitoredResource resource = RESOURCE.get(options);
if (resource != null) {
Expand All @@ -752,10 +758,26 @@ private static WriteLogEntriesRequest writeLogEntriesRequest(
if (labels != null) {
builder.putAllLabels(labels);
}

if (projectId == null) projectId = serviceOptions.getProjectId();
builder.addAllEntries(Iterables.transform(logEntries, LogEntry.toPbFunction(projectId)));
losalex marked this conversation as resolved.
Show resolved Hide resolved
return builder.build();
}

private static LogName getLogName(
String projectId, String logName, LogDestinationName destination) {
if (logName == null) {
return null;
}

// If no destination specified, fallback to project based log name
if (destination == null) {
return LogName.ofProjectLogName(projectId, logName);
}

return destination.toLogName(logName);
}

public void write(Iterable<LogEntry> logEntries, WriteOption... options) {
if (inWriteCall.get() != null) {
return;
Expand Down
Expand Up @@ -310,6 +310,28 @@ public void testPublishCustomResource() {
handler.publish(newLogRecord(Level.FINEST, MESSAGE));
}

@Test
public void testPublishCustomResourceWithFolder() {
testPublishCustomResourceWithDestination(FINEST_ENTRY, LogDestinationName.folder("folder"));
}

@Test
public void testPublishCustomResourceWithBilling() {
testPublishCustomResourceWithDestination(
FINEST_ENTRY, LogDestinationName.billingAccount("billing"));
}

@Test
public void testPublishCustomResourceWithOrganization() {
testPublishCustomResourceWithDestination(
FINEST_ENTRY, LogDestinationName.organization("organization"));
}

@Test
public void testPublishCustomResourceWithProject() {
testPublishCustomResourceWithDestination(FINEST_ENTRY, LogDestinationName.project(PROJECT));
}

@Test
public void testPublishKubernetesContainerResource() {
expect(options.getProjectId()).andReturn(PROJECT).anyTimes();
Expand Down Expand Up @@ -582,4 +604,27 @@ public void testClose() throws Exception {
handler.close();
handler.close();
}

private void testPublishCustomResourceWithDestination(
LogEntry entry, LogDestinationName destination) {
expect(options.getProjectId()).andReturn(PROJECT).anyTimes();
expect(options.getService()).andReturn(logging);
logging.setFlushSeverity(Severity.ERROR);
expectLastCall().once();
logging.setWriteSynchronicity(Synchronicity.ASYNC);
expectLastCall().once();
MonitoredResource resource = MonitoredResource.of("custom", ImmutableMap.<String, String>of());
logging.write(
ImmutableList.of(entry),
WriteOption.logName(LOG_NAME),
WriteOption.resource(resource),
WriteOption.labels(BASE_SEVERITY_MAP),
WriteOption.destination(destination));
expectLastCall().once();
replay(options, logging);
Handler handler = new LoggingHandler(LOG_NAME, options, resource, null, destination);
handler.setLevel(Level.ALL);
handler.setFormatter(new TestFormatter());
handler.publish(newLogRecord(Level.FINEST, MESSAGE));
}
}
Expand Up @@ -91,6 +91,10 @@
public class LoggingImplTest {

private static final String PROJECT = "project";
private static final String PROJECT_OVERRIDE = "projectoverride";
losalex marked this conversation as resolved.
Show resolved Hide resolved
private static final String FOLDER = "folder";
private static final String BILLING = "billing";
private static final String ORGANIZATION = "organization";
private static final String PROJECT_PB = "projects/" + PROJECT;
private static final String SINK_NAME = "sink";
private static final SinkInfo SINK_INFO =
Expand All @@ -113,6 +117,12 @@ public class LoggingImplTest {
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 String LOG_NAME_OVERRIDE_PB =
losalex marked this conversation as resolved.
Show resolved Hide resolved
"projects/" + PROJECT_OVERRIDE + "/logs/" + LOG_NAME;
private static final String LOG_NAME_FOLDER = "folders/" + FOLDER + "/logs/" + LOG_NAME;
private static final String LOG_NAME_BILLING = "billingAccounts/" + BILLING + "/logs/" + LOG_NAME;
private static final String LOG_NAME_ORGANIZATION =
"organizations/" + ORGANIZATION + "/logs/" + LOG_NAME;
private static final MonitoredResource MONITORED_RESOURCE =
MonitoredResource.newBuilder("global").addLabel("project-id", PROJECT).build();
private static final LogEntry LOG_ENTRY1 =
Expand Down Expand Up @@ -1795,25 +1805,32 @@ public void testWriteLogEntriesWithSeverityFlushEnabled() {

@Test
public void testWriteLogEntriesWithOptions() {
Map<String, String> labels = ImmutableMap.of("key", "value");
WriteLogEntriesRequest request =
WriteLogEntriesRequest.newBuilder()
.putAllLabels(labels)
.setLogName(LOG_NAME_PB)
.setResource(MONITORED_RESOURCE.toPb())
.addAllEntries(
Iterables.transform(
ImmutableList.of(LOG_ENTRY1, LOG_ENTRY2), LogEntry.toPbFunction(PROJECT)))
.build();
WriteLogEntriesResponse response = WriteLogEntriesResponse.newBuilder().build();
EasyMock.expect(loggingRpcMock.write(request)).andReturn(ApiFutures.immediateFuture(response));
EasyMock.replay(rpcFactoryMock, loggingRpcMock);
logging = options.getService();
logging.write(
ImmutableList.of(LOG_ENTRY1, LOG_ENTRY2),
WriteOption.logName(LOG_NAME),
WriteOption.resource(MONITORED_RESOURCE),
WriteOption.labels(labels));
test_write_log_entries_with_destination(
losalex marked this conversation as resolved.
Show resolved Hide resolved
PROJECT, LOG_NAME_PB, LogDestinationName.project(PROJECT));
losalex marked this conversation as resolved.
Show resolved Hide resolved
}

@Test
public void testWriteLogEntriesWithProjectOverrideOptions() {
test_write_log_entries_with_destination(
PROJECT_OVERRIDE, LOG_NAME_OVERRIDE_PB, LogDestinationName.project(PROJECT_OVERRIDE));
losalex marked this conversation as resolved.
Show resolved Hide resolved
}

@Test
public void testWriteLogEntriesWithFolderOptions() {
test_write_log_entries_with_destination(
PROJECT, LOG_NAME_FOLDER, LogDestinationName.folder(FOLDER));
}

@Test
public void testWriteLogEntriesWithBillingOptions() {
test_write_log_entries_with_destination(
PROJECT, LOG_NAME_BILLING, LogDestinationName.billingAccount(BILLING));
}

@Test
public void testWriteLogEntriesWithOrganizationOptions() {
test_write_log_entries_with_destination(
PROJECT, LOG_NAME_ORGANIZATION, LogDestinationName.organization(ORGANIZATION));
}

@Test
Expand Down Expand Up @@ -1852,7 +1869,8 @@ public void testWriteLogEntriesAsyncWithOptions() {
ImmutableList.of(LOG_ENTRY1, LOG_ENTRY2),
WriteOption.logName(LOG_NAME),
WriteOption.resource(MONITORED_RESOURCE),
WriteOption.labels(labels));
WriteOption.labels(labels),
WriteOption.destination(LogDestinationName.project(PROJECT)));
losalex marked this conversation as resolved.
Show resolved Hide resolved
logging.flush();
}

Expand Down Expand Up @@ -2183,4 +2201,28 @@ public void run() {
}
assertSame(0, exceptions.get());
}

private void test_write_log_entries_with_destination(
String projectId, String logName, LogDestinationName destination) {
Map<String, String> labels = ImmutableMap.of("key", "value");
WriteLogEntriesRequest request =
WriteLogEntriesRequest.newBuilder()
.putAllLabels(labels)
.setLogName(logName)
.setResource(MONITORED_RESOURCE.toPb())
.addAllEntries(
Iterables.transform(
ImmutableList.of(LOG_ENTRY1, LOG_ENTRY2), LogEntry.toPbFunction(projectId)))
.build();
WriteLogEntriesResponse response = WriteLogEntriesResponse.newBuilder().build();
EasyMock.expect(loggingRpcMock.write(request)).andReturn(ApiFutures.immediateFuture(response));
EasyMock.replay(rpcFactoryMock, loggingRpcMock);
logging = options.getService();
logging.write(
ImmutableList.of(LOG_ENTRY1, LOG_ENTRY2),
WriteOption.logName(LOG_NAME),
WriteOption.resource(MONITORED_RESOURCE),
WriteOption.labels(labels),
WriteOption.destination(destination));
}
}
Expand Up @@ -39,6 +39,10 @@ public class LoggingTest {
private static final String LOG_NAME = "logName";
private static final MonitoredResource RESOURCE =
MonitoredResource.of("global", ImmutableMap.of("project_id", "p"));
private static final String PROJECT_NAME = "project";
private static final String FOLDER_NAME = "folder";
private static final String ORGANIZATION_NAME = "organization";
private static final String BILLING_NAME = "billing";

@Test
public void testListOption() {
Expand Down Expand Up @@ -100,8 +104,33 @@ public void testWriteOption() {
writeOption = WriteOption.logName(LOG_NAME);
assertEquals(LOG_NAME, writeOption.getValue());
assertEquals(WriteOption.OptionType.LOG_NAME, writeOption.getOptionType());

writeOption = WriteOption.resource(RESOURCE);
assertEquals(RESOURCE, writeOption.getValue());
assertEquals(WriteOption.OptionType.RESOURCE, writeOption.getOptionType());

writeOption = WriteOption.destination(LogDestinationName.project(PROJECT_NAME));
LogDestinationName resource = (LogDestinationName) writeOption.getValue();
assertEquals(WriteOption.OptionType.LOG_DESTINATION, writeOption.getOptionType());
assertEquals(LogDestinationName.DestinationType.PROJECT, resource.getOptionType());
assertEquals(PROJECT_NAME, resource.getValue());

writeOption = WriteOption.destination(LogDestinationName.billingAccount(BILLING_NAME));
resource = (LogDestinationName) writeOption.getValue();
assertEquals(WriteOption.OptionType.LOG_DESTINATION, writeOption.getOptionType());
assertEquals(LogDestinationName.DestinationType.BILLINGACCOUNT, resource.getOptionType());
assertEquals(BILLING_NAME, resource.getValue());

writeOption = WriteOption.destination(LogDestinationName.folder(FOLDER_NAME));
resource = (LogDestinationName) writeOption.getValue();
assertEquals(WriteOption.OptionType.LOG_DESTINATION, writeOption.getOptionType());
assertEquals(LogDestinationName.DestinationType.FOLDER, resource.getOptionType());
assertEquals(FOLDER_NAME, resource.getValue());

writeOption = WriteOption.destination(LogDestinationName.organization(ORGANIZATION_NAME));
resource = (LogDestinationName) writeOption.getValue();
assertEquals(WriteOption.OptionType.LOG_DESTINATION, writeOption.getOptionType());
assertEquals(LogDestinationName.DestinationType.ORGANIZATION, resource.getOptionType());
assertEquals(ORGANIZATION_NAME, resource.getValue());
losalex marked this conversation as resolved.
Show resolved Hide resolved
}
}
Expand Up @@ -72,6 +72,8 @@ public class SerializationTest extends BaseSerializationTest {
private static final WriteOption LABELS_OPTION =
WriteOption.labels(ImmutableMap.of("key", "val"));
private static final WriteOption LOG_OPTION = WriteOption.labels(ImmutableMap.of("key", "val"));
private static final WriteOption DESTINATION_OPTION =
WriteOption.destination(LogDestinationName.project("project"));
private static final WriteOption RESOURCE_OPTION =
WriteOption.resource(MonitoredResource.of("global", ImmutableMap.of("project_id", "p")));
private static final EntryListOption ENTRY_PAGE_TOKEN_OPTION = EntryListOption.pageToken("token");
Expand Down Expand Up @@ -108,6 +110,7 @@ protected Serializable[] serializableObjects() {
LABELS_OPTION,
LOG_OPTION,
RESOURCE_OPTION,
DESTINATION_OPTION,
ENTRY_PAGE_TOKEN_OPTION,
ENTRY_PAGE_SIZE_OPTION,
ENTRY_FILTER_OPTION,
Expand Down