001/*
002 * (C) Copyright 2013 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     vpasquier <vpasquier@nuxeo.com>
018 *     slacoin <slacoin@nuxeo.com>
019 */
020package org.nuxeo.ecm.automation.core.trace;
021
022import java.util.Arrays;
023import java.util.HashMap;
024import java.util.List;
025import java.util.Map;
026import java.util.concurrent.TimeUnit;
027import java.util.function.Function;
028
029import org.apache.commons.logging.Log;
030import org.apache.commons.logging.LogFactory;
031import org.nuxeo.ecm.automation.OperationCallback;
032import org.nuxeo.ecm.automation.OperationChain;
033import org.nuxeo.ecm.automation.OperationContext;
034import org.nuxeo.ecm.automation.OperationException;
035import org.nuxeo.ecm.automation.OperationType;
036import org.nuxeo.ecm.automation.core.impl.InvokableMethod;
037import org.nuxeo.runtime.api.Framework;
038
039import com.google.common.cache.Cache;
040import com.google.common.cache.CacheBuilder;
041
042/**
043 * @since 5.7.3 The Automation tracer factory service.
044 */
045public class TracerFactory implements TracerFactoryMBean {
046
047    public static final String AUTOMATION_TRACE_PROPERTY = "org.nuxeo.automation.trace";
048
049    public static final String AUTOMATION_TRACE_PRINTABLE_PROPERTY = "org.nuxeo.automation.trace.printable";
050
051    protected static final Integer CACHE_CONCURRENCY_LEVEL = 10;
052
053    protected static final Integer CACHE_MAXIMUM_SIZE = 1000;
054
055    protected static final Integer CACHE_TIMEOUT = 10;
056
057    protected String printable;
058
059    protected Function<String, Boolean> printableAssertor;
060
061    protected Cache<String, ChainTraces> tracesCache;
062
063    protected boolean recording;
064
065    protected Trace lastError;
066
067    public TracerFactory() {
068        tracesCache = CacheBuilder.newBuilder()
069                                  .concurrencyLevel(CACHE_CONCURRENCY_LEVEL)
070                                  .maximumSize(CACHE_MAXIMUM_SIZE)
071                                  .expireAfterWrite(CACHE_TIMEOUT, TimeUnit.MINUTES)
072                                  .build();
073        recording = Framework.isBooleanPropertyTrue(AUTOMATION_TRACE_PROPERTY);
074        setPrintableTraces(Framework.getProperty(AUTOMATION_TRACE_PRINTABLE_PROPERTY, "*"));
075    }
076
077    protected static class ChainTraces {
078
079        protected OperationType chain;
080
081        protected Map<Integer, Trace> traces = new HashMap<>();
082
083        protected ChainTraces(OperationType chain) {
084            this.chain = chain;
085        }
086
087        protected String add(Trace trace) {
088            int index = traces.size();
089            traces.put(Integer.valueOf(index), trace);
090            return formatKey(trace.chain, index);
091        }
092
093        protected Trace getTrace(int index) {
094            return traces.get(index);
095        }
096
097        protected void removeTrace(int index) {
098            traces.remove(index);
099        }
100
101        protected void clear() {
102            traces.clear();
103        }
104
105        protected int size() {
106            return traces.size();
107        }
108
109    }
110
111    public OperationCallback newTracer() {
112        return new Tracer(this);
113    }
114
115    /**
116     * If trace mode is enabled, instantiate {@link Call}. If not, instantiate {@link LiteCall}.
117     */
118    public Call newCall(OperationType chain, OperationContext context, OperationType type, InvokableMethod method,
119            Map<String, Object> params) {
120        if (!recording) {
121            return new LiteCall(chain, type);
122        }
123        return new Call(chain, context, type, method, params);
124    }
125
126    public Trace newTrace(Call parent, OperationType typeof, List<Call> calls, Object output,
127            OperationException error) {
128        return new Trace(parent, typeof, calls, calls.get(0).details.input, output, error);
129    }
130
131    protected void recordTrace(Trace trace) {
132        String chainId = trace.chain.getId();
133        ChainTraces chainTraces = tracesCache.getIfPresent(chainId);
134        if (chainTraces == null) {
135            tracesCache.put(chainId, new ChainTraces(trace.chain));
136        }
137        if (trace.getError() != null) {
138            lastError = trace;
139        }
140        chainTraces = tracesCache.getIfPresent(chainId);
141        if (chainTraces.size() != 0) {
142            chainTraces.removeTrace(1);
143        }
144        tracesCache.getIfPresent(chainId).add(trace);
145    }
146
147    public Trace getTrace(OperationChain chain, int index) {
148        return tracesCache.getIfPresent(chain.getId()).getTrace(index);
149    }
150
151    /**
152     * @param key The name of the chain.
153     * @return The last trace of the given chain.
154     */
155    public Trace getTrace(String key) {
156        return getTrace(key, -1);
157    }
158
159    public Trace getTrace(String key, int index) {
160        ChainTraces chainTrace = tracesCache.getIfPresent(key);
161        if (chainTrace == null) {
162            return null;
163        }
164        if (index < 0) {
165            index = chainTrace.traces.size() - 1;
166        }
167        return tracesCache.getIfPresent(key).getTrace(index);
168    }
169
170    public Trace getLastErrorTrace() {
171        return lastError;
172    }
173
174    public void clearTrace(OperationChain chain, int index) {
175        tracesCache.getIfPresent(chain).removeTrace(index);
176    }
177
178    public void clearTrace(OperationChain chain) {
179        tracesCache.invalidate(chain);
180    }
181
182    @Override
183    public void clearTraces() {
184        tracesCache.invalidateAll();
185    }
186
187    protected static String formatKey(OperationType chain, int index) {
188        return String.format("%s:%s", chain.getId(), index);
189    }
190
191    public void onTrace(Trace trace) {
192        boolean containsError = trace.error != null;
193        if (!(recording || containsError)) {
194            return;
195        }
196
197        if (containsError) {
198            trace.error.addSuppressed(new Throwable(print(trace)));
199        }
200        if (printableAssertor.apply(trace.chain.getId())) {
201            Log log = LogFactory.getLog(Trace.class);
202            if (containsError) {
203                log.warn(print(trace));
204            } else {
205                log.info(print(trace));
206            }
207        }
208        recordTrace(trace);
209    }
210
211    @Override
212    public boolean toggleRecording() {
213        return recording = !recording;
214    }
215
216    @Override
217    public boolean getRecordingState() {
218        return recording;
219    }
220
221    @Override
222    public String getPrintableTraces() {
223        return printable;
224    }
225
226    @Override
227    public String setPrintableTraces(String option) {
228        if ("*".equals(option)) {
229            printableAssertor = s -> Boolean.TRUE;
230        } else {
231            List<String> patterns = Arrays.asList(option.split(","));
232            printableAssertor = s -> {
233                return Boolean.valueOf(patterns.contains(s));
234            };
235        }
236        printable = option;
237        return printable;
238    }
239
240    public String print(Trace trace) {
241        return trace.print(!recording);
242    }
243}