@@ -0,0 +1,99 @@
/*
* JBoss, Home of Professional Open Source.
*
* Copyright 2019 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* 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 org.wildfly.event.logger;

import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;

/**
* @author <a href="mailto:jperkins@redhat.com">James R. Perkins</a>
*/
class AsyncEventLogger extends AbstractEventLogger implements EventLogger, Runnable {

//0 = not running
//1 = queued
//2 = running
@SuppressWarnings({"unused", "FieldMayBeFinal"})
private volatile int state = 0;

private static final AtomicIntegerFieldUpdater<AsyncEventLogger> stateUpdater = AtomicIntegerFieldUpdater.newUpdater(AsyncEventLogger.class, "state");

private final EventWriter writer;
private final Executor executor;
private final Deque<Event> pendingMessages;

AsyncEventLogger(final String id, final EventWriter writer, final Executor executor) {
super(id);
this.writer = writer;
this.executor = executor;
pendingMessages = new ConcurrentLinkedDeque<>();
}

@Override
void log(final Event event) {
pendingMessages.add(event);
int state = stateUpdater.get(this);
if (state == 0) {
if (stateUpdater.compareAndSet(this, 0, 1)) {
executor.execute(this);
}
}
}

@Override
public void run() {
if (!stateUpdater.compareAndSet(this, 1, 2)) {
return;
}
List<Event> events = new ArrayList<>();
Event event;
// Only grab at most 1000 messages at a time
for (int i = 0; i < 1000; ++i) {
event = pendingMessages.poll();
if (event == null) {
break;
}
events.add(event);
}
try {
if (!events.isEmpty()) {
writeMessage(events);
}
} finally {
stateUpdater.set(this, 0);
// Check to see if there is still more messages and run again if there are
if (!events.isEmpty()) {
if (stateUpdater.compareAndSet(this, 0, 1)) {
executor.execute(this);
}
}
}
}

private void writeMessage(final List<Event> events) {
for (Event event : events) {
writer.write(event);
}
}
}
@@ -0,0 +1,53 @@
/*
* JBoss, Home of Professional Open Source.
*
* Copyright 2019 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* 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 org.wildfly.event.logger;

import java.time.Instant;
import java.util.Map;

/**
* Describes an event that has taken place.
*
* @author <a href="mailto:jperkins@redhat.com">James R. Perkins</a>
*/
public interface Event {

/**
* The source of this event.
*
* @return the source of this event
*/
String getSource();

/**
* The date this event was created.
*
* @return the date the event was created
*/
@SuppressWarnings("unused")
Instant getInstant();

/**
* The data associated with this event.
*
* @return the data for this event
*/
Map<String, Object> getData();
}
@@ -0,0 +1,37 @@
/*
* JBoss, Home of Professional Open Source.
*
* Copyright 2019 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* 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 org.wildfly.event.logger;

/**
* A formatter for formatting events.
*
* @author <a href="mailto:jperkins@redhat.com">James R. Perkins</a>
*/
public interface EventFormatter {

/**
* Formats the event into a string.
*
* @param event the event to format
*
* @return the formatted string
*/
String format(Event event);
}
@@ -0,0 +1,117 @@
/*
* JBoss, Home of Professional Open Source.
*
* Copyright 2019 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* 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 org.wildfly.event.logger;

import java.util.Map;
import java.util.concurrent.Executor;
import java.util.function.Supplier;

/**
* A logger for various events such as access or audit logging.
* <p>
* Note that a {@linkplain #getEventSource() event source} is an arbitrary string used to differentiate logging events.
* For example a web access event may have an even source of {@code web-access}.
* </p>
*
* @author <a href="mailto:jperkins@redhat.com">James R. Perkins</a>
*/
@SuppressWarnings({"StaticMethodOnlyUsedInOneClass", "unused", "UnusedReturnValue"})
public interface EventLogger {

/**
* Creates a new logger which defaults to writing {@linkplain JsonEventFormatter JSON} to
* {@link StdoutEventWriter stdout}.
*
* @param eventSource the identifier for the source of the event this logger is used for
*
* @return a new event logger
*/
static EventLogger createLogger(final String eventSource) {
return new StandardEventLogger(eventSource, StdoutEventWriter.of(JsonEventFormatter.builder().build()));
}

/**
* Creates a new event logger.
*
* @param eventSource the identifier for the source of the event this logger is used for
* @param writer the writer this logger will write to
*
* @return a new event logger
*/
static EventLogger createLogger(final String eventSource, final EventWriter writer) {
return new StandardEventLogger(eventSource, writer);
}

/**
* Creates a new asynchronous logger which defaults to writing {@linkplain JsonEventFormatter JSON} to
* {@link StdoutEventWriter stdout}.
*
* @param eventSource the identifier for the source of the event this logger is used for
* @param executor the executor to execute the threads in
*
* @return the new event logger
*/
static EventLogger createAsyncLogger(final String eventSource, final Executor executor) {
return new AsyncEventLogger(eventSource, StdoutEventWriter.of(JsonEventFormatter.builder().build()), executor);
}

/**
* Creates a new asynchronous event logger.
*
* @param eventSource the identifier for the source of the event this logger is used for
* @param writer the writer this logger will write to
* @param executor the executor to execute the threads in
*
* @return a new event logger
*/
static EventLogger createAsyncLogger(final String eventSource, final EventWriter writer, final Executor executor) {
return new AsyncEventLogger(eventSource, writer, executor);
}

/**
* Logs the event.
*
* @param event the event to log
*
* @return this logger
*/
EventLogger log(Map<String, Object> event);

/**
* Logs the event.
* <p>
* The supplier can lazily load the data. Note that in the cases of an
* {@linkplain #createAsyncLogger(String, Executor) asynchronous logger} the {@linkplain Supplier#get() data} will
* be retrieved in a different thread.
* </p>
*
* @param event the event to log
*
* @return this logger
*/
EventLogger log(Supplier<Map<String, Object>> event);

/**
* Returns the source of event this logger is logging.
*
* @return the event source
*/
String getEventSource();
}
@@ -0,0 +1,35 @@
/*
* JBoss, Home of Professional Open Source.
*
* Copyright 2019 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* 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 org.wildfly.event.logger;

/**
* A writer used to write events.
*
* @author <a href="mailto:jperkins@redhat.com">James R. Perkins</a>
*/
public interface EventWriter extends AutoCloseable {

/**
* Writes the event.
*
* @param event the event to write
*/
void write(Event event);
}
@@ -0,0 +1,248 @@
/*
* JBoss, Home of Professional Open Source.
*
* Copyright 2019 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* 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 org.wildfly.event.logger;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.json.Json;
import javax.json.JsonArrayBuilder;
import javax.json.JsonBuilderFactory;
import javax.json.JsonObjectBuilder;
import javax.json.JsonValue;

/**
* A formatter which transforms the event into a JSON string.
*
* @author <a href="mailto:jperkins@redhat.com">James R. Perkins</a>
*/
public class JsonEventFormatter implements EventFormatter {

private final JsonBuilderFactory factory;
private final Map<String, Object> metaData;
private final String timestampKey;
private final DateTimeFormatter formatter;
private final boolean includeTimestamp;

private JsonEventFormatter(final Map<String, Object> metaData, final String timestampKey,
final DateTimeFormatter formatter, final boolean includeTimestamp) {
this.metaData = metaData;
this.timestampKey = timestampKey;
this.formatter = formatter;
this.includeTimestamp = includeTimestamp;
factory = Json.createBuilderFactory(Collections.emptyMap());
}

/**
* Creates a new builder to build a {@link JsonEventFormatter}.
*
* @return a new builder
*/
@SuppressWarnings("WeakerAccess")
public static Builder builder() {
return new Builder();
}

@Override
public String format(final Event event) {
final JsonObjectBuilder builder = factory.createObjectBuilder();
builder.add("eventSource", event.getSource());
if (includeTimestamp) {
builder.add(timestampKey, formatter.format(event.getInstant()));
}
add(builder, metaData);
add(builder, event.getData());
return builder.build().toString();
}

private void add(final JsonObjectBuilder builder, final Map<String, Object> data) {
for (Map.Entry<String, Object> entry : data.entrySet()) {
final String key = entry.getKey();
final Object value = entry.getValue();
if (value == null) {
builder.addNull(key);
} else if (value instanceof Boolean) {
builder.add(key, (Boolean) value);
} else if (value instanceof Double) {
builder.add(key, (Double) value);
} else if (value instanceof Integer) {
builder.add(key, (Integer) value);
} else if (value instanceof Long) {
builder.add(key, (Long) value);
} else if (value instanceof String) {
builder.add(key, (String) value);
} else if (value instanceof BigDecimal) {
builder.add(key, (BigDecimal) value);
} else if (value instanceof BigInteger) {
builder.add(key, (BigInteger) value);
} else if (value instanceof Collection) {
builder.add(key, factory.createArrayBuilder((Collection<?>) value));
} else if (value instanceof Map) {
final Map<?, ?> mapValue = (Map<?, ?>) value;
final JsonObjectBuilder valueBuilder = factory.createObjectBuilder();
// Convert the map to a string/object map
final Map<String, Object> map = new LinkedHashMap<>();
for (Map.Entry<?, ?> valueEntry : mapValue.entrySet()) {
final Object valueKey = valueEntry.getKey();
final Object valueValue = valueEntry.getValue();
if (valueKey instanceof String) {
map.put((String) valueKey, valueValue);
} else {
map.put(String.valueOf(valueKey), valueValue);
}
}
add(valueBuilder, map);
builder.add(key, valueBuilder);
} else if (value instanceof JsonArrayBuilder) {
builder.add(key, (JsonArrayBuilder) value);
} else if (value instanceof JsonObjectBuilder) {
builder.add(key, (JsonObjectBuilder) value);
} else if (value instanceof JsonValue) {
builder.add(key, (JsonValue) value);
} else if (value.getClass().isArray()) {
// We'll rely on the array builder to convert to the correct object type
builder.add(key, factory.createArrayBuilder(Arrays.asList((Object[]) value)));
} else {
builder.add(key, String.valueOf(value));
}
}
}

/**
* Builder used to create the {@link JsonEventFormatter}.
*/
@SuppressWarnings({"unused", "WeakerAccess"})
public static class Builder {
private Map<String, Object> metaData;
private String timestampKey;
private DateTimeFormatter formatter;
private ZoneId zoneId;
private boolean includeTimestamp = true;

private Builder() {
metaData = new LinkedHashMap<>();
}

/**
* Adds meta-data to the final output.
*
* @param key the key to add
* @param value the value for the key
*
* @return this builder
*/
public Builder addMetaData(final String key, final Object value) {
if (metaData == null) {
metaData = new LinkedHashMap<>();
}
metaData.put(key, value);
return this;
}

/**
* Adds meta-data to the final output.
*
* @param metaData the meta-data to add
*
* @return this builder
*/
public Builder addMetaData(final Map<String, Object> metaData) {
if (this.metaData == null) {
this.metaData = new LinkedHashMap<>();
}
this.metaData.putAll(metaData);
return this;
}

/**
* Sets the key for the timestamp for the event. The default is {@code timestamp}.
*
* @param timestampKey the key name or {@code null} to revert to the default
*
* @return this builder
*/
public Builder setTimestampKey(final String timestampKey) {
this.timestampKey = timestampKey;
return this;
}

/**
* Set the formatter used to format the timestamp on the event. The default is
* {@linkplain DateTimeFormatter#ISO_OFFSET_DATE_TIME ISO-8601}.
* <p>
* Note the {@linkplain #setZoneId(ZoneId) zone id} is {@linkplain DateTimeFormatter#withZone(ZoneId) zone id}
* on the formatter.
* </p>
*
* @param formatter the formatter to use or {@code null} to revert to the default.
*
* @return this builder
*/
public Builder setTimestampFormatter(final DateTimeFormatter formatter) {
this.formatter = formatter;
return this;
}

/**
* Set the zone id for the timestamp. The default is {@link ZoneId#systemDefault()}.
*
* @param zoneId the zone id to use or {@code null} to revert to the default
*
* @return this builder
*/
public Builder setZoneId(final ZoneId zoneId) {
this.zoneId = zoneId;
return this;
}

/**
* Sets whether or not the timestamp should be added to the output. The default is {@code true}. If set to
* {@code false} the {@linkplain #setZoneId(ZoneId) zone id} and
* {@linkplain #setTimestampFormatter(DateTimeFormatter) format} are ignored.
*
* @param includeTimestamp {@code true} to include the timestamp or {@code false} to leave the timestamp off
*
* @return this builder
*/
public Builder setIncludeTimestamp(final boolean includeTimestamp) {
this.includeTimestamp = includeTimestamp;
return this;
}

/**
* Creates the {@link JsonEventFormatter}.
*
* @return the newly created formatter
*/
public JsonEventFormatter build() {
final Map<String, Object> metaData = (this.metaData == null ? Collections.emptyMap() : new LinkedHashMap<>(this.metaData));
final String timestampKey = (this.timestampKey == null ? "timestamp" : this.timestampKey);
final DateTimeFormatter formatter = (this.formatter == null ? DateTimeFormatter.ISO_OFFSET_DATE_TIME : this.formatter);
final ZoneId zoneId = (this.zoneId == null ? ZoneId.systemDefault() : this.zoneId);
return new JsonEventFormatter(metaData, timestampKey, formatter.withZone(zoneId), includeTimestamp);
}
}
}
@@ -0,0 +1,43 @@
/*
* JBoss, Home of Professional Open Source.
*
* Copyright 2019 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* 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 org.wildfly.event.logger;

import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Supplier;

/**
* @author <a href="mailto:jperkins@redhat.com">James R. Perkins</a>
*/
class LazyEvent extends AbstractEvent implements Event {

private final Supplier<Map<String, Object>> data;

LazyEvent(final String eventSource, final Supplier<Map<String, Object>> data) {
super(eventSource);
this.data = data;
}

@Override
public Map<String, Object> getData() {
return Collections.unmodifiableMap(new LinkedHashMap<>(data.get()));
}
}
@@ -0,0 +1,41 @@
/*
* JBoss, Home of Professional Open Source.
*
* Copyright 2019 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* 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 org.wildfly.event.logger;

import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;

/**
* @author <a href="mailto:jperkins@redhat.com">James R. Perkins</a>
*/
class StandardEvent extends AbstractEvent implements Event {
private final Map<String, Object> data;

StandardEvent(final String eventSource, final Map<String, Object> data) {
super(eventSource);
this.data = Collections.unmodifiableMap(new LinkedHashMap<>(data));
}

@Override
public Map<String, Object> getData() {
return data;
}
}
@@ -0,0 +1,39 @@
/*
* JBoss, Home of Professional Open Source.
*
* Copyright 2019 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* 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 org.wildfly.event.logger;

/**
* @author <a href="mailto:jperkins@redhat.com">James R. Perkins</a>
*/
class StandardEventLogger extends AbstractEventLogger implements EventLogger {

private final EventWriter writer;

StandardEventLogger(final String eventSource, final EventWriter writer) {
super(eventSource);
this.writer = writer;
}

@Override
void log(final Event event) {
writer.write(event);
}

}
@@ -0,0 +1,63 @@
/*
* JBoss, Home of Professional Open Source.
*
* Copyright 2019 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* 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 org.wildfly.event.logger;

import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.PrintStream;

/**
* An event writer which writes directly to {@code stdout}.
*
* @author <a href="mailto:jperkins@redhat.com">James R. Perkins</a>
*/
public class StdoutEventWriter implements EventWriter {

private static final PrintStream STDOUT = new PrintStream(new FileOutputStream(FileDescriptor.out), true);

private final EventFormatter formatter;

private StdoutEventWriter(final EventFormatter formatter) {
this.formatter = formatter;
}

/**
* Creates a new {@code stdout} event writer with the provided formatter.
*
* @param formatter the formatter to use for formatting the event
*
* @return a new {@code stdout} event writer
*/
public static StdoutEventWriter of(final EventFormatter formatter) {
return new StdoutEventWriter(formatter);
}

@Override
public void write(final Event event) {
final EventFormatter formatter = this.formatter;
STDOUT.println(formatter.format(event));
}

@Override
public void close() {
// Don't actually close, just flush
STDOUT.flush();
}
}
@@ -0,0 +1,145 @@
/*
* JBoss, Home of Professional Open Source.
*
* Copyright 2019 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* 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 org.wildfly.event.logger;

import java.io.StringReader;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
import java.util.function.Function;
import javax.json.Json;
import javax.json.JsonObject;
import javax.json.JsonReader;

import org.junit.Assert;
import org.wildfly.common.cpu.ProcessorInfo;

/**
* @author <a href="mailto:jperkins@redhat.com">James R. Perkins</a>
*/
abstract class AbstractEventLoggerTestCase {
static final long TIMEOUT = 5L;

static void testLogger(final EventLogger logger, final QueuedJsonWriter writer) throws Exception {
final ZonedDateTime now = ZonedDateTime.now();

final TestValues values = new TestValues()
.add("eventSource", logger.getEventSource(), JsonObject::getString)
.add("testBoolean", true, JsonObject::getBoolean)
.add("testString", "Test string", JsonObject::getString)
.add("testInt", 33, JsonObject::getInt)
.add("testLong", 138L, (json, key) -> json.getJsonNumber(key).longValue())
.add("testDouble", 6.50d, Double::doubleToLongBits, (json, key) -> Double.doubleToLongBits(json.getJsonNumber(key).doubleValue()))
.add("testDate", now, ZonedDateTime::toString, JsonObject::getString)
.add("testDecimal", new BigDecimal("33.50"), (json, key) -> json.getJsonNumber(key).bigDecimalValue())
.add("testBigInt", new BigInteger("8675309"), (json, key) -> json.getJsonNumber(key).bigIntegerValue());

logger.log(values.asMap());

final String jsonString = writer.events.poll(TIMEOUT, TimeUnit.SECONDS);
Assert.assertNotNull("Expected value written, but was null", jsonString);

try (JsonReader reader = Json.createReader(new StringReader(jsonString))) {
final JsonObject jsonObject = reader.readObject();
for (TestValue<?> testValue : values) {
testValue.compare(jsonObject);
}
}

Assert.assertTrue("Expected no more events: " + writer.events, writer.events.isEmpty());
}

static ExecutorService createExecutor() {
return Executors.newFixedThreadPool(Math.max(2, ProcessorInfo.availableProcessors() - 2));
}

static class TestValue<V> {
final String key;
final V value;
final BiFunction<JsonObject, String, Object> mapper;
final Function<V, Object> valueConverter;

private TestValue(final String key, final V value, final BiFunction<JsonObject, String, Object> mapper) {
this(key, value, (v) -> v, mapper);
}

private TestValue(final String key, final V value, final Function<V, Object> valueConverter, final BiFunction<JsonObject, String, Object> mapper) {
this.key = key;
this.value = value;
this.valueConverter = valueConverter;
this.mapper = mapper;
}

void compare(final JsonObject json) {
Assert.assertEquals(valueConverter.apply(value), mapper.apply(json, key));
}

@Override
public String toString() {
return "TestValue[key=" + key + ", value=" + value + "]";
}
}

static class TestValues implements Iterable<TestValue<?>> {
private final Collection<TestValue<?>> values;

TestValues() {
values = new ArrayList<>();
}

<V> TestValues add(final String key, final V value, final BiFunction<JsonObject, String, Object> mapper) {
values.add(new TestValue<>(key, value, mapper));
return this;
}

<V> TestValues add(final String key, final V value, final Function<V, Object> valueConverter, final BiFunction<JsonObject, String, Object> mapper) {
values.add(new TestValue<>(key, value, valueConverter, mapper));
return this;
}

Map<String, Object> asMap() {
final Map<String, Object> result = new LinkedHashMap<>();
for (TestValue<?> testValue : values) {
result.put(testValue.key, testValue.value);
}
return result;
}

@SuppressWarnings("NullableProblems")
@Override
public Iterator<TestValue<?>> iterator() {
return values.iterator();
}

@Override
public String toString() {
return "TestValues[values=" + values + "]";
}
}
}
@@ -0,0 +1,135 @@
/*
* JBoss, Home of Professional Open Source.
*
* Copyright 2019 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* 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 org.wildfly.event.logger;

import java.io.StringReader;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import javax.json.Json;
import javax.json.JsonObject;
import javax.json.JsonReader;

import org.junit.Assert;
import org.junit.Test;

/**
* @author <a href="mailto:jperkins@redhat.com">James R. Perkins</a>
*/
@SuppressWarnings("MagicNumber")
public class AsyncEventLoggerTestCase extends AbstractEventLoggerTestCase {

@Test
public void testLogger() throws Exception {
final ExecutorService executor = Executors.newSingleThreadExecutor();
try {
final QueuedJsonWriter writer = new QueuedJsonWriter();
final EventLogger logger = EventLogger.createAsyncLogger("test-async-logger", writer, executor);
testLogger(logger, writer);
} finally {
executor.shutdown();
Assert.assertTrue(String.format("Executed did not complete within %d seconds", TIMEOUT),
executor.awaitTermination(TIMEOUT, TimeUnit.SECONDS));
}
}

@Test
public void testMultiLogger() throws Exception {
final ExecutorService executor = createExecutor();
try {
final QueuedJsonWriter writer = new QueuedJsonWriter();
final EventLogger logger = EventLogger.createAsyncLogger("test=multi-async-logger", writer, executor);
testMultiLogger(logger, writer, 100, true);
} finally {
executor.shutdown();
Assert.assertTrue(String.format("Executed did not complete within %d seconds", TIMEOUT),
executor.awaitTermination(TIMEOUT, TimeUnit.SECONDS));
}
}

@Test
public void testMultiFloodLogger() throws Exception {
final ExecutorService executor = createExecutor();
try {
final QueuedJsonWriter writer = new QueuedJsonWriter();
final EventLogger logger = EventLogger.createAsyncLogger("test=multi-async-logger", writer, executor);
testMultiLogger(logger, writer, 10000, false);
} finally {
executor.shutdown();
Assert.assertTrue(String.format("Executed did not complete within %d seconds", TIMEOUT),
executor.awaitTermination(TIMEOUT, TimeUnit.SECONDS));
}
}

private static void testMultiLogger(final EventLogger logger, final QueuedJsonWriter writer, final int logCount,
final boolean sleep) throws Exception {
final Random r = new Random();
final ExecutorService executor = createExecutor();
try {
final Map<Integer, TestValues> createValues = new HashMap<>();
for (int i = 0; i < logCount; i++) {
final TestValues values = new TestValues()
.add("eventSource", logger.getEventSource(), JsonObject::getString)
.add("count", i, JsonObject::getInt);
createValues.put(i, values);
// Use a supplier for every 5th event
final boolean useSupplier = (i % 5 == 0);
executor.submit(() -> {
if (useSupplier) {
logger.log(values::asMap);
} else {
logger.log(values.asMap());
}
if (sleep) {
// Add a short sleep to ensure slower messages still make it through and aren't lost
try {
TimeUnit.MILLISECONDS.sleep(r.nextInt(150));
} catch (InterruptedException e) {
Assert.fail("Interrupted running thread " + Thread.currentThread().getName() + ": " + e.getMessage());
}
}
});
}

for (int i = 0; i < logCount; i++) {
final String jsonString = writer.events.poll(TIMEOUT, TimeUnit.SECONDS);
Assert.assertNotNull("Expected value written, but was null", jsonString);

try (JsonReader reader = Json.createReader(new StringReader(jsonString))) {
final JsonObject jsonObject = reader.readObject();
final int count = jsonObject.getInt("count");
final TestValues values = createValues.remove(count);
Assert.assertNotNull("Failed to find value for entry " + count, values);
for (TestValue<?> testValue : values) {
testValue.compare(jsonObject);
}
}
}
Assert.assertTrue("Values were created that were not logged: " + createValues, createValues.isEmpty());
} finally {
executor.shutdown();
Assert.assertTrue(String.format("Executed did not complete within %d seconds", TIMEOUT),
executor.awaitTermination(TIMEOUT, TimeUnit.SECONDS));
}
}
}
@@ -0,0 +1,47 @@
/*
* JBoss, Home of Professional Open Source.
*
* Copyright 2019 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* 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 org.wildfly.event.logger;

import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;

/**
* @author <a href="mailto:jperkins@redhat.com">James R. Perkins</a>
*/
public class QueuedJsonWriter implements EventWriter {

private final JsonEventFormatter formatter;

final BlockingDeque<String> events = new LinkedBlockingDeque<>();

QueuedJsonWriter() {
this.formatter = JsonEventFormatter.builder().build();
}

@Override
public void write(final Event event) {
events.add(formatter.format(event));
}

@Override
public void close() {
events.clear();
}
}
@@ -0,0 +1,179 @@
/*
* JBoss, Home of Professional Open Source.
*
* Copyright 2019 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* 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 org.wildfly.event.logger;

import java.io.StringReader;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonObject;
import javax.json.JsonReader;

import org.junit.Assert;
import org.junit.Test;

/**
* @author <a href="mailto:jperkins@redhat.com">James R. Perkins</a>
*/
public class StandardEventLoggerTestCase extends AbstractEventLoggerTestCase {
private static final int LOG_COUNT = 10000;

@Test
public void testLogger() throws Exception {
final QueuedJsonWriter writer = new QueuedJsonWriter();
final EventLogger logger = EventLogger.createLogger("test-logger", writer);
testLogger(logger, writer);
}

@Test
public void testMultiLogger() throws Exception {
final QueuedJsonWriter writer = new QueuedJsonWriter();
final EventLogger logger = EventLogger.createLogger("test-multi-logger", writer);
testMultiLogger(logger, writer);
}

@Test
public void testCollection() throws Exception {
final QueuedJsonWriter writer = new QueuedJsonWriter();
final EventLogger logger = EventLogger.createLogger("test-collection-logger", writer);
final List<String> expectedValues = Arrays.asList("a", "b", "c", "1", "2", "3");
final Map<String, Object> events = new LinkedHashMap<>();
events.put("testCollection", expectedValues);
logger.log(events);

final String jsonString = writer.events.poll(TIMEOUT, TimeUnit.SECONDS);
Assert.assertNotNull("Expected value written, but was null", jsonString);

try (JsonReader reader = Json.createReader(new StringReader(jsonString))) {
final JsonObject jsonObject = reader.readObject();
final JsonArray array = jsonObject.getJsonArray("testCollection");
Assert.assertEquals(expectedValues.size(), array.size());
for (int i = 0; i < expectedValues.size(); i++) {
Assert.assertEquals(expectedValues.get(i), array.getString(i));
}
}
}

@Test
public void testMap() throws Exception {
final QueuedJsonWriter writer = new QueuedJsonWriter();
final EventLogger logger = EventLogger.createLogger("test-map-logger", writer);
final Map<String, String> expectedValues = new LinkedHashMap<>();
expectedValues.put("key1", "value1");
expectedValues.put("key2", "value2");
expectedValues.put("key3", "value3");
final Map<String, Object> events = new LinkedHashMap<>();
events.put("testMap", expectedValues);
logger.log(events);

final String jsonString = writer.events.poll(TIMEOUT, TimeUnit.SECONDS);
Assert.assertNotNull("Expected value written, but was null", jsonString);

try (JsonReader reader = Json.createReader(new StringReader(jsonString))) {
final JsonObject jsonObject = reader.readObject();
final JsonObject value = jsonObject.getJsonObject("testMap");
Assert.assertEquals(expectedValues.size(), value.size());
Assert.assertEquals(expectedValues.get("key1"), value.getString("key1"));
Assert.assertEquals(expectedValues.get("key2"), value.getString("key2"));
Assert.assertEquals(expectedValues.get("key3"), value.getString("key3"));
}
}

@Test
public void testArrays() throws Exception {
final QueuedJsonWriter writer = new QueuedJsonWriter();
final EventLogger logger = EventLogger.createLogger("test-array-logger", writer);
final Integer[] expectedIntValues = new Integer[] {1, 2, 3, 4, 5, 6};
final Map<String, Object> events = new LinkedHashMap<>();
events.put("testIntArray", expectedIntValues);
final String[] expectedStringValues = new String[] {"a", "b", "c"};
events.put("testStringArray", expectedStringValues);
logger.log(events);

final String jsonString = writer.events.poll(TIMEOUT, TimeUnit.SECONDS);
Assert.assertNotNull("Expected value written, but was null", jsonString);

try (JsonReader reader = Json.createReader(new StringReader(jsonString))) {
final JsonObject jsonObject = reader.readObject();

// Test int values
JsonArray array = jsonObject.getJsonArray("testIntArray");
Assert.assertEquals(expectedIntValues.length, array.size());
for (int i = 0; i < expectedIntValues.length; i++) {
Assert.assertEquals((int) expectedIntValues[i], array.getInt(i));
}

// Test string values
array = jsonObject.getJsonArray("testStringArray");
Assert.assertEquals(expectedStringValues.length, array.size());
for (int i = 0; i < expectedStringValues.length; i++) {
Assert.assertEquals(expectedStringValues[i], array.getString(i));
}
}
}

private static void testMultiLogger(final EventLogger logger, final QueuedJsonWriter writer) throws Exception {
final ExecutorService executor = createExecutor();
try {
final Map<Integer, TestValues> createValues = new HashMap<>();
for (int i = 0; i < LOG_COUNT; i++) {
final TestValues values = new TestValues()
.add("eventSource", logger.getEventSource(), JsonObject::getString)
.add("count", i, JsonObject::getInt);
createValues.put(i, values);
// Use a supplier for every 5th event
final boolean useSupplier = (i % 5 == 0);
executor.submit(() -> {
if (useSupplier) {
logger.log(values::asMap);
} else {
logger.log(values.asMap());
}
});
}

for (int i = 0; i < LOG_COUNT; i++) {
final String jsonString = writer.events.poll(TIMEOUT, TimeUnit.SECONDS);
Assert.assertNotNull("Expected value written, but was null", jsonString);

try (JsonReader reader = Json.createReader(new StringReader(jsonString))) {
final JsonObject jsonObject = reader.readObject();
final int count = jsonObject.getInt("count");
final TestValues values = createValues.remove(count);
Assert.assertNotNull("Failed to find value for entry " + count, values);
for (TestValue<?> testValue : values) {
testValue.compare(jsonObject);
}
}
}
Assert.assertTrue("Values were created that were not logged: " + createValues, createValues.isEmpty());
} finally {
executor.shutdown();
Assert.assertTrue(String.format("Executed did not complete within %d seconds", TIMEOUT),
executor.awaitTermination(TIMEOUT, TimeUnit.SECONDS));
}
}
}
@@ -243,6 +243,7 @@
<module>domain-management</module>
<module>elytron</module>
<module>embedded</module>
<module>event-logger</module>
<module>host-controller</module>
<module>logging</module>
<module>management-client-content</module>
@@ -1302,6 +1303,11 @@
<artifactId>wildfly-embedded</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.wildfly.core</groupId>
<artifactId>wildfly-event-logger</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.wildfly.core</groupId>
<artifactId>wildfly-host-controller</artifactId>
@@ -39,7 +39,9 @@
"javax.sql.api",
// No patching modules in layers
"org.jboss.as.patching",
"org.jboss.as.patching.cli"
"org.jboss.as.patching.cli",
// Not currently used internally
"org.wildfly.event.logger"
};
// Packages that are not referenced from the module graph but needed.
// This is the expected set of un-referenced modules found when scanning