View Javadoc
1   /*
2    * Licensed to The Apereo Foundation under one or more contributor license
3    * agreements. See the NOTICE file distributed with this work for additional
4    * information regarding copyright ownership.
5    *
6    *
7    * The Apereo Foundation licenses this file to you under the Educational
8    * Community License, Version 2.0 (the "License"); you may not use this file
9    * except in compliance with the License. You may obtain a copy of the License
10   * at:
11   *
12   *   http://opensource.org/licenses/ecl2.txt
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
17   * License for the specific language governing permissions and limitations under
18   * the License.
19   *
20   */
21  
22  package org.opencastproject.serviceregistry.api;
23  
24  import static org.opencastproject.util.data.Tuple.tuple;
25  
26  import org.opencastproject.job.api.Incident;
27  import org.opencastproject.job.api.Incident.Severity;
28  import org.opencastproject.job.api.IncidentTree;
29  import org.opencastproject.job.api.Job;
30  import org.opencastproject.util.NotFoundException;
31  import org.opencastproject.util.data.Tuple;
32  
33  import org.apache.commons.lang3.exception.ExceptionUtils;
34  import org.slf4j.Logger;
35  import org.slf4j.LoggerFactory;
36  
37  import java.util.ArrayList;
38  import java.util.Arrays;
39  import java.util.Collections;
40  import java.util.Date;
41  import java.util.List;
42  import java.util.Map;
43  
44  /** Create and record job incidents. Facade for {@link IncidentService}. */
45  public final class Incidents {
46  
47    private static final Logger log = LoggerFactory.getLogger(Incident.class);
48  
49    /**
50     * System error codes
51     */
52    private static final String SYSTEM_UNHANDLED_EXCEPTION = "org.opencastproject.system.unhandled-exception";
53    private static final String SYSTEM_JOB_CREATION_EXCEPTION = "org.opencastproject.system.job-creation-exception";
54  
55    public static final Map<String, String> NO_PARAMS = Collections.emptyMap();
56    public static final List<Tuple<String, String>> NO_DETAILS = Collections.emptyList();
57  
58    private final IncidentService is;
59    private final ServiceRegistry sr;
60  
61    public Incidents(ServiceRegistry sr, IncidentService is) {
62      this.is = is;
63      this.sr = sr;
64    }
65  
66    /**
67     * Record an incident for a given job. This method is intended to record client incidents, i.e. incidents crafted by
68     * the programmer.
69     *
70     * @param code
71     *          A code number. This incident factory method enforces an incident code schema of <code>job_type.code</code>
72     *          , e.g. <code>org.opencastproject.service.1511</code> . So instead of aligning
73     *          <code>job.getJobType()</code> and the incident's code prefix manually this is done automatically for you
74     *          by this method. See {@link org.opencastproject.job.api.Incident#getCode()}.
75     * @see org.opencastproject.job.api.Incident
76     */
77    public void record(Job job, Severity severity, int code, Map<String, String> params,
78            List<Tuple<String, String>> details) {
79      try {
80        is.storeIncident(job, new Date(), job.getJobType() + "." + code, severity, params, details);
81      } catch (IncidentServiceException e) {
82        logException(e);
83      }
84    }
85  
86    /**
87     * Record an incident for a given job. This method is intended to record client incidents, i.e. incidents crafted by
88     * the programmer.
89     *
90     * @see #record(org.opencastproject.job.api.Job, org.opencastproject.job.api.Incident.Severity, int, java.util.Map,
91     *      java.util.List)
92     * @see org.opencastproject.job.api.Incident
93     */
94    public void record(Job job, Severity severity, int code) {
95      record(job, severity, code, NO_PARAMS, NO_DETAILS);
96    }
97  
98    /**
99     * Record a failure incident for a given job.
100    *
101    * @see #record(org.opencastproject.job.api.Job, org.opencastproject.job.api.Incident.Severity, int, java.util.Map,
102    *      java.util.List)
103    * @see org.opencastproject.job.api.Incident
104    */
105   public void recordFailure(Job job, int code, Map<String, String> params) {
106     record(job, Severity.FAILURE, code, params, NO_DETAILS);
107   }
108 
109   /**
110    * Record a failure incident for a given job.
111    *
112    * @see #record(org.opencastproject.job.api.Job, org.opencastproject.job.api.Incident.Severity, int, java.util.Map,
113    *      java.util.List)
114    * @see org.opencastproject.job.api.Incident
115    */
116   public void recordFailure(Job job, int code, Throwable t, Map<String, String> params,
117           List<Tuple<String, String>> details) {
118     List<Tuple<String, String>> detailList = new ArrayList<>(details);
119     detailList.add(tuple("stack-trace", ExceptionUtils.getStackTrace(t)));
120     record(job, Severity.FAILURE, code, params, detailList);
121   }
122 
123   public void recordJobCreationIncident(Job job, Throwable t) {
124     unhandledException(job, SYSTEM_JOB_CREATION_EXCEPTION, Severity.FAILURE, t);
125   }
126 
127   /**
128    * Record an incident for a given job caused by an uncatched exception. This method is intended to record incidents by
129    * the job system itself, e.g. the job dispatcher.
130    */
131   public void unhandledException(Job job, Severity severity, Throwable t) {
132     unhandledException(job, SYSTEM_UNHANDLED_EXCEPTION, severity, t);
133   }
134 
135   /**
136    * Record an incident for a given job caused by an uncatched exception. This method is intended to record incidents by
137    * the job system itself, e.g. the job dispatcher. Please note that an incident will <em>only</em> be recorded if none
138    * of severity {@link org.opencastproject.job.api.Incident.Severity#FAILURE} has already been recorded by the job or
139    * one of its child jobs. If no job with the given job id exists nothing happens.
140    */
141   public void unhandledException(long jobId, Severity severity, Throwable t) {
142     try {
143       unhandledException(sr.getJob(jobId), severity, t);
144     } catch (NotFoundException ignore) {
145     } catch (ServiceRegistryException e) {
146       logException(e);
147     }
148   }
149 
150   /**
151    * Record an incident for a given job caused by an uncatched exception. This method is intended to record incidents by
152    * the job system itself, e.g. the job dispatcher.
153    */
154   private void unhandledException(Job job, String code, Severity severity, Throwable t) {
155     if (!alreadyRecordedFailureIncident(job.getId())) {
156       try {
157         is.storeIncident(
158                 job,
159                 new Date(),
160                 code,
161                 severity,
162                 Collections.singletonMap("exception", ExceptionUtils.getMessage(t)),
163                 Arrays.asList(tuple("job-type", job.getJobType()), tuple("job-operation", job.getOperation()),
164                         tuple("stack-trace", ExceptionUtils.getStackTrace(t))));
165       } catch (IncidentServiceException e) {
166         logException(e);
167       }
168     }
169   }
170 
171   private void logException(Throwable t) {
172     log.error("Error recording job incident. Log exception and move on.", t);
173   }
174 
175   public boolean alreadyRecordedFailureIncident(long jobId) {
176     try {
177       return findFailure(is.getIncidentsOfJob(jobId, true));
178     } catch (Exception e) {
179       return false;
180     }
181   }
182 
183   static boolean findFailure(IncidentTree tree) {
184     return tree.getIncidents().stream().anyMatch(i -> i.getSeverity() == Severity.FAILURE)
185         || tree.getDescendants().stream().anyMatch(Incidents::findFailure);
186   }
187 }