All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.epam.reportportal.service.LoggingContext Maven / Gradle / Ivy

Go to download

A application used as an example on how to set up pushing its components to the Central Repository .

The newest version!
/*
 * Copyright 2021 EPAM Systems
 *
 * 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 com.epam.reportportal.service;

import com.epam.reportportal.listeners.ListenerParameters;
import com.epam.reportportal.message.TypeAwareByteSource;
import com.epam.reportportal.service.logs.LogBatchingFlowable;
import com.epam.reportportal.service.logs.LoggingSubscriber;
import com.epam.reportportal.utils.RetryWithDelay;
import com.epam.reportportal.utils.http.HttpRequestUtils;
import com.epam.ta.reportportal.ws.model.BatchSaveOperatingRS;
import com.epam.ta.reportportal.ws.model.log.SaveLogRQ;
import com.google.common.io.ByteSource;
import io.reactivex.*;
import io.reactivex.functions.Function;
import io.reactivex.internal.operators.flowable.FlowableFromObservable;
import io.reactivex.plugins.RxJavaPlugins;
import io.reactivex.subjects.PublishSubject;
import org.apache.commons.lang3.tuple.Pair;
import org.reactivestreams.Publisher;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

import static com.epam.reportportal.utils.files.ImageConverter.convert;
import static com.epam.reportportal.utils.files.ImageConverter.isImage;
import static java.util.Optional.ofNullable;

/**
 * Logging context holds thread-local context for logging and converts
 * {@link SaveLogRQ} to multipart HTTP request to ReportPortal
 * Basic flow:
 * After start some test item (suite/test/step) context should be initialized with observable of
 * item ID and ReportPortal client.
 * Before actual finish of test item, context should be closed/completed.
 * Context consists of {@link Flowable} with buffering back-pressure strategy to be able
 * to batch incoming log messages into one request
 *
 * @author Andrei Varabyeu
 * @see LoggingContext#init(Maybe, Maybe, ReportPortalClient, Scheduler)
 */
public class LoggingContext {
    private static final int DEFAULT_RETRY_COUNT = 5;
    private static final int DEFAULT_RETRY_TIMEOUT = 2;
    public static final int DEFAULT_LOG_BATCH_SIZE = 10;

    @Deprecated
    public static final int DEFAULT_BUFFER_SIZE = DEFAULT_LOG_BATCH_SIZE;

    private static final ThreadLocal>> CONTEXT_THREAD_LOCAL = new InheritableThreadLocal<>();

    private static final Set THREAD_IDS = Collections.newSetFromMap(new ConcurrentHashMap<>());

    @Nonnull
    private static Deque createContext() {
        Long threadKey = Thread.currentThread().getId();
        if (!THREAD_IDS.contains(threadKey) || CONTEXT_THREAD_LOCAL.get() == null) {
            Deque context = new ArrayDeque<>();
            CONTEXT_THREAD_LOCAL.set(Pair.of(threadKey, context));
            THREAD_IDS.add(threadKey);
            return context;
        }
        return CONTEXT_THREAD_LOCAL.get().getValue();
    }

    @Nullable
    private static Deque getContext() {
        Long threadKey = Thread.currentThread().getId();
        return ofNullable(CONTEXT_THREAD_LOCAL.get()).filter(ctx -> threadKey.equals(ctx.getKey())).map(Pair::getValue).orElse(null);
    }

    /**
     * Return current logging context attached to the current thread. Return parent thread context if self context not found. And return
     * 'null' if nothing was found at all.
     *
     * @return current or parent logging context or 'null'
     */
    @Nullable
    public static LoggingContext context() {
        return ofNullable(CONTEXT_THREAD_LOCAL.get()).map(Pair::getValue).map(Deque::peek).orElse(null);
    }

    /**
     * Initializes new logging context and attaches it to current thread
     *
     * @param launchUuid        a UUID of a Launch
     * @param itemUuid          a Test Item UUID
     * @param client            Client of ReportPortal
     * @param scheduler         a {@link Scheduler} to use with this LoggingContext
     * @param parameters        Report Portal client configuration parameters
     * @param loggingSubscriber RxJava subscriber on logging results
     * @return New Logging Context
     */
    @Nonnull
    public static LoggingContext init(@Nonnull final Maybe launchUuid, @Nullable final Maybe itemUuid,
                                      @Nonnull final ReportPortalClient client, @Nonnull final Scheduler scheduler, @Nonnull final ListenerParameters parameters,
                                      @Nonnull final FlowableSubscriber loggingSubscriber) {
        LoggingContext context = new LoggingContext(launchUuid, itemUuid, client, scheduler, parameters, loggingSubscriber);
        createContext().push(context);
        return context;
    }

    /**
     * Initializes new logging context and attaches it to current thread
     *
     * @param launchUuid a UUID of a Launch
     * @param itemUuid   a Test Item UUID
     * @param client     Client of ReportPortal
     * @param scheduler  a {@link Scheduler} to use with this LoggingContext
     * @param parameters Report Portal client configuration parameters
     * @return New Logging Context
     */
    @Nonnull
    public static LoggingContext init(@Nonnull final Maybe launchUuid, @Nullable final Maybe itemUuid,
                                      @Nonnull final ReportPortalClient client, @Nonnull final Scheduler scheduler, @Nonnull final ListenerParameters parameters) {
        return init(launchUuid, itemUuid, client, scheduler, parameters, new LoggingSubscriber());
    }

