001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.logging;
019
020import java.io.Serializable;
021import java.nio.charset.StandardCharsets;
022import java.util.concurrent.atomic.AtomicBoolean;
023import java.util.concurrent.atomic.AtomicLong;
024import org.apache.logging.log4j.core.Appender;
025import org.apache.logging.log4j.core.Core;
026import org.apache.logging.log4j.core.Filter;
027import org.apache.logging.log4j.core.Layout;
028import org.apache.logging.log4j.core.LogEvent;
029import org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender;
030import org.apache.logging.log4j.core.appender.ManagerFactory;
031import org.apache.logging.log4j.core.appender.OutputStreamManager;
032import org.apache.logging.log4j.core.appender.rolling.FileSize;
033import org.apache.logging.log4j.core.config.Property;
034import org.apache.logging.log4j.core.config.plugins.Plugin;
035import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute;
036import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
037import org.apache.logging.log4j.core.config.plugins.validation.constraints.Required;
038
039/**
040 * Log4j2 appender to be used when running UTs.
041 * <p/>
042 * The main point here is to limit the total output size to prevent eating all the space of our ci
043 * system when something wrong in our code.
044 * <p/>
045 * See HBASE-26947 for more details.
046 */
047@Plugin(name = HBaseTestAppender.PLUGIN_NAME, category = Core.CATEGORY_NAME,
048    elementType = Appender.ELEMENT_TYPE, printObject = true)
049public final class HBaseTestAppender extends AbstractOutputStreamAppender<OutputStreamManager> {
050
051  public static final String PLUGIN_NAME = "HBaseTest";
052  private static final HBaseTestManagerFactory FACTORY = new HBaseTestManagerFactory();
053
054  public static class Builder<B extends Builder<B>> extends AbstractOutputStreamAppender.Builder<B>
055    implements org.apache.logging.log4j.core.util.Builder<HBaseTestAppender> {
056
057    @PluginBuilderAttribute
058    @Required
059    private Target target;
060
061    @PluginBuilderAttribute
062    @Required
063    private String maxSize;
064
065    public B setTarget(Target target) {
066      this.target = target;
067      return asBuilder();
068    }
069
070    public B setMaxSize(String maxSize) {
071      this.maxSize = maxSize;
072      return asBuilder();
073    }
074
075    @Override
076    public HBaseTestAppender build() {
077      long size = FileSize.parse(maxSize, -1);
078      if (size <= 0) {
079        LOGGER.error("Invalid maxSize {}", size);
080      }
081      Layout<? extends Serializable> layout = getOrCreateLayout(StandardCharsets.UTF_8);
082      OutputStreamManager manager =
083        OutputStreamManager.getManager(target.name(), FACTORY, new FactoryData(target, layout));
084      return new HBaseTestAppender(getName(), layout, getFilter(), isIgnoreExceptions(),
085        isImmediateFlush(), getPropertyArray(), manager, size);
086    }
087  }
088
089  /**
090   * Data to pass to factory method.Unable to instantiate
091   */
092  private static class FactoryData {
093    private final Target target;
094    private final Layout<? extends Serializable> layout;
095
096    public FactoryData(Target target, Layout<? extends Serializable> layout) {
097      this.target = target;
098      this.layout = layout;
099    }
100  }
101
102  /**
103   * Factory to create the Appender.
104   */
105  private static class HBaseTestManagerFactory
106    implements ManagerFactory<HBaseTestOutputStreamManager, FactoryData> {
107
108    /**
109     * Create an OutputStreamManager.
110     * @param name The name of the entity to manage.
111     * @param data The data required to create the entity.
112     * @return The OutputStreamManager
113     */
114    @Override
115    public HBaseTestOutputStreamManager createManager(final String name, final FactoryData data) {
116      return new HBaseTestOutputStreamManager(data.target, data.layout);
117    }
118  }
119
120  @PluginBuilderFactory
121  public static <B extends Builder<B>> B newBuilder() {
122    return new Builder<B>().asBuilder();
123  }
124
125  private final long maxSize;
126
127  private final AtomicLong size = new AtomicLong(0);
128
129  private final AtomicBoolean stop = new AtomicBoolean(false);
130
131  private HBaseTestAppender(String name, Layout<? extends Serializable> layout, Filter filter,
132    boolean ignoreExceptions, boolean immediateFlush, Property[] properties,
133    OutputStreamManager manager, long maxSize) {
134    super(name, layout, filter, ignoreExceptions, immediateFlush, properties, manager);
135    this.maxSize = maxSize;
136  }
137
138  @Override
139  public void append(LogEvent event) {
140    if (stop.get()) {
141      return;
142    }
143    // for accounting, here we always convert the event to byte array first
144    // this will effect performance a bit but it is OK since this is for UT only
145    byte[] bytes = getLayout().toByteArray(event);
146    if (bytes == null || bytes.length == 0) {
147      return;
148    }
149    long sizeAfterAppend = size.addAndGet(bytes.length);
150    if (sizeAfterAppend >= maxSize) {
151      // stop logging if the log size exceeded the limit
152      if (stop.compareAndSet(false, true)) {
153        LOGGER.error("Log size exceeded the limit {}, will stop logging to prevent eating"
154          + " too much disk space", maxSize);
155      }
156      return;
157    }
158    super.append(event);
159  }
160}