http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/inject/DefaultProvider.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/org/apache/aurora/common/inject/DefaultProvider.java b/commons/src/main/java/org/apache/aurora/common/inject/DefaultProvider.java new file mode 100644 index 0000000..38fbe4d --- /dev/null +++ b/commons/src/main/java/org/apache/aurora/common/inject/DefaultProvider.java @@ -0,0 +1,166 @@ +/** + * 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.apache.aurora.common.inject; + +import com.google.common.base.Preconditions; +import com.google.inject.AbstractModule; +import com.google.inject.Binder; +import com.google.inject.Inject; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Provider; +import com.google.inject.TypeLiteral; +import com.google.inject.name.Named; +import com.google.inject.name.Names; + +/** + * Provider that has a default value which can be overridden. + * + * The intended use of this class is: + * <pre> + * Default installer: + * bind(DefaultProvider.makeDefaultKey(Runnable.class, "mykey").toInstance(defaultRunnable); + * DefaultProvider.bindOrElse(Runnable.class, "mykey", binder()); + * + * Custom override: + * bind(DefaultProvider.makeCustomKey(Runnable.class, "mykey")).toInstance(myCustomRunnable); + * + * Injection: + * {@literal Inject} Named("myKey") Runnable runnable; + * + * </pre> + * + * @param <T> the type of object this provides + * + * @author William Farner + * @author John Sirois + */ +public class DefaultProvider<T> implements Provider<T> { + private static final String DEFAULT_BINDING_KEY_SUFFIX = "_default"; + private static final String CUSTOM_BINDING_KEY_SUFFIX = "_custom"; + + private final Key<T> defaultProviderKey; + private final Key<T> customProviderKey; + + private Injector injector; + + public DefaultProvider(Key<T> defaultProviderKey, Key<T> customProviderKey) { + this.defaultProviderKey = Preconditions.checkNotNull(defaultProviderKey); + this.customProviderKey = Preconditions.checkNotNull(customProviderKey); + Preconditions.checkArgument(!defaultProviderKey.equals(customProviderKey)); + } + + @Inject + public void setInjector(Injector injector) { + this.injector = injector; + } + + @Override + public T get() { + Preconditions.checkNotNull(injector); + return injector.getBindings().containsKey(customProviderKey) + ? injector.getInstance(customProviderKey) + : injector.getInstance(defaultProviderKey); + } + + /** + * Creates a DefaultProvider and installs a new module to {@code binder}, which will serve as + * an indirection layer for swapping the default binding with a custom one. + * + * @param customBinding The custom binding key. + * @param defaultBinding The default binding key. + * @param exposedBinding The exposed binding key. + * @param binder The binder to install bindings to. + * @param <T> The type of binding to make. + */ + public static <T> void bindOrElse(final Key<T> customBinding, final Key<T> defaultBinding, + final Key<T> exposedBinding, Binder binder) { + Preconditions.checkNotNull(customBinding); + Preconditions.checkNotNull(defaultBinding); + Preconditions.checkNotNull(exposedBinding); + Preconditions.checkArgument(!customBinding.equals(defaultBinding) + && !customBinding.equals(exposedBinding)); + + binder.install(new AbstractModule() { + @Override protected void configure() { + Provider<T> defaultProvider = new DefaultProvider<T>(defaultBinding, customBinding); + requestInjection(defaultProvider); + bind(exposedBinding).toProvider(defaultProvider); + } + }); + } + + /** + * Convenience function for creating and installing a DefaultProvider. This will use internal + * suffixes to create names for the custom and default bindings. When bound this way, callers + * should use one of the functions such as {@link #makeDefaultBindingKey(String)} to set default + * and custom bindings. + * + * @param type The type of object to bind. + * @param exposedKey The exposed key. + * @param binder The binder to install to. + * @param <T> The type of binding to make. + */ + public static <T> void bindOrElse(TypeLiteral<T> type, String exposedKey, Binder binder) { + bindOrElse(Key.get(type, Names.named(makeCustomBindingKey(exposedKey))), + Key.get(type, Names.named(makeDefaultBindingKey(exposedKey))), + Key.get(type, Names.named(exposedKey)), + binder); + } + + /** + * Convenience method for calls to {@link #bindOrElse(TypeLiteral, String, Binder)}, that are not + * binding a parameterized type. + * + * @param type The class of the object to bind. + * @param exposedKey The exposed key. + * @param binder The binder to install to. + * @param <T> The type of binding to make. + */ + public static <T> void bindOrElse(Class<T> type, String exposedKey, Binder binder) { + bindOrElse(TypeLiteral.get(type), exposedKey, binder); + } + + public static String makeDefaultBindingKey(String rootKey) { + return rootKey + DEFAULT_BINDING_KEY_SUFFIX; + } + + public static Named makeDefaultBindingName(String rootKey) { + return Names.named(makeDefaultBindingKey(rootKey)); + } + + public static <T> Key<T> makeDefaultKey(TypeLiteral<T> type, String rootKey) { + return Key.get(type, makeDefaultBindingName(rootKey)); + } + + public static <T> Key<T> makeDefaultKey(Class<T> type, String rootKey) { + return makeDefaultKey(TypeLiteral.get(type), rootKey); + } + + public static String makeCustomBindingKey(String rootKey) { + return rootKey + CUSTOM_BINDING_KEY_SUFFIX; + } + + public static Named makeCustomBindingName(String rootKey) { + return Names.named(makeCustomBindingKey(rootKey)); + } + + public static <T> Key<T> makeCustomKey(Class<T> type, String rootKey) { + return Key.get(type, makeCustomBindingName(rootKey)); + } + + public static <T> Key<T> makeCustomKey(TypeLiteral<T> type, String rootKey) { + return Key.get(type, makeCustomBindingName(rootKey)); + } +}
http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/inject/ProviderMethodModule.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/org/apache/aurora/common/inject/ProviderMethodModule.java b/commons/src/main/java/org/apache/aurora/common/inject/ProviderMethodModule.java new file mode 100644 index 0000000..7543ff1 --- /dev/null +++ b/commons/src/main/java/org/apache/aurora/common/inject/ProviderMethodModule.java @@ -0,0 +1,32 @@ +/** + * 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.apache.aurora.common.inject; + +import com.google.inject.AbstractModule; + +/** + * A convenience base class for modules that do all their binding via provider methods. + * + * @author John Sirois + */ +public abstract class ProviderMethodModule extends AbstractModule { + + /** + * Does no binding; subclasses should implement provider methods. + */ + @Override + protected final void configure() { + // noop + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/inject/TimedInterceptor.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/org/apache/aurora/common/inject/TimedInterceptor.java b/commons/src/main/java/org/apache/aurora/common/inject/TimedInterceptor.java new file mode 100644 index 0000000..684e7bb --- /dev/null +++ b/commons/src/main/java/org/apache/aurora/common/inject/TimedInterceptor.java @@ -0,0 +1,106 @@ +/** + * 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.apache.aurora.common.inject; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Method; + +import com.google.common.base.Preconditions; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.inject.Binder; +import com.google.inject.matcher.Matchers; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.apache.commons.lang.StringUtils; + +import org.apache.aurora.common.stats.SlidingStats; +import org.apache.aurora.common.stats.TimeSeriesRepository; + +/** + * A method interceptor that exports timing information for methods annotated with + * {@literal @Timed}. + * + * @author John Sirois + */ +public final class TimedInterceptor implements MethodInterceptor { + + /** + * Marks a method as a target for timing. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + public @interface Timed { + + /** + * The base name to export timing data with; empty to use the annotated method's name. + */ + String value() default ""; + } + + private final LoadingCache<Method, SlidingStats> stats = + CacheBuilder.newBuilder().build(new CacheLoader<Method, SlidingStats>() { + @Override public SlidingStats load(Method method) { + return createStats(method); + } + }); + + private TimedInterceptor() { + // preserve for guice + } + + private SlidingStats createStats(Method method) { + Timed timed = method.getAnnotation(Timed.class); + Preconditions.checkArgument(timed != null, + "TimedInterceptor can only be applied to @Timed methods"); + + String name = timed.value(); + String statName = !StringUtils.isEmpty(name) ? name : method.getName(); + return new SlidingStats(statName, "nanos"); + } + + @Override + public Object invoke(MethodInvocation methodInvocation) throws Throwable { + // TODO(John Sirois): consider including a SlidingRate tracking thrown exceptions + SlidingStats stat = stats.get(methodInvocation.getMethod()); + long start = System.nanoTime(); + try { + return methodInvocation.proceed(); + } finally { + stat.accumulate(System.nanoTime() - start); + } + } + + /** + * Installs an interceptor in a guice {@link com.google.inject.Injector}, enabling + * {@literal @Timed} method interception in guice-provided instances. Requires that a + * {@link TimeSeriesRepository} is bound elsewhere. + * + * @param binder a guice binder to require bindings against + */ + public static void bind(Binder binder) { + Preconditions.checkNotNull(binder); + + Bindings.requireBinding(binder, TimeSeriesRepository.class); + + TimedInterceptor interceptor = new TimedInterceptor(); + binder.requestInjection(interceptor); + binder.bindInterceptor(Matchers.any(), Matchers.annotatedWith(Timed.class), interceptor); + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/io/Base64ZlibCodec.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/org/apache/aurora/common/io/Base64ZlibCodec.java b/commons/src/main/java/org/apache/aurora/common/io/Base64ZlibCodec.java new file mode 100644 index 0000000..ed36e2a --- /dev/null +++ b/commons/src/main/java/org/apache/aurora/common/io/Base64ZlibCodec.java @@ -0,0 +1,169 @@ +/** + * 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.apache.aurora.common.io; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.UnsupportedEncodingException; +import java.io.Writer; +import java.nio.charset.Charset; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.GZIPInputStream; +import java.util.zip.InflaterInputStream; + +import com.google.common.base.Preconditions; +import com.google.common.base.Throwables; +import com.google.common.io.ByteStreams; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.codec.binary.Base64OutputStream; + +/** + * Utility class providing encoding and decoding methods to and from a string to a utf-8 encoded, + * zlib compressed, Base64 encoded representation of the string. For wider compatibility, the + * decoder can also automatically recognize GZIP (instead of plain zlib) compressed data too and + * decode it accordingly. + * + * @author Attila Szegedi + */ +public final class Base64ZlibCodec { + /** + * Thrown to indicate invalid data while decoding or unzipping. + * + * @author Attila Szegedi + */ + public static class InvalidDataException extends Exception { + private static final long serialVersionUID = 1L; + + public InvalidDataException(String message) { + super(message); + } + + public InvalidDataException(String message, Throwable cause) { + super(message, cause); + } + } + + /** + * Text encoding used by the Base64 output stream. + */ + public static final String BASE64_TEXT_ENCODING = "ASCII"; + private static final int ESTIMATED_PLAINTEXT_TO_ENCODED_RATIO = 4; + + // Prefix all Base64-encoded, zlib compressed data must have + private static final byte[] ZLIB_HEADER_PREFIX = new byte[] { 120 }; + // Prefix all Base64-encoded, GZIP compressed data must have + private static final byte[] GZIP_HEADER_PREFIX = new byte[] {31, -117, 8, 0, 0, 0, 0, 0, 0 }; + private static final int DIAGNOSTIC_PREFIX_LENGTH = 16; + // Text encoding for char-to-byte transformation before compressing a stack trace + private static final Charset TEXT_ENCODING = com.google.common.base.Charsets.UTF_8; + + private Base64ZlibCodec() { + // Utility class + } + + /** + * Decodes a string. In addition to zlib, it also automatically detects GZIP compressed data and + * adjusts accordingly. + * + * @param encoded the encoded string, represented as a byte array of ASCII-encoded characters + * @return the decoded string + * @throws InvalidDataException if the string can not be decoded. + */ + public static byte[] decode(String encoded) throws InvalidDataException { + Preconditions.checkNotNull(encoded); + return decompress(new Base64().decode(encoded)); + } + + private static byte[] decompress(byte[] compressed) throws InvalidDataException { + byte[] bytes; + try { + final InputStream bin = new ByteArrayInputStream(compressed); + final InputStream zin; + if (startsWith(compressed, GZIP_HEADER_PREFIX)) { + zin = new GZIPInputStream(bin); + } else if (startsWith(compressed, ZLIB_HEADER_PREFIX)) { + zin = new InflaterInputStream(bin); + } else { + throw new Base64ZlibCodec.InvalidDataException("Value doesn't start with either GZIP or zlib header"); + } + try { + bytes = ByteStreams.toByteArray(zin); + } finally { + zin.close(); + } + } catch (IOException e) { + throw new Base64ZlibCodec.InvalidDataException("zlib/GZIP decoding error", e); + } + return bytes; + } + + private static boolean startsWith(byte[] value, byte[] prefix) { + final int pl = prefix.length; + if (value.length < pl) { + return false; + } + for (int i = 0; i < pl; ++i) { + if (value[i] != prefix[i]) { + return false; + } + } + return true; + } + + /** + * Encodes a set of bytes. + * + * @param plain the non-encoded bytes + * @return the encoded string + */ + public static String encode(byte[] plain) { + final ByteArrayOutputStream out = new ByteArrayOutputStream(plain.length + / ESTIMATED_PLAINTEXT_TO_ENCODED_RATIO); + final OutputStream w = getDeflatingEncodingStream(out); + try { + w.write(plain); + w.close(); + return out.toString(BASE64_TEXT_ENCODING); + } catch (UnsupportedEncodingException e) { + throw reportUnsupportedEncoding(); + } catch (IOException e) { + throw Throwables.propagate(e); + } + } + + private static OutputStream getDeflatingEncodingStream(OutputStream out) { + return new DeflaterOutputStream(new Base64OutputStream(out, true, + Integer.MAX_VALUE, null)); + } + + /** + * Returns a writer that writes through to the specified output stream, utf-8 encoding, + * zlib compressing, and Base64 encoding its input along the way. + * + * @param out the output stream that receives the final output + * @return a writer for the input + */ + public static Writer getEncodingWriter(OutputStream out) { + return new OutputStreamWriter(getDeflatingEncodingStream(out), TEXT_ENCODING); + } + + private static AssertionError reportUnsupportedEncoding() { + return new AssertionError(String.format("JVM doesn't support the %s encoding", TEXT_ENCODING)); + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/io/Codec.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/org/apache/aurora/common/io/Codec.java b/commons/src/main/java/org/apache/aurora/common/io/Codec.java new file mode 100644 index 0000000..94d1e36 --- /dev/null +++ b/commons/src/main/java/org/apache/aurora/common/io/Codec.java @@ -0,0 +1,53 @@ +/** + * 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.apache.aurora.common.io; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * A Codec represents a reversible encoding for a given type. Codecs are able to both + * {@link #deserialize(java.io.InputStream) read} items from streams and + * {@link #serialize(Object, java.io.OutputStream) write} items to streams. + * + * <p> TODO(John Sirois): consider whether this interface should optionally support null items to be + * read and written. + * + * @param <T> The type of object the Codec can handle. + * + * @author John Sirois + */ +public interface Codec<T> { + + /** + * Writes a representation of {@code item} to the {@code sink} that can be read back by + * {@link #deserialize(java.io.InputStream)}. + * + * @param item the item to serialize + * @param sink the stream to write the item out to + * @throws IOException if there is a problem serializing the item + */ + void serialize(T item, OutputStream sink) throws IOException; + + /** + * Reads an item from the {@code source} stream that was written by + * {@link #serialize(Object, java.io.OutputStream)}. + * + * @param source the stream to read an item from + * @return the deserialized item + * @throws IOException if there is a problem reading an item + */ + T deserialize(InputStream source) throws IOException; +} http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/io/CompatibilityCodec.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/org/apache/aurora/common/io/CompatibilityCodec.java b/commons/src/main/java/org/apache/aurora/common/io/CompatibilityCodec.java new file mode 100644 index 0000000..c49c7dd --- /dev/null +++ b/commons/src/main/java/org/apache/aurora/common/io/CompatibilityCodec.java @@ -0,0 +1,95 @@ +/** + * 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.apache.aurora.common.io; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PushbackInputStream; + +import com.google.common.base.Preconditions; +import com.google.common.base.Predicate; + +/** + * A codec that composes two codecs: a primary and a compatibility codec. It always serializes with + * the primary codec, but can make a decision on deserialization based on the first few bytes of the + * serialized format whether to use the compatibility codec. This allows for easier transition + * between storage formats as the codec remains able to read the old serialized format. + * + * @author Attila Szegedi + * + * @param <T> the type of objects this codec is for. + */ +public class CompatibilityCodec<T> implements Codec<T> { + private final Codec<T> primaryCodec; + private final Codec<T> secondaryCodec; + private final int prefixLength; + private final Predicate<byte[]> discriminator; + + private CompatibilityCodec(Codec<T> primaryCodec, Codec<T> secondaryCodec, int prefixLength, + Predicate<byte[]> discriminator) { + Preconditions.checkNotNull(primaryCodec); + Preconditions.checkNotNull(secondaryCodec); + this.primaryCodec = primaryCodec; + this.secondaryCodec = secondaryCodec; + this.prefixLength = prefixLength; + this.discriminator = discriminator; + } + + /** + * Creates a new compatibility codec instance. + * + * @param primaryCodec the codec used to serialize objects, as well as deserialize them when the + * first byte of the serialized format matches the discriminator. + * @param secondaryCodec the codec used to deserialize objects when the first byte of the + * serialized format does not match the discriminator. + * @param prefixLength the length, in bytes, of the prefix of the message that is inspected for + * determining the format. + * @param discriminator a predicate that will receive an array of at most prefixLength bytes + * (it can receive less if the serialized format is shorter) and has to return true + * if the primary codec should be used for deserialization, otherwise false. + */ + public static <T> CompatibilityCodec<T> create(Codec<T> primaryCodec, Codec<T> secondaryCodec, + int prefixLength, Predicate<byte[]> discriminator) { + return new CompatibilityCodec<T>(primaryCodec, secondaryCodec, prefixLength, discriminator); + } + + @Override + public T deserialize(InputStream source) throws IOException { + final PushbackInputStream in = new PushbackInputStream(source, prefixLength); + final byte[] prefix = readAtMostPrefix(in); + in.unread(prefix); + return (discriminator.apply(prefix) ? primaryCodec : secondaryCodec).deserialize(in); + } + + private byte[] readAtMostPrefix(InputStream in) throws IOException { + final byte[] prefix = new byte[prefixLength]; + int read = 0; + do { + final int readNow = in.read(prefix, read, prefixLength - read); + if (readNow == -1) { + byte[] newprefix = new byte[read]; + System.arraycopy(prefix, 0, newprefix, 0, read); + return newprefix; + } + read += readNow; + } while (read < prefixLength); + return prefix; + } + + @Override + public void serialize(T item, OutputStream sink) throws IOException { + primaryCodec.serialize(item, sink); + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/io/FileUtils.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/org/apache/aurora/common/io/FileUtils.java b/commons/src/main/java/org/apache/aurora/common/io/FileUtils.java new file mode 100644 index 0000000..348e859 --- /dev/null +++ b/commons/src/main/java/org/apache/aurora/common/io/FileUtils.java @@ -0,0 +1,193 @@ +/** + * 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.apache.aurora.common.io; + +import java.io.File; +import java.io.IOException; +import java.util.UUID; + +import com.google.common.base.Preconditions; + +import org.apache.commons.lang.SystemUtils; + +import org.apache.aurora.common.base.ExceptionalClosure; +import org.apache.aurora.common.base.ExceptionalFunction; + +/** + * Utility methods for working with files and directories. + * + * @author John Sirois + */ +public final class FileUtils { + + /** + * A utility for creating and working with temporary files and directories. + */ + public static class Temporary { + private static final int MAX_TMP_DIR_TRIES = 5; + + private final File basedir; + + /** + * Creates a new temporary utility that creates files and directories rooted at {@code basedir}. + * + * @param basedir The base directory to generate temporary files and directories in. + */ + public Temporary(File basedir) { + Preconditions.checkNotNull(basedir); + this.basedir = basedir; + } + + /** + * Returns a new empty temporary directory. + * + * @return a file representing the newly created directory. + * @throws IllegalStateException if a new temporary directory could not be created + */ + public File createDir() { + File tempDir; + int tries = 0; + do { + // For sanity sake, die eventually if we keep failing to pick a new unique directory name. + if (++tries > MAX_TMP_DIR_TRIES) { + throw new IllegalStateException("Failed to create a new temp directory in " + + MAX_TMP_DIR_TRIES + " attempts, giving up"); + } + tempDir = new File(basedir, UUID.randomUUID().toString()); + } while (!tempDir.mkdir()); + return tempDir; + } + + /** + * Creates a new empty temporary file. + * + * @return a new empty temporary file + * @throws IOException if there was a problem creating a new temporary file + */ + public File createFile() throws IOException { + return createFile(".tempfile"); + } + + /** + * Creates a new empty temporary file with the given filename {@code suffix}. + * + * @param suffix The suffix for the temporary file name + * @return a new empty temporary file + * @throws IOException if there was a problem creating a new temporary file + */ + public File createFile(String suffix) throws IOException { + return File.createTempFile(FileUtils.class.getName(), suffix, basedir); + } + + /** + * Creates a new temporary directory and executes the unit of {@code work} against it ensuring + * the directory and its contents are removed after the work completes normally or abnormally. + * + * @param work The unit of work to execute against the new temporary directory. + * @param <E> The type of exception this unit of work can throw. + * @throws E bubbled transparently when the unit of work throws + */ + public <E extends Exception> void doWithDir(final ExceptionalClosure<File, E> work) + throws E { + Preconditions.checkNotNull(work); + doWithDir(new ExceptionalFunction<File, Void, E>() { + @Override public Void apply(File dir) throws E { + work.execute(dir); + return null; + } + }); + } + + /** + * Creates a new temporary directory and executes the unit of {@code work} against it ensuring + * the directory and its contents are removed after the work completes normally or abnormally. + * + * @param work The unit of work to execute against the new temporary directory. + * @param <T> The type of result this unit of work produces. + * @param <E> The type of exception this unit of work can throw. + * @return the result when the unit of work completes successfully + * @throws E bubbled transparently when the unit of work throws + */ + public <T, E extends Exception> T doWithDir(ExceptionalFunction<File, T, E> work) + throws E { + Preconditions.checkNotNull(work); + return doWithTemp(createDir(), work); + } + + /** + * Creates a new temporary file and executes the unit of {@code work} against it ensuring + * the file is removed after the work completes normally or abnormally. + * + * @param work The unit of work to execute against the new temporary file. + * @param <E> The type of exception this unit of work can throw. + * @throws E bubbled transparently when the unit of work throws + * @throws IOException if there was a problem creating a new temporary file + */ + public <E extends Exception> void doWithFile(final ExceptionalClosure<File, E> work) + throws E, IOException { + Preconditions.checkNotNull(work); + doWithFile(new ExceptionalFunction<File, Void, E>() { + @Override public Void apply(File dir) throws E { + work.execute(dir); + return null; + } + }); + } + + /** + * Creates a new temporary file and executes the unit of {@code work} against it ensuring + * the file is removed after the work completes normally or abnormally. + * + * @param work The unit of work to execute against the new temporary file. + * @param <T> The type of result this unit of work produces. + * @param <E> The type of exception this unit of work can throw. + * @return the result when the unit of work completes successfully + * @throws E bubbled transparently when the unit of work throws + * @throws IOException if there was a problem creating a new temporary file + */ + public <T, E extends Exception> T doWithFile(ExceptionalFunction<File, T, E> work) + throws E, IOException { + Preconditions.checkNotNull(work); + return doWithTemp(createFile(), work); + } + + private static <T, E extends Exception> T doWithTemp(File file, + ExceptionalFunction<File, T, E> work) throws E { + try { + return work.apply(file); + } finally { + org.apache.commons.io.FileUtils.deleteQuietly(file); + } + } + } + + /** + * A temporary based at the default system temporary directory. + */ + public static final Temporary SYSTEM_TMP = new Temporary(SystemUtils.getJavaIoTmpDir()); + + /** + * Returns a new empty temporary directory. + * + * @return a file representing the newly created directory. + * @throws IllegalStateException if a new temporary directory could not be created + */ + public static File createTempDir() { + return SYSTEM_TMP.createDir(); + } + + private FileUtils() { + // utility + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/io/JsonCodec.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/org/apache/aurora/common/io/JsonCodec.java b/commons/src/main/java/org/apache/aurora/common/io/JsonCodec.java new file mode 100644 index 0000000..1b955db --- /dev/null +++ b/commons/src/main/java/org/apache/aurora/common/io/JsonCodec.java @@ -0,0 +1,124 @@ +/** + * 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.apache.aurora.common.io; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.util.BitSet; + +import com.google.common.base.Preconditions; +import com.google.gson.ExclusionStrategy; +import com.google.gson.FieldAttributes; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +/** + * A {@code Codec} that can encode and decode objects to and from JSON using the GSON library + * (which in turn will use reflection). The codec uses the UTF-8 encoding. + * + * @author Attila Szegedi + */ +public class JsonCodec<T> implements Codec<T> { + + private static final String ENCODING = "utf-8"; + + private final Class<T> clazz; + private final Gson gson; + + /** + * Creates a new JSON codec instance for objects of the specified class. + * + * @param clazz the class of the objects the created codec is for. + * @return a newly constructed JSON codec instance for objects of the requested class. + */ + public static <T> JsonCodec<T> create(Class<T> clazz) { + return new JsonCodec<T>(clazz, DefaultGsonHolder.instance); + } + + /** + * Creates a new JSON codec instance for objects of the specified class and the specified Gson + * instance. You can use this method if you need to customize the behavior of the Gson + * serializer. + * + * @param clazz the class of the objects the created codec is for. + * @param gson the Gson instance to use for serialization/deserialization. + * @return a newly constructed JSON codec instance for objects of the requested class. + */ + public static <T> JsonCodec<T> create(Class<T> clazz, Gson gson) { + return new JsonCodec<T>(clazz, gson); + } + + private JsonCodec(Class<T> clazz, Gson gson) { + Preconditions.checkNotNull(clazz); + Preconditions.checkNotNull(gson); + this.clazz = clazz; + this.gson = gson; + } + + private static final class DefaultGsonHolder { + static final Gson instance = new Gson(); + } + + /** + * Returns a Gson exclusion strategy that excludes Thrift synthetic fields from JSON + * serialization. You can pass it to a {@link GsonBuilder} to construct a customized {@link Gson} + * instance to use with {@link JsonCodec#create(Class, Gson)}. + * + * @return a Gson exclusion strategy for thrift synthetic fields. + */ + public static ExclusionStrategy getThriftExclusionStrategy() { + return ThriftExclusionStrategy.instance; + } + + private static final class ThriftExclusionStrategy implements ExclusionStrategy { + static final ExclusionStrategy instance = new ThriftExclusionStrategy(); + + public boolean shouldSkipClass(Class<?> clazz) { + return false; + } + + public boolean shouldSkipField(FieldAttributes f) { + // Exclude Thrift synthetic fields + return f.getDeclaredClass() == BitSet.class && f.getName().equals("__isset_bit_vector"); + } + } + + @Override + public T deserialize(InputStream source) throws IOException { + return gson.fromJson(new InputStreamReader(source, ENCODING), clazz); + } + + @Override + public void serialize(T item, OutputStream sink) throws IOException { + final Writer w = new OutputStreamWriter(new UnflushableOutputStream(sink), ENCODING); + gson.toJson(item, clazz, w); + w.flush(); + } + + private static class UnflushableOutputStream extends FilterOutputStream { + UnflushableOutputStream(OutputStream out) { + super(out); + } + + @Override + public void flush() throws IOException { + // Intentionally do nothing + } + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/io/Streamer.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/org/apache/aurora/common/io/Streamer.java b/commons/src/main/java/org/apache/aurora/common/io/Streamer.java new file mode 100644 index 0000000..9026760 --- /dev/null +++ b/commons/src/main/java/org/apache/aurora/common/io/Streamer.java @@ -0,0 +1,54 @@ +/** + * 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.apache.aurora.common.io; + +import com.google.common.base.Predicate; +import org.apache.aurora.common.base.Closure; + +/** + * Encapsulates iteration over a typed data stream that can be filtered. + * + * @author John Sirois + */ +public interface Streamer<T> { + + /** + * Processes a stream fully. This may cause a database query to be executed, a file to be read + * or even just call {@link Iterable#iterator()} depending on the implementation. Implementations + * guaranty that any resources allocated opening the stream will be closed whether or not process + * completes normally. + * + * @param work a closure over the work to be done for each item in the stream. + */ + void process(Closure<T> work); + + /** + * Returns a {@code Streamer} that will process the same stream as this streamer, but will stop + * processing when encountering the first item for which {@code cond} is true. + * + * @param cond a predicate that returns {@code false} as long as the stream should keep being + * processed. + * @return a streamer that will process items until the condition triggers. + */ + Streamer<T> endOn(Predicate<T> cond); + + /** + * Returns a {@code Streamer} that will process the same stream as this streamer, but with any + * items failing the filter to be omitted from processing. + * @param filter a predicate that returns {@code true} if an item in the stream should be + * processed + * @return a filtered streamer + */ + Streamer<T> filter(Predicate<T> filter); +} http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/io/ThriftCodec.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/org/apache/aurora/common/io/ThriftCodec.java b/commons/src/main/java/org/apache/aurora/common/io/ThriftCodec.java new file mode 100644 index 0000000..6644788 --- /dev/null +++ b/commons/src/main/java/org/apache/aurora/common/io/ThriftCodec.java @@ -0,0 +1,104 @@ +/** + * 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.apache.aurora.common.io; + +import com.google.common.base.Function; +import com.google.common.base.Preconditions; +import com.google.common.base.Supplier; +import org.apache.aurora.common.base.MoreSuppliers; +import org.apache.thrift.TBase; +import org.apache.thrift.TException; +import org.apache.thrift.protocol.TBinaryProtocol; +import org.apache.thrift.protocol.TCompactProtocol; +import org.apache.thrift.protocol.TJSONProtocol; +import org.apache.thrift.protocol.TProtocol; +import org.apache.thrift.transport.TIOStreamTransport; +import org.apache.thrift.transport.TTransport; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * A {@code Codec} that can encode and decode thrift structs. + */ +public class ThriftCodec<T extends TBase> implements Codec<T> { + + public static final Function<TTransport, TProtocol> JSON_PROTOCOL = + new Function<TTransport, TProtocol>() { + @Override public TProtocol apply(TTransport transport) { + return new TJSONProtocol(transport); + } + }; + + public static final Function<TTransport, TProtocol> BINARY_PROTOCOL = + new Function<TTransport, TProtocol>() { + @Override public TProtocol apply(TTransport transport) { + return new TBinaryProtocol(transport); + } + }; + + public static final Function<TTransport, TProtocol> COMPACT_PROTOCOL = + new Function<TTransport, TProtocol>() { + @Override public TProtocol apply(TTransport transport) { + return new TCompactProtocol(transport); + } + }; + + private final Supplier<T> templateSupplier; + private final Function<TTransport, TProtocol> protocolFactory; + + public static <T extends TBase> ThriftCodec<T> create(final Class<T> thriftStructType, + Function<TTransport, TProtocol> protocolFactory) { + return new ThriftCodec<T>(MoreSuppliers.of(thriftStructType), protocolFactory); + } + + /** + * @deprecated use {@link ThriftCodec#create(Class, Function)} instead. + */ + @Deprecated + public ThriftCodec(final Class<T> thriftStructType, + Function<TTransport, TProtocol> protocolFactory) { + this(MoreSuppliers.of(thriftStructType), protocolFactory); + } + + public ThriftCodec(Supplier<T> templateSupplier, + Function<TTransport, TProtocol> protocolFactory) { + this.templateSupplier = Preconditions.checkNotNull(templateSupplier); + this.protocolFactory = Preconditions.checkNotNull(protocolFactory); + } + + @Override + public void serialize(T item, OutputStream sink) throws IOException { + Preconditions.checkNotNull(item); + Preconditions.checkNotNull(sink); + try { + item.write(protocolFactory.apply(new TIOStreamTransport(null, sink))); + } catch (TException e) { + throw new IOException("Problem serializing thrift struct: " + item, e); + } + } + + @Override + public T deserialize(InputStream source) throws IOException { + Preconditions.checkNotNull(source); + T template = templateSupplier.get(); + try { + template.read(protocolFactory.apply(new TIOStreamTransport(source, null))); + } catch (TException e) { + throw new IOException("Problem de-serializing thrift struct from stream", e); + } + return template; + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/logging/BufferedLog.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/org/apache/aurora/common/logging/BufferedLog.java b/commons/src/main/java/org/apache/aurora/common/logging/BufferedLog.java new file mode 100644 index 0000000..a83895d --- /dev/null +++ b/commons/src/main/java/org/apache/aurora/common/logging/BufferedLog.java @@ -0,0 +1,278 @@ +/** + * 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.apache.aurora.common.logging; + +import com.google.common.base.Preconditions; +import com.google.common.base.Predicate; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import org.apache.aurora.common.quantity.Amount; +import org.apache.aurora.common.quantity.Time; +import org.apache.aurora.common.stats.StatImpl; +import org.apache.aurora.common.stats.Stats; + +import java.util.List; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.logging.Logger; + +/** + * Log that buffers requests before sending them to a wrapped log. + * + * @author William Farner + */ +public class BufferedLog<T, R> implements Log<T, Void> { + private static final Logger LOG = Logger.getLogger(BufferedLog.class.getName()); + + private static final ExecutorService DEFAULT_EXECUTOR_SERVICE = + Executors.newSingleThreadExecutor( + new ThreadFactoryBuilder().setDaemon(true).setNameFormat("Log Pusher-%d").build()); + private static final int DEFAULT_MAX_BUFFER_SIZE = 100000; + + // TODO(William Farner): Change to use a ScheduledExecutorService instead of a timer. + private final TimerTask logPusher = new TimerTask() { + @Override public void run() { + flush(); + } + }; + + // Local buffer of log messages. + private final List<T> localBuffer = Lists.newLinkedList(); + + // The log that is being buffered. + private Log<T, R> bufferedLog; + + // Filter to determine when a log request should be retried. + private Predicate<R> retryFilter = null; + + // Maximum number of log entries that can be buffered before truncation (lost messages). + private int maxBufferSize = DEFAULT_MAX_BUFFER_SIZE; + + // Maximum buffer length before attempting to submit. + private int chunkLength; + + // Maximum time for a message to sit in the buffer before attempting to flush. + private Amount<Integer, Time> flushInterval; + + // Service to handle flushing the log. + private ExecutorService logSubmitService = DEFAULT_EXECUTOR_SERVICE; + + private BufferedLog() { + // Created through builder. + + Stats.export(new StatImpl<Integer>("scribe_buffer_size") { + public Integer read() { return getBacklog(); } + }); + } + + public static <T, R> Builder<T, R> builder() { + return new Builder<T, R>(); + } + + /** + * Starts the log submission service by scheduling a timer to periodically submit messages. + */ + private void start() { + long flushIntervalMillis = flushInterval.as(Time.MILLISECONDS); + + new Timer(true).scheduleAtFixedRate(logPusher, flushIntervalMillis, flushIntervalMillis); + } + + /** + * Gets the current number of messages in the local buffer. + * + * @return The number of backlogged messages. + */ + protected int getBacklog() { + synchronized (localBuffer) { + return localBuffer.size(); + } + } + + /** + * Stores a log entry, flushing immediately if the buffer length limit is exceeded. + * + * @param entry Entry to log. + */ + @Override + public Void log(T entry) { + synchronized (localBuffer) { + localBuffer.add(entry); + + if (localBuffer.size() >= chunkLength) { + logSubmitService.submit(logPusher); + } + } + + return null; + } + + @Override + public Void log(List<T> entries) { + for (T entry : entries) log(entry); + + return null; + } + + @Override + public void flush() { + List<T> buffer = copyBuffer(); + if (buffer.isEmpty()) return; + + R result = bufferedLog.log(buffer); + + // Restore the buffer if the write was not successful. + if (retryFilter != null && retryFilter.apply(result)) { + LOG.warning("Log request failed, restoring spooled messages."); + restoreToLocalBuffer(buffer); + } + } + + /** + * Creats a snapshot of the local buffer and clears the local buffer. + * + * @return A snapshot of the local buffer. + */ + private List<T> copyBuffer() { + synchronized (localBuffer) { + List<T> bufferCopy = ImmutableList.copyOf(localBuffer); + localBuffer.clear(); + return bufferCopy; + } + } + + /** + * Restores log entries back to the local buffer. This can be used to commit entries back to the + * buffer after a flush operation failed. + * + * @param buffer The log entries to restore. + */ + private void restoreToLocalBuffer(List<T> buffer) { + synchronized (localBuffer) { + int restoreRecords = Math.min(buffer.size(), maxBufferSize - localBuffer.size()); + + if (restoreRecords != buffer.size()) { + LOG.severe((buffer.size() - restoreRecords) + " log records truncated!"); + + if (restoreRecords == 0) return; + } + + localBuffer.addAll(0, buffer.subList(buffer.size() - restoreRecords, buffer.size())); + } + } + + /** + * Configures a BufferedLog object. + * + * @param <T> Log message type. + * @param <R> Log result type. + */ + public static class Builder<T, R> { + private final BufferedLog<T, R> instance; + + public Builder() { + instance = new BufferedLog<T, R>(); + } + + /** + * Specifies the log that should be buffered. + * + * @param bufferedLog Log to buffer requests to. + * @return A reference to the builder. + */ + public Builder<T, R> buffer(Log<T, R> bufferedLog) { + instance.bufferedLog = bufferedLog; + return this; + } + + /** + * Adds a custom retry filter that will be used to determine whether a log result {@code R} + * should be used to indicate that a log request should be retried. Log submit retry behavior + * is not defined when the filter throws uncaught exceptions. + * + * @param retryFilter Filter to determine whether to retry. + * @return A reference to the builder. + */ + public Builder<T, R> withRetryFilter(Predicate<R> retryFilter) { + instance.retryFilter = retryFilter; + return this; + } + + /** + * Specifies the maximum allowable buffer size, after which log records will be dropped to + * conserve memory. + * + * @param maxBufferSize Maximum buffer size. + * @return A reference to the builder. + */ + public Builder<T, R> withMaxBuffer(int maxBufferSize) { + instance.maxBufferSize = maxBufferSize; + return this; + } + + /** + * Specifies the desired number of log records to submit in each request. + * + * @param chunkLength Maximum number of records to accumulate before trying to submit. + * @return A reference to the builder. + */ + public Builder<T, R> withChunkLength(int chunkLength) { + instance.chunkLength = chunkLength; + return this; + } + + /** + * Specifies the maximum amount of time that a log entry may wait in the buffer before an + * attempt is made to flush the buffer. + * + * @param flushInterval Log flush interval. + * @return A reference to the builder. + */ + public Builder<T, R> withFlushInterval(Amount<Integer, Time> flushInterval) { + instance.flushInterval = flushInterval; + return this; + } + + /** + * Specifies the executor service to use for (synchronously or asynchronously) sending + * log entries. + * + * @param logSubmitService Log submit executor service. + * @return A reference to the builder. + */ + public Builder<T, R> withExecutorService(ExecutorService logSubmitService) { + instance.logSubmitService = logSubmitService; + return this; + } + + /** + * Creates the buffered log. + * + * @return The prepared buffered log. + */ + public BufferedLog<T, R> build() { + Preconditions.checkArgument(instance.chunkLength > 0); + Preconditions.checkArgument(instance.flushInterval.as(Time.MILLISECONDS) > 0); + Preconditions.checkNotNull(instance.logSubmitService); + Preconditions.checkArgument(instance.chunkLength <= instance.maxBufferSize); + + instance.start(); + + return instance; + } + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/logging/Glog.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/org/apache/aurora/common/logging/Glog.java b/commons/src/main/java/org/apache/aurora/common/logging/Glog.java new file mode 100644 index 0000000..5bae399 --- /dev/null +++ b/commons/src/main/java/org/apache/aurora/common/logging/Glog.java @@ -0,0 +1,208 @@ +/** + * 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.apache.aurora.common.logging; + +import javax.annotation.Nullable; + +import com.google.common.base.Objects; +import com.google.common.base.Throwables; + +import org.joda.time.DateTimeZone; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; + +/** + * A utility that can format log records to match the format generated by glog: + * <pre> + * I0218 17:36:47.461 (source) (message) + * </pre> + */ +public final class Glog { + + /** + * Classifies the importance of a log message. + */ + public enum Level { + + /** + * Indicates the message's classification is unknown. This most likely indicates a + * configuration or programming error that can be corrected by mapping the underlying log + * system's level appropriately. + */ + UNKNOWN('U'), + + /** + * Indicates the message is for debugging purposes only. + */ + DEBUG('D'), + + /** + * Indicates a message of general interest. + */ + INFO('I'), + + /** + * Indicates a warning message likely worth of attention. + */ + WARNING('W'), + + /** + * Indicates an unexpected error. + */ + ERROR('E'), + + /** + * Indicates a fatal exception generally paired with actions to shut down the errored process. + */ + FATAL('F'); + + final char label; + + private Level(char label) { + this.label = label; + } + } + + /** + * An object that can provide details of a log record. + * + * @param <T> The type of log record the formatter handles. + */ + public interface Formatter<T> { + + /** + * Gets the message contained in the log record. + * + * @param record The record to extract a message from. + * @return The formatted message. + */ + String getMessage(T record); + + /** + * Gets the class name of the class that sent the log record for logging. + * + * @param record The record to extract a producing class name from. + * @return The producing class if known; otherwise {@code null}. + */ + @Nullable + String getClassName(T record); + + /** + * Gets the name of the method of within the class that sent the log record for logging. + * + * @param record The record to extract a producing method name from. + * @return The producing method name if known; otherwise {@code null}. + */ + @Nullable + String getMethodName(T record); + + /** + * Gets the level of the log record. + * + * @param record The record to extract a log level from. + * @return The record's log level. Can be {@code null} or {@link Level#UNKNOWN} if unknown. + */ + @Nullable + Level getLevel(T record); + + /** + * Gets the timestamp in milliseconds since the epoch when the log record was generated. + * + * @param record The record to extract a time stamp from. + * @return The log record's birth date. + */ + long getTimeStamp(T record); + + /** + * Gets the id of the thread that generated the log record. + * + * @param record The record to extract a thread id from. + * @return The id of the thread that generated the log record. + */ + long getThreadId(T record); + + /** + * Gets the exception associated with the log record if any. + * + * @param record The record to extract an exception from. + * @return The exception associated with the log record; may be {@code null}. + */ + @Nullable + Throwable getThrowable(T record); + } + + private static final DateTimeFormatter DATE_TIME_FORMATTER = + DateTimeFormat.forPattern("MMdd HH:mm:ss.SSS").withZone(DateTimeZone.UTC); + + private static final int BASE_MESSAGE_LENGTH = + 1 // Level char. + + 4 // Month + day + + 1 // space + + 12 // Timestamp + + 1 // space + + 6 // THREAD + + 4 // Room for thread ID. + + 1; // space + + /** + * Converts the given log record into a glog format log line using the given formatter. + * + * @param formatter A formatter that understands how to unpack the given log record. + * @param record A structure containing log data. + * @param <T> The type of log record. + * @return A glog formatted log line. + */ + public static <T> String formatRecord(Formatter<T> formatter, T record) { + String message = formatter.getMessage(record); + int messageLength = BASE_MESSAGE_LENGTH + + 2 // Colon and space + + message.length(); + + String className = formatter.getClassName(record); + String methodName = null; + if (className != null) { + messageLength += className.length(); + methodName = formatter.getMethodName(record); + if (methodName != null) { + messageLength += 1; // Period between class and method. + messageLength += methodName.length(); + } + } + + StringBuilder sb = new StringBuilder(messageLength) + .append(Objects.firstNonNull(formatter.getLevel(record), Level.UNKNOWN).label) + .append(DATE_TIME_FORMATTER.print(formatter.getTimeStamp(record))) + .append(" THREAD") + .append(formatter.getThreadId(record)); + + if (className != null) { + sb.append(' ').append(className); + if (methodName != null) { + sb.append('.').append(methodName); + } + } + + sb.append(": ").append(message); + Throwable throwable = formatter.getThrowable(record); + if (throwable != null) { + sb.append('\n').append(Throwables.getStackTraceAsString(throwable)); + } + + return sb.append('\n').toString(); + } + + private Glog() { + // utility + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/logging/Log.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/org/apache/aurora/common/logging/Log.java b/commons/src/main/java/org/apache/aurora/common/logging/Log.java new file mode 100644 index 0000000..3f045f7 --- /dev/null +++ b/commons/src/main/java/org/apache/aurora/common/logging/Log.java @@ -0,0 +1,45 @@ +/** + * 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.apache.aurora.common.logging; + +import java.util.List; + +/** + * Logs messages to scribe. + * + * @author William Farner + */ +public interface Log<T, R> { + + /** + * Submits a log message. + * + * @param entry Entry to log. + * @return The result of the log request. + */ + public R log(T entry); + + /** + * Batch version of log. + * + * @param entries Entries to log. + * @return The result of the log request. + */ + public R log(List<T> entries); + + /** + * Flushes the log, attempting to purge any state that is only stored locally. + */ + public void flush(); +} http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/logging/LogFormatter.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/org/apache/aurora/common/logging/LogFormatter.java b/commons/src/main/java/org/apache/aurora/common/logging/LogFormatter.java new file mode 100644 index 0000000..0cb621d --- /dev/null +++ b/commons/src/main/java/org/apache/aurora/common/logging/LogFormatter.java @@ -0,0 +1,80 @@ +/** + * 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.apache.aurora.common.logging; + +import java.util.logging.Formatter; +import java.util.logging.Level; +import java.util.logging.LogRecord; + +import com.google.common.collect.ImmutableMap; + +/** + * Log formatter to match the format generated by glog. + * + * @see Glog + */ +public class LogFormatter extends Formatter implements Glog.Formatter<LogRecord> { + + private static final ImmutableMap<Level, Glog.Level> LEVEL_LABELS = + ImmutableMap.<Level, Glog.Level>builder() + .put(Level.FINEST, Glog.Level.DEBUG) + .put(Level.FINER, Glog.Level.DEBUG) + .put(Level.FINE, Glog.Level.DEBUG) + .put(Level.CONFIG, Glog.Level.INFO) + .put(Level.INFO, Glog.Level.INFO) + .put(Level.WARNING, Glog.Level.WARNING) + .put(Level.SEVERE, Glog.Level.ERROR) + .build(); + + + @Override + public String format(final LogRecord record) { + return Glog.formatRecord(this, record); + } + + @Override + public String getMessage(LogRecord record) { + return formatMessage(record); + } + + @Override + public String getClassName(LogRecord record) { + return record.getSourceClassName(); + } + + @Override + public String getMethodName(LogRecord record) { + return record.getSourceMethodName(); + } + + @Override + public Glog.Level getLevel(LogRecord record) { + return LEVEL_LABELS.get(record.getLevel()); + } + + @Override + public long getTimeStamp(LogRecord record) { + return record.getMillis(); + } + + @Override + public long getThreadId(LogRecord record) { + return record.getThreadID(); + } + + @Override + public Throwable getThrowable(LogRecord record) { + return record.getThrown(); + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/logging/LogUtil.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/org/apache/aurora/common/logging/LogUtil.java b/commons/src/main/java/org/apache/aurora/common/logging/LogUtil.java new file mode 100644 index 0000000..5f97cb2 --- /dev/null +++ b/commons/src/main/java/org/apache/aurora/common/logging/LogUtil.java @@ -0,0 +1,90 @@ +/** + * 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.apache.aurora.common.logging; + +import com.google.common.annotations.VisibleForTesting; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.SystemUtils; + +import java.io.File; +import java.util.logging.LogManager; +import java.util.logging.Logger; + +/** + * Logging utility functions. + * + * @author William Farner + */ +public class LogUtil { + + private static final Logger LOG = Logger.getLogger(LogUtil.class.getName()); + + private static final String LOG_MANAGER_FILE_PROP = "java.util.logging.FileHandler.pattern"; + + @VisibleForTesting + static final File DEFAULT_LOG_DIR = new File("/var/log"); + + /** + * Gets the log directory as configured with the log manager. This will attempt to expand any + * directory wildcards that are included in log file property. + * + * @return The configured log directory. + */ + public static File getLogManagerLogDir() { + return getLogManagerLogDir(LogManager.getLogManager().getProperty(LOG_MANAGER_FILE_PROP)); + } + + /** + * Gets the log directory as specified in a log file pattern. This will attempt to expand any + * directory wildcards that are included in log file property. + * + * @param logFilePattern The pattern to extract the log directory from. + * @return The configured log directory. + */ + public static File getLogManagerLogDir(String logFilePattern) { + if (StringUtils.isEmpty(logFilePattern)) { + LOG.warning("Could not find log dir in logging property " + LOG_MANAGER_FILE_PROP + + ", reading from " + DEFAULT_LOG_DIR); + return DEFAULT_LOG_DIR; + } + + String logDir = expandWildcard(logFilePattern, "%t", SystemUtils.JAVA_IO_TMPDIR); + logDir = expandWildcard(logDir, "%h", SystemUtils.USER_HOME); + File parent = new File(logDir).getParentFile(); + return parent == null ? new File(".") : parent; + } + + /** + * Expands a directory path wildcard within a file pattern string. + * Correctly handles cases where the replacement string does and does not contain a trailing + * slash. + * + * @param pattern File pattern string, which may or may not contain a wildcard. + * @param dirWildcard Wildcard string to expand. + * @param replacement Path component to expand wildcard to. + * @return {@code pattern} with all instances of {@code dirWildcard} replaced with + * {@code replacement}. + */ + private static String expandWildcard(String pattern, String dirWildcard, String replacement) { + String replace = dirWildcard; + if (replacement.charAt(replacement.length() - 1) == '/') { + replace += '/'; + } + return pattern.replaceAll(replace, replacement); + } + + private LogUtil() { + // Utility class. + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/logging/RootLogConfig.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/org/apache/aurora/common/logging/RootLogConfig.java b/commons/src/main/java/org/apache/aurora/common/logging/RootLogConfig.java new file mode 100644 index 0000000..7f010fd --- /dev/null +++ b/commons/src/main/java/org/apache/aurora/common/logging/RootLogConfig.java @@ -0,0 +1,339 @@ +/** + * 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.apache.aurora.common.logging; + +import java.util.HashMap; +import java.util.Map; +import java.util.logging.ConsoleHandler; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.Logger; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; + +import org.apache.aurora.common.args.Arg; +import org.apache.aurora.common.args.CmdLine; + +/** + * A configuration class for the root java.util.logging Logger. + * + * Defines flags to control the behavior behavior of the root logger similarly to Google's glog + * library (see http://code.google.com/p/google-glog ). + */ +public class RootLogConfig { + /** + * An enum reflecting log {@link Level} constants. + */ + public enum LogLevel { + FINEST(Level.FINEST), + FINER(Level.FINER), + FINE(Level.FINE), + CONFIG(Level.CONFIG), + INFO(Level.INFO), + WARNING(Level.WARNING), + SEVERE(Level.SEVERE); + + private final Level level; + + private LogLevel(Level level) { + this.level = level; + } + + private Level getLevel() { + return level; + } + } + + @CmdLine(name = "logtostderr", help = "Log messages to stderr instead of logfiles.") + private static Arg<Boolean> LOGTOSTDERR = Arg.create(false); + + @CmdLine(name = "alsologtostderr", + help = "Log messages to stderr, in addition to log files. Ignored when --logtostderr") + private static Arg<Boolean> ALSOLOGTOSTDERR = Arg.create(false); + + @CmdLine(name = "vlog", + help = "The value is one of the constants in java.util.logging.Level. " + + "Shows all messages with level equal or higher " + + "than the value of this flag.") + private static Arg<LogLevel> VLOG = Arg.create(LogLevel.INFO); + + @CmdLine(name = "vmodule", + help = "Per-class verbose level. The argument has to contain a comma-separated list " + + "of <class_name>=<log_level>. <class_name> is the full-qualified name of a " + + "class, <log_level> is one of the constants in java.util.logging.Level. " + + "<log_level> overrides any value given by --vlog.") + private static Arg<Map<Class<?>, LogLevel>> VMODULE = + Arg.<Map<Class<?>, LogLevel>>create(new HashMap<Class<?>, LogLevel>()); + + @CmdLine(name = "use_glog_formatter", help = "True to use the glog formatter exclusively.") + private static Arg<Boolean> USE_GLOG_FORMATTER = Arg.create(true); + + /** + * Represents a logging configuration for java.util.logging. + */ + public static final class Configuration { + boolean logToStderr = false; + boolean alsoLogToStderr = false; + boolean useGLogFormatter = true; + LogLevel vlog = LogLevel.INFO; + ImmutableMap<Class<?>, LogLevel> vmodule = ImmutableMap.of(); + String rootLoggerName = ""; + + Configuration() { + // Guard for internal use only. + } + + /** + * Returns {@code true} if configured to log just to stderr. + */ + public boolean isLogToStderr() { + return logToStderr; + } + + /** + * Returns {@code true} if configured to log to stderr in addition to log files.. + */ + public boolean isAlsoLogToStderr() { + return alsoLogToStderr; + } + + /** + * Returns {@code true} if configured to log in google-glog format. + */ + public boolean isUseGLogFormatter() { + return useGLogFormatter; + } + + /** + * Returns the default global log level. + */ + public LogLevel getVlog() { + return vlog; + } + + /** + * Returns the custom log levels configured for individual classes. + */ + public ImmutableMap<Class<?>, LogLevel> getVmodule() { + return vmodule; + } + + /** + * Applies this configuration to the root log. + */ + public void apply() { + RootLogConfig.configure(this); + } + } + + /** + * A builder-pattern class used to perform the configuration programmatically + * (i.e. not through flags). + * Example: + * <code> + * RootLogConfig.builder().logToStderr(true).build().apply(); + * </code> + */ + public static final class Builder { + private final Configuration configuration; + + private Builder() { + this.configuration = new Configuration(); + } + + /** + * Only log messages to stderr, instead of log files. Overrides alsologtostderr. + * Default: false. + * + * @param flag True to enable, false to disable. + * @return this Configuration object. + */ + public Builder logToStderr(boolean flag) { + configuration.logToStderr = flag; + return this; + } + + /** + * Also log messages to stderr, in addition to log files. + * Overridden by logtostderr. + * Default: false. + * + * @param flag True to enable, false to disable. + * @return this Configuration object. + */ + public Builder alsoLogToStderr(boolean flag) { + configuration.alsoLogToStderr = flag; + return this; + } + + /** + * Format log messages in one-line with a header, similar to google-glog. + * Default: false. + * + * @param flag True to enable, false to disable. + * @return this Configuration object. + */ + public Builder useGLogFormatter(boolean flag) { + configuration.useGLogFormatter = flag; + return this; + } + + /** + * Output log messages at least at the given verbosity level. + * Overridden by vmodule. + * Default: INFO + * + * @param level LogLevel enumerator for the minimum log message verbosity level that is output. + * @return this Configuration object. + */ + public Builder vlog(LogLevel level) { + Preconditions.checkNotNull(level); + configuration.vlog = level; + return this; + } + + /** + * Output log messages for a given set of classes at the associated verbosity levels. + * Overrides vlog. + * Default: no classes are treated specially. + * + * @param pairs Map of classes and correspoding log levels. + * @return this Configuration object. + */ + public Builder vmodule(Map<Class<?>, LogLevel> pairs) { + Preconditions.checkNotNull(pairs); + configuration.vmodule = ImmutableMap.copyOf(pairs); + return this; + } + + /** + * Returns the built up configuration. + */ + public Configuration build() { + return configuration; + } + + // Intercepts the root logger, for testing purposes only. + @VisibleForTesting + Builder rootLoggerName(String name) { + Preconditions.checkNotNull(name); + Preconditions.checkArgument(!name.isEmpty()); + configuration.rootLoggerName = name; + return this; + } + } + + /** + * Creates a new Configuration builder object. + * + * @return The builder. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Creates a logging configuration using flags + * + * @return The logging configuration specified via command line flags. + */ + public static Configuration configurationFromFlags() { + return builder() + .logToStderr(LOGTOSTDERR.get()) + .alsoLogToStderr(ALSOLOGTOSTDERR.get()) + .useGLogFormatter(USE_GLOG_FORMATTER.get()) + .vlog(VLOG.get()) + .vmodule(VMODULE.get()) + .build(); + } + + private static void configure(Configuration configuration) { + // Edit the properties of the root logger. + Logger rootLogger = Logger.getLogger(configuration.rootLoggerName); + if (configuration.logToStderr) { + setLoggerToStderr(rootLogger); + } else if (configuration.alsoLogToStderr) { + setLoggerToAlsoStderr(rootLogger); + } + if (configuration.useGLogFormatter) { + setGLogFormatter(rootLogger); + } + if (configuration.vlog != null) { + setVlog(rootLogger, configuration.vlog); + } + if (configuration.vmodule != null) { + setVmodules(configuration.vmodule); + } + } + + private static void setLoggerToStderr(Logger logger) { + LogManager.getLogManager().reset(); + setConsoleHandler(logger, true); + } + + private static void setLoggerToAlsoStderr(Logger logger) { + setConsoleHandler(logger, false); + } + + private static void setConsoleHandler(Logger logger, boolean removeOtherHandlers) { + Handler consoleHandler = null; + for (Handler h : logger.getHandlers()) { + if (h instanceof ConsoleHandler) { + consoleHandler = h; + } else if (removeOtherHandlers) { + logger.removeHandler(h); + } + } + if (consoleHandler == null) { + consoleHandler = new ConsoleHandler(); + logger.addHandler(new ConsoleHandler()); + } + consoleHandler.setLevel(Level.ALL); + consoleHandler.setFilter(null); + } + + private static void setGLogFormatter(Logger logger) { + for (Handler h : logger.getHandlers()) { + h.setFormatter(new LogFormatter()); + } + } + + private static void setVmodules(Map<Class<?>, LogLevel> vmodules) { + for (Map.Entry<Class<?>, LogLevel> entry : vmodules.entrySet()) { + String className = entry.getKey().getName(); + Logger logger = Logger.getLogger(className); + setVlog(logger, entry.getValue()); + } + } + + private static void setVlog(Logger logger, LogLevel logLevel) { + final Level newLevel = logLevel.getLevel(); + logger.setLevel(newLevel); + do { + for (Handler handler : logger.getHandlers()) { + Level handlerLevel = handler.getLevel(); + if (newLevel.intValue() < handlerLevel.intValue()) { + handler.setLevel(newLevel); + } + } + } while (logger.getUseParentHandlers() && (logger = logger.getParent()) != null); + } + + // Utility class. + private RootLogConfig() { + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/logging/julbridge/JULBridgeHandler.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/org/apache/aurora/common/logging/julbridge/JULBridgeHandler.java b/commons/src/main/java/org/apache/aurora/common/logging/julbridge/JULBridgeHandler.java new file mode 100644 index 0000000..8a9e18e --- /dev/null +++ b/commons/src/main/java/org/apache/aurora/common/logging/julbridge/JULBridgeHandler.java @@ -0,0 +1,196 @@ +/** + * 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.apache.aurora.common.logging.julbridge; + +import java.text.MessageFormat; +import java.util.MissingResourceException; +import java.util.logging.Formatter; +import java.util.logging.Handler; +import java.util.logging.LogRecord; + +import javax.annotation.Nullable; + +import org.apache.log4j.Level; +import org.apache.log4j.LogManager; +import org.apache.log4j.Logger; +import org.apache.log4j.spi.LocationInfo; +import org.apache.log4j.spi.LoggerRepository; +import org.apache.log4j.spi.LoggingEvent; +import org.apache.log4j.spi.ThrowableInformation; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * JUL Handler to convert JUL {@link LogRecord} messages into Log4j's {@link LoggingEvent} messages, + * and route them to a Log4J logger with the same name as the JUL logger. + */ +public class JULBridgeHandler extends Handler { + private static final String UNKNOWN_LOGGERNAME = "unknown"; + + /** + * Converts a JUL log record into a Log4J logging event. + * + * @param record the JUL log record to convert + * @param logger the Log4J logger to use for the logging event + * @param level the Log4J level to use for the logging event + * @param useExtendedLocationInfo if false, do no try to get source file and line informations + * @return a Log4J logging event + */ + static LoggingEvent toLoggingEvent(LogRecord record, Logger logger, Level level, + boolean useExtendedLocationInfo) { + + LocationInfo locationInfo = useExtendedLocationInfo + ? new LocationInfo(new Throwable(), record.getSourceClassName()) + : new LocationInfo("?", record.getSourceClassName(), record.getSourceMethodName(), "?"); + + // Getting thread name from thread id? complicated... + String threadName = String.valueOf(record.getThreadID()); + ThrowableInformation throwableInformation = record.getThrown() == null + ? null + : new ThrowableInformation(record.getThrown()); + + return new LoggingEvent( + record.getSourceClassName(), + logger, + record.getMillis(), + level, + formatMessage(record), + threadName, + throwableInformation, + null /* ndc */, + locationInfo, + null /* properties */); + } + + /** + * Formats a log record message in a way similar to {@link Formatter#formatMessage(LogRecord)}. + * + * If the record contains a resource bundle, a lookup is done to find a localized version. + * + * If the record contains parameters, the message is formatted using + * {@link MessageFormat#format(String, Object...)} + * + * @param record the log record used to format the message + * @return a formatted string + */ + static String formatMessage(LogRecord record) { + String message = record.getMessage(); + + // Look for a resource bundle + java.util.ResourceBundle catalog = record.getResourceBundle(); + if (catalog != null) { + try { + message = catalog.getString(record.getMessage()); + } catch (MissingResourceException e) { + // Not found? Fallback to original message string + message = record.getMessage(); + } + } + + Object parameters[] = record.getParameters(); + if (parameters == null || parameters.length == 0) { + // No parameters? just return the message string + return message; + } + + // Try formatting + try { + return MessageFormat.format(message, parameters); + } catch (IllegalArgumentException e) { + return message; + } + } + + private final LoggerRepository loggerRepository; + private final boolean useExtendedLocationInfo; + + /** + * Creates a new JUL handler. Equivalent to calling {@link #JULBridgeHandler(boolean)} passing + * <code>false</code> as argument. + */ + public JULBridgeHandler() { + this(LogManager.getLoggerRepository(), false); + } + + /** + * Creates a new JUL handler. + * Equivalent to calling {@link #JULBridgeHandler(LoggerRepository, boolean)} passing + * <code>LogManager.getLoggerRepository()</code> and <code>useExtendedLocationInfo</code> as + * arguments. + * + * @param useExtendedLocationInfo if true, try to add source filename and line info to log message + */ + public JULBridgeHandler(boolean useExtendedLocationInfo) { + this(LogManager.getLoggerRepository(), useExtendedLocationInfo); + } + + /** + * Creates a new JUL handler. + * + * @param loggerRepository Log4j logger repository where to get loggers from + * @param useExtendedLocationInfo if true, try to add source filename and line info to log message + * @throws NullPointerException if loggerRepository is null + */ + public JULBridgeHandler(LoggerRepository loggerRepository, boolean useExtendedLocationInfo) { + this.loggerRepository = checkNotNull(loggerRepository); + this.useExtendedLocationInfo = useExtendedLocationInfo; + } + + /** + * Gets a Log4J Logger with the same name as the logger name stored in the log record. + * + * @param record a JUL log record + * @return a Log4J logger with the same name, or name {@value #UNKNOWN_LOGGERNAME} if no name is + * present in the record. + */ + Logger getLogger(LogRecord record) { + String loggerName = record.getLoggerName(); + if (loggerName == null) { + loggerName = UNKNOWN_LOGGERNAME; + } + + return loggerRepository.getLogger(loggerName); + } + + /** + * Publishes the log record to a Log4J logger of the same name. + * + * Before formatting the message, level is converted and message is discarded if Log4j logger is + * not enabled for that level. + * + * @param record the record to publish + */ + @Override + public void publish(@Nullable LogRecord record) { + // Ignore silently null records + if (record == null) { + return; + } + + Logger log4jLogger = getLogger(record); + Level log4jLevel = JULBridgeLevelConverter.toLog4jLevel(record.getLevel()); + + if (log4jLogger.isEnabledFor(log4jLevel)) { + LoggingEvent event = toLoggingEvent(record, log4jLogger, log4jLevel, useExtendedLocationInfo); + + log4jLogger.callAppenders(event); + } + } + + @Override + public void flush() {} + + @Override + public void close() {} +}