    /**
     * Initializes new logging context and attaches it to current thread
     *
     * @param launchUuid    a UUID of a Launch
     * @param itemUuid      a Test Item UUID
     * @param client        Client of ReportPortal
     * @param scheduler     a {@link Scheduler} to use with this LoggingContext
     * @param batchLogsSize Size of a log batch
     * @param convertImages Whether Image should be converted to BlackAndWhite
     * @return New Logging Context
     */
    @Nonnull
    public static LoggingContext init(@Nonnull final Maybe launchUuid, @Nullable final Maybe itemUuid,
                                      @Nonnull final ReportPortalClient client, @Nonnull final Scheduler scheduler, final int batchLogsSize,
                                      final boolean convertImages) {
        ListenerParameters params = new ListenerParameters();
        params.setBatchLogsSize(batchLogsSize);
        params.setConvertImage(convertImages);
        return init(launchUuid, itemUuid, client, scheduler, params);
    }

    /**
     * Initializes new logging context and attaches it to current thread
     *
     * @param launchUuid a UUID of a Launch
     * @param itemUuid   a Test Item UUID
     * @param client     Client of ReportPortal
     * @param scheduler  a {@link Scheduler} to use with this LoggingContext
     * @return New Logging Context
     */
    @Nonnull
    public static LoggingContext init(@Nonnull final Maybe launchUuid, @Nullable final Maybe itemUuid,
                                      @Nonnull final ReportPortalClient client, @Nonnull final Scheduler scheduler) {
        return init(launchUuid, itemUuid, client, scheduler, DEFAULT_LOG_BATCH_SIZE, false);
    }

    /**
     * Completes context attached to the current thread
     *
     * @return Waiting queue to be able to track request sending completion
     */
    @Nonnull
    public static Completable complete() {
        final LoggingContext loggingContext = ofNullable(getContext()).map(Deque::poll).orElse(null);
        if (null != loggingContext) {
            return loggingContext.completed();
        } else {
            return Maybe.empty().ignoreElement();
        }
    }

    /* Log emitter */
    private final PublishSubject> emitter;

    /* a UUID of Launch in ReportPortal */
    private final Maybe launchUuid;
    /* a UUID of TestItem in ReportPortal to report into */
    private final Maybe itemUuid;
    /* Whether Image should be converted to BlackAndWhite */
    private final boolean convertImages;

    LoggingContext(@Nonnull final Maybe launchUuid, @Nullable final Maybe itemUuid,
                   @Nonnull final ReportPortalClient client, @Nonnull final Scheduler scheduler, @Nonnull final ListenerParameters parameters,
                   @Nonnull final FlowableSubscriber loggingSubscriber) {
        this.launchUuid = launchUuid;
        this.itemUuid = itemUuid;
        this.emitter = PublishSubject.create();
        this.convertImages = parameters.isConvertImage();

        RxJavaPlugins.onAssembly(new LogBatchingFlowable(
                        new FlowableFromObservable<>(emitter).flatMap((Function, Publisher>) Maybe::toFlowable),
                        parameters
                ))
                .flatMap((Function, Flowable>) rqs -> client.log(HttpRequestUtils.buildLogMultiPartRequest(
                        rqs)).toFlowable().retry(new RetryWithDelay((e) -> true, DEFAULT_RETRY_COUNT,
                        TimeUnit.SECONDS.toMillis(DEFAULT_RETRY_TIMEOUT))))
                .observeOn(scheduler)
                .onBackpressureBuffer(parameters.getRxBufferSize(), false, true)
                .subscribe(loggingSubscriber);
    }

    private SaveLogRQ prepareRequest(@Nonnull final String launchId, @Nullable final String itemId,
                                     @Nonnull final java.util.function.Function logSupplier) throws IOException {
        final SaveLogRQ rq = logSupplier.apply(itemId);
        rq.setLaunchUuid(launchId);
        SaveLogRQ.File file = rq.getFile();
        if (convertImages && null != file && isImage(file.getContentType())) {
            final TypeAwareByteSource source = convert(ByteSource.wrap(file.getContent()));
            file.setContent(source.read());
            file.setContentType(source.getMediaType());
        }
        return rq;
    }

    /**
     * Emits log. Basically, put it into processing pipeline
     *
     * @param logSupplier Log Message Factory. Key if the function is actual test item ID
     */
    public void emit(@Nonnull final java.util.function.Function logSupplier) {
        emitter.onNext(launchUuid.zipWith(itemUuid, (launchId, itemId) -> prepareRequest(launchId, itemId, logSupplier)));
    }

    /**
     * Emits log. Basically, put it into processing pipeline
     *
     * @param logItemUuid Test Item ID promise
     * @param logSupplier Log Message Factory. Key if the function is actual test item ID
     */
    public void emit(@Nonnull final Maybe logItemUuid, @Nonnull final java.util.function.Function logSupplier) {
        emitter.onNext(launchUuid.zipWith(logItemUuid, (launchId, itemId) -> prepareRequest(launchId, itemId, logSupplier)));
    }

    /**
     * Marks flow as completed
     *
     * @return {@link Completable}
     */
    @Nonnull
    public Completable completed() {
        emitter.onComplete();
        return emitter.ignoreElements();
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy