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;
028import org.apache.commons.logging.LogFactory;
029import org.nuxeo.ecm.automation.OperationCallback;
030import org.nuxeo.ecm.automation.OperationChain;
031import org.nuxeo.ecm.automation.OperationContext;
032import org.nuxeo.ecm.automation.OperationException;
033import org.nuxeo.ecm.automation.OperationType;
034import org.nuxeo.ecm.automation.core.impl.InvokableMethod;
035import org.nuxeo.runtime.api.Framework;
036
037import com.google.common.cache.Cache;
038import com.google.common.cache.CacheBuilder;
039
040/**
041 * @since 5.7.3 The Automation tracer factory service.
042 */
043public class TracerFactory implements TracerFactoryMBean {
044
045    public static final String AUTOMATION_TRACE_PROPERTY = "org.nuxeo.automation.trace";
046
047    public static final String AUTOMATION_TRACE_PRINTABLE_PROPERTY = "org.nuxeo.automation.trace.printable";
048
049    protected static final Integer CACHE_CONCURRENCY_LEVEL = 10;
050
051    protected static final Integer CACHE_MAXIMUM_SIZE = 1000;
052
053    protected static final Integer CACHE_TIMEOUT = 10;
054
055    protected String printable;
056
057    protected Function<String,Boolean> printableAssertor;
058
059    protected Cache<String, ChainTraces> tracesCache;
060
061    protected boolean recording;
062
063    protected Trace lastError;
064
065    public TracerFactory() {
066        tracesCache = CacheBuilder.newBuilder().concurrencyLevel(CACHE_CONCURRENCY_LEVEL)
067                .maximumSize(CACHE_MAXIMUM_SIZE).expireAfterWrite(CACHE_TIMEOUT, TimeUnit.MINUTES).build();
068        recording = Framework.isBooleanPropertyTrue(AUTOMATION_TRACE_PROPERTY);
069        setPrintableTraces(Framework.getProperty(AUTOMATION_TRACE_PRINTABLE_PROPERTY, "*"));
070    }
071
072    protected static class ChainTraces {
073
074        protected OperationType chain;
075
076        protected Map<Integer, Trace> traces = new HashMap<Integer, Trace>();
077
078        protected ChainTraces(OperationType chain) {
079            this.chain = chain;
080        }
081
082        protected String add(Trace trace) {
083            int index = Integer.valueOf(traces.size());
084            traces.put(Integer.valueOf(index), trace);
085            return formatKey(trace.chain, index);
086        }
087
088        protected Trace getTrace(int index) {
089            return traces.get(index);
090        }
091
092        protected void removeTrace(int index) {
093            traces.remove(index);
094        }
095
096        protected void clear() {
097            traces.clear();
098        }
099
100        protected int size() {
101            return traces.size();
102        }
103
104    }
105
106    /**
107     * If trace mode is enabled, instantiate {@link Tracer}. If not, instantiate {@link TracerLite}.
108     */
109    public OperationCallback newTracer() {
110        return new Tracer(this);
111    }
112
113    public Call newCall(OperationType chain, OperationContext context, OperationType type, InvokableMethod method,
114            Map<String, Object> params) {
115        if (!recording) {
116            return new Call(chain, type);
117        }
118        return new Call(chain, context, type, method, params);
119    }
120
121    public Trace newTrace(Call parent, OperationType typeof, List<Call> calls, Object output, OperationException error) {
122        return new Trace(parent, typeof, calls, calls.get(0).details.input, output, error);
123    }
124
125    protected void recordTrace(Trace trace) {
126        String chainId = trace.chain.getId();
127        ChainTraces chainTraces = tracesCache.getIfPresent(chainId);
128        if (chainTraces == null) {
129            tracesCache.put(chainId, new ChainTraces(trace.chain));
130        }
131        if (trace.getError() != null) {
132            lastError = trace;
133        }
134        chainTraces = tracesCache.getIfPresent(chainId);
135        if (chainTraces.size() != 0) {
136            chainTraces.removeTrace(1);
137        }
138        tracesCache.getIfPresent(chainId).add(trace);
139    }
140
141    public Trace getTrace(OperationChain chain, int index) {
142        return tracesCache.getIfPresent(chain.getId()).getTrace(index);
143    }
144
145    /**
146     * @param key
147     *            The name of the chain.
148     * @return The last trace of the given chain.
149     */
150    public Trace getTrace(String key) {
151        return getTrace(key, -1);
152    }
153
154    public Trace getTrace(String key, int index) {
155        ChainTraces chainTrace = tracesCache.getIfPresent(key);
156        if (chainTrace == null) {
157            return null;
158        }
159        if (index < 0) {
160            index = chainTrace.traces.size() - 1;
161        }
162        return tracesCache.getIfPresent(key).getTrace(index);
163    }
164
165    public Trace getLastErrorTrace() {
166        return lastError;
167    }
168
169    public void clearTrace(OperationChain chain, int index) {
170        tracesCache.getIfPresent(chain).removeTrace(Integer.valueOf(index));
171    }
172
173    public void clearTrace(OperationChain chain) {
174        tracesCache.invalidate(chain);
175    }
176
177    @Override
178    public void clearTraces() {
179        tracesCache.invalidateAll();
180    }
181
182    protected static String formatKey(OperationType chain, int index) {
183        return String.format("%s:%s", chain.getId(), index);
184    }
185
186    public void onTrace(Trace trace) {
187        if (trace.error != null) {
188            trace.error.addSuppressed(new Throwable(print(trace)));
189        }
190        if (!recording) {
191            return;
192        }
193        if (printableAssertor.apply(trace.chain.getId())) {
194            LogFactory.getLog(Trace.class).debug(print(trace));
195        }
196        recordTrace(trace);
197    }
198
199    @Override
200    public boolean toggleRecording() {
201        return recording = !recording;
202    }
203
204    @Override
205    public boolean getRecordingState() {
206        return recording;
207    }
208
209    @Override
210    public String getPrintableTraces() {
211        return printable;
212    }
213
214    @Override
215    public String setPrintableTraces(String option) {
216        if ("*".equals(option)) {
217            printableAssertor = s -> Boolean.TRUE;
218        } else {
219            List<String> patterns = Arrays.asList(option.split(","));
220            printableAssertor = s -> {
221                return Boolean.valueOf(patterns.contains(s));
222            };
223        }
224        printable = option;
225        return printable;
226    }
227
228    public String print(Trace trace)  {
229        return TracePrinter.print(trace, !recording);
230    }
231}