diff --git a/dspace-api/src/main/java/org/dspace/app/statistics/LogAnalyser.java b/dspace-api/src/main/java/org/dspace/app/statistics/LogAnalyser.java index fdfa6dd526..b8d62e694b 100644 --- a/dspace-api/src/main/java/org/dspace/app/statistics/LogAnalyser.java +++ b/dspace-api/src/main/java/org/dspace/app/statistics/LogAnalyser.java @@ -71,6 +71,9 @@ public class LogAnalyser /** warning counter */ private static int warnCount = 0; + + /** exception counter */ + private static int excCount = 0; /** log line counter */ private static int lineCount = 0; @@ -289,7 +292,7 @@ public class LogAnalyser * @param myEndDate the desired end of the analysis. Goes to the end otherwise * @param myLookUp force a lookup of the database */ - public static void processLogs(Context context, String myLogDir, + public static String processLogs(Context context, String myLogDir, String myFileTemplate, String myConfigFile, String myOutFile, Date myStartDate, Date myEndDate, boolean myLookUp) @@ -428,7 +431,12 @@ public class LogAnalyser // aggregator warnCount++; } - + // count the exceptions + if (logLine.isLevel("ERROR")) + { + excCount++; + } + // is the action a search? if (logLine.isAction("search")) { @@ -513,9 +521,7 @@ public class LogAnalyser } // finally, write the output - createOutput(); - - return; + return createOutput(); } @@ -575,7 +581,7 @@ public class LogAnalyser /** * generate the analyser's output to the specified out file */ - public static void createOutput() + public static String createOutput() { // start a string buffer to hold the final output StringBuffer summary = new StringBuffer(); @@ -588,6 +594,7 @@ public class LogAnalyser // output the number of warnings encountered summary.append("warnings=" + Integer.toString(warnCount) + "\n"); + summary.append("exceptions=" + Integer.toString(excCount) + "\n"); // set the general summary config up in the aggregator file for (int i = 0; i < generalSummary.size(); i++) @@ -725,7 +732,7 @@ public class LogAnalyser System.exit(0); } - return; + return summary.toString(); } diff --git a/dspace-api/src/main/java/org/dspace/health/Check.java b/dspace-api/src/main/java/org/dspace/health/Check.java new file mode 100644 index 0000000000..fb61fc2cb9 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/health/Check.java @@ -0,0 +1,52 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + * + * by lindat-dev team + */ +package org.dspace.health; + +import org.apache.log4j.Logger; + +/** + * Abstract check interface. + */ + +public abstract class Check { + + protected static Logger log = Logger.getLogger(Check.class); + long took_ = -1L; + String report_ = null; + private String errors_ = ""; + + // this method should be overridden + protected abstract String run( ReportInfo ri ); + + public void report( ReportInfo ri ) { + took_ = System.currentTimeMillis(); + try { + String run_report = run(ri); + report_ = errors_ + run_report; + }finally { + took_ = System.currentTimeMillis() - took_; + } + } + + protected void error( Throwable e ) { + error(e, null); + } + protected void error( Throwable e, String msg ) { + errors_ += "====\nException occurred!\n"; + if ( null != e ) { + errors_ += e.toString() + "\n"; + log.error("Exception during healthcheck:", e); + } + if ( null != msg ) { + errors_ += "Reason: " + msg + "\n"; + } + } + +} diff --git a/dspace-api/src/main/java/org/dspace/health/ChecksumCheck.java b/dspace-api/src/main/java/org/dspace/health/ChecksumCheck.java new file mode 100644 index 0000000000..f231c817b2 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/health/ChecksumCheck.java @@ -0,0 +1,80 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + * + * by lindat-dev team + */ +package org.dspace.health; + +import org.dspace.checker.*; +import org.dspace.core.Context; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; + +public class ChecksumCheck extends Check { + + @Override + public String run( ReportInfo ri ) { + String ret = "No md5 checks made!"; + CheckerCommand checker = new CheckerCommand(); + Date process_start = Calendar.getInstance().getTime(); + checker.setProcessStartDate(process_start); + checker.setDispatcher( + // new LimitedCountDispatcher(new SimpleDispatcher(new + // BitstreamInfoDAO(), null, false), 1) + // loop through all files + new SimpleDispatcher(new BitstreamInfoDAO(), process_start, false)); + + md5_collector collector = new md5_collector(); + checker.setCollector(collector); + checker.setReportVerbose(true); + Context context = null; + try { + context = new Context(); + checker.process(context); + } catch (SQLException e) { + error(e); + } finally { + if (context != null) { + context.abort(); + } + } + + if (collector.arr.size() > 0) { + ret = String.format("Checksum performed on [%d] items:\n", + collector.arr.size()); + int ok_items = 0; + for (BitstreamInfo bi : collector.arr) { + if (!ChecksumCheckResults.CHECKSUM_MATCH.equals(bi + .getChecksumCheckResult())) { + ret += String + .format("md5 checksum FAILED (%s): %s id: %s bitstream-id: %s\n was: %s\n is: %s\n", + bi.getChecksumCheckResult(), bi.getName(), + bi.getInternalId(), bi.getBitstreamId(), + bi.getStoredChecksum(), + bi.getCalculatedChecksum()); + } else { + ok_items++; + } + } + + ret += String.format("checksum OK for [%d] items\n", ok_items); + } + return ret; + } +} + +class md5_collector implements ChecksumResultsCollector { + public List arr = new ArrayList<>(); + + public void collect(BitstreamInfo info) { + arr.add(info); + } +} diff --git a/dspace-api/src/main/java/org/dspace/health/Core.java b/dspace-api/src/main/java/org/dspace/health/Core.java new file mode 100644 index 0000000000..f67afb7adf --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/health/Core.java @@ -0,0 +1,267 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + * + * by lindat-dev team + */ +package org.dspace.health; + +import org.apache.commons.io.FileUtils; +import org.dspace.content.Community; +import org.dspace.content.Item; +import org.dspace.content.ItemIterator; +import org.dspace.content.Metadatum; +import org.dspace.core.Context; +import org.dspace.storage.rdbms.DatabaseManager; +import org.dspace.storage.rdbms.TableRow; +import org.dspace.storage.rdbms.TableRowIterator; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class Core { + + // get info + // + public static String getCollectionSizesInfo() throws SQLException { + String ret = ""; + List rows = sql( + "SELECT " + + "(SELECT text_value FROM metadatavalue " + + "WHERE metadata_field_id=64 AND resource_type_id=3 AND resource_id=col.collection_id) AS name, " + + "SUM(bit.size_bytes) AS sum " + + "FROM collection2item col, item2bundle item, bundle2bitstream bun, bitstream bit " + + "WHERE col.item_id=item.item_id AND item.bundle_id=bun.bundle_id AND bun.bitstream_id=bit.bitstream_id " + + "GROUP BY col.collection_id;"); + long total_size = 0; + for (TableRow row : rows) { + double size = row.getLongColumn("sum") / (1024. * 1024.); + total_size += size; + ret += String.format( + "\t%s: %s\n", row.getStringColumn("name"), FileUtils.byteCountToDisplaySize((long)size)); + } + ret += String.format( + "Total size: %s\n", FileUtils.byteCountToDisplaySize(total_size)); + + ret += String.format( + "Resource without policy: %d\n", getBitstreamsWithoutPolicyCount()); + + ret += String.format( + "Deleted bitstreams: %d\n", getBitstreamsDeletedCount()); + + rows = getBitstreamOrphansRows(); + String list_str = ""; + for (TableRow row : rows) { + list_str += String.format("%d, ", row.getIntColumn("bitstream_id")); + } + ret += String.format( + "Orphan bitstreams: %d [%s]\n", rows.size(), list_str); + + return ret; + } + + public static String getObjectSizesInfo() throws SQLException { + String ret = ""; + Context c = new Context(); + + for (String tb : new String[] { "bitstream", "bundle", "collection", + "community", "dcvalue", "eperson", "item", "handle", + "epersongroup", "workflowitem", "workspaceitem", }) { + TableRowIterator irows = DatabaseManager.query(c, + "SELECT COUNT(*) from " + tb); + List rows = irows.toList(); + ret += String.format("Count %s: %s\n", tb, + String.valueOf(rows.get(0).getLongColumn("count"))); + } + + c.complete(); + return ret; + } + + + // get objects + // + + public static List getWorkspaceItemsRows() throws SQLException { + return sql("SELECT stage_reached, count(1) AS cnt FROM workspaceitem GROUP BY stage_reached ORDER BY stage_reached;"); + } + + public static List getBitstreamOrphansRows() throws SQLException { + return sql("SELECT bitstream_id FROM bitstream WHERE deleted<>true AND bitstream_id " + + "NOT IN (" + + "SELECT bitstream_id FROM bundle2bitstream " + + "UNION SELECT logo_bitstream_id FROM community WHERE logo_bitstream_id IS NOT NULL " + + "UNION SELECT primary_bitstream_id FROM bundle WHERE primary_bitstream_id IS NOT NULL ORDER BY bitstream_id " + + ")"); + + } + + public static List getSubscribersRows() throws SQLException { + return sql("SELECT DISTINCT ON (eperson_id) eperson_id FROM subscription"); + } + + public static List getSubscribedCollectionsRows() throws SQLException { + return sql("SELECT DISTINCT ON (collection_id) collection_id FROM subscription"); + } + + public static List getHandlesInvalidRows() throws SQLException { + List rows = sql("SELECT * FROM handle " + + " WHERE NOT (" + + " (handle IS NOT NULL AND resource_type_id IS NOT NULL AND resource_id IS NOT NULL)" + + " OR " + " (handle IS NOT NULL AND url IS NOT NULL)" + + " ) "); + return rows; + } + + // get sizes + // + + public static int getItemsTotalCount() throws SQLException { + int total = 0; + for (java.util.Map.Entry name_count : getCommunities()) { + total += name_count.getValue(); + } + return total; + } + + public static int getWorkflowItemsCount() throws SQLException { + return sql("SELECT * FROM workflowitem;").size(); + } + + public static int getNotArchivedItemsCount() throws SQLException { + return sql( + "SELECT * FROM item WHERE in_archive=false AND withdrawn=false").size(); + } + + public static int getWithdrawnItemsCount() throws SQLException { + return sql("SELECT * FROM item WHERE withdrawn=true").size(); + } + + public static int getBitstreamsWithoutPolicyCount() throws SQLException { + return sql( + "SELECT bitstream_id FROM bitstream WHERE deleted<>true AND bitstream_id NOT IN " + + "(SELECT resource_id FROM resourcepolicy WHERE resource_type_id=0)") + .size(); + } + + public static int getBitstreamsDeletedCount() throws SQLException { + return sql("SELECT * FROM bitstream WHERE deleted=true").size(); + } + + public static long getHandlesTotalCount() throws SQLException { + List rows = sql("SELECT count(1) AS cnt FROM handle"); + return rows.get(0).getLongColumn("cnt"); + } + + + // get more complex information + // + + public static List> getCommunities() + throws SQLException { + + List> cl = new java.util.ArrayList<>(); + Context context = new Context(); + Community[] top_communities = Community.findAllTop(context); + for (Community c : top_communities) { + cl.add( + new java.util.AbstractMap.SimpleEntry<>(c.getName(), c.countItems()) + ); + } + context.complete(); + return cl; + } + + public static List getEmptyGroups() throws SQLException { + List ret = new ArrayList<>(); + Context c = new Context(); + TableRowIterator irows = DatabaseManager + .query(c, + "SELECT eperson_group_id, " + + "(SELECT text_value FROM metadatavalue " + + "WHERE metadata_field_id=64 AND resource_type_id=6 AND resource_id=eperson_group_id) AS name " + + "FROM epersongroup " + + "WHERE eperson_group_id NOT IN (SELECT eperson_group_id FROM epersongroup2eperson)"); + for (TableRow row : irows.toList()) { + ret.add( row.getStringColumn("name") ); + } + c.complete(); + return ret; + } + + public static List getSubscribers() throws SQLException { + List ret = new ArrayList<>(); + for (TableRow row : getSubscribersRows()) { + ret.add(row.getIntColumn("eperson_id")); + } + return ret; + } + + public static List getSubscribedCollections() throws SQLException { + List ret = new ArrayList<>(); + for (TableRow row : getSubscribedCollectionsRows()) { + ret.add(row.getIntColumn("collection_id")); + } + return ret; + } + + @SuppressWarnings("deprecation") + public static Map getItemRightsInfo() { + Map ret = new HashMap<>(); + Map info = new HashMap<>(); + try { + Context context = new Context(); + ItemIterator it = Item.findAll(context); + while (it.hasNext()) { + Item i = it.next(); + Metadatum[] labels = i.getMetadata("dc", "rights", "label", + Item.ANY); + String pub_dc_value = ""; + + if (labels.length > 0) { + for (Metadatum dc : labels) { + if (pub_dc_value.length() == 0) { + pub_dc_value = dc.value; + } else { + pub_dc_value = pub_dc_value + " " + dc.value; + } + } + } else { + pub_dc_value = "no licence"; + } + + if (!info.containsKey(pub_dc_value)) { + info.put(pub_dc_value, 0); + } + info.put(pub_dc_value, info.get(pub_dc_value) + 1); + } + context.complete(); + + for (Map.Entry e : info.entrySet()) { + ret.put(e.getKey(), String.valueOf(e.getValue())); + } + + } catch (SQLException e) { + e.printStackTrace(); + } + return ret; + } + + // + // + + static List sql(String sql) throws SQLException { + Context c = new Context(); + List ret = DatabaseManager.query(c, sql).toList(); + c.complete(); + return ret; + } + +} + diff --git a/dspace-api/src/main/java/org/dspace/health/EmbargoCheck.java b/dspace-api/src/main/java/org/dspace/health/EmbargoCheck.java new file mode 100644 index 0000000000..23e839ebf8 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/health/EmbargoCheck.java @@ -0,0 +1,64 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + * + * by lindat-dev team + */ +package org.dspace.health; + + +import org.dspace.content.DCDate; +import org.dspace.content.Item; +import org.dspace.core.Context; +import org.dspace.embargo.factory.EmbargoServiceFactory; +import org.dspace.embargo.service.EmbargoService; + +import java.sql.SQLException; +import java.util.Iterator; + +public class EmbargoCheck extends Check { + + private static final EmbargoService embargoService = EmbargoServiceFactory.getInstance().getEmbargoService(); + + @Override + public String run( ReportInfo ri ) { + String ret = ""; + Context context = null; + try { + context = new Context(); + Iterator item_iter = null; + try { + item_iter = embargoService.findItemsByLiftMetadata(context); + } catch (IllegalArgumentException e) { + error(e, "No embargoed items found"); + } catch (Exception e) { + error(e); + } + + while (item_iter != null && item_iter.hasNext()) { + Item item = item_iter.next(); + String handle = item.getHandle(); + DCDate date = null; + try { + date = embargoService.getEmbargoTermsAsDate(context, item); + } catch (Exception e) { + } + ret += String.format("%s embargoed till [%s]\n", handle, + date != null ? date.toString() : "null"); + } + context.complete(); + } catch (SQLException e) { + try { + if ( null != context ) { + context.abort(); + } + } catch (Exception e1) { + } + } + + return ret; + } +} diff --git a/dspace-api/src/main/java/org/dspace/health/InfoCheck.java b/dspace-api/src/main/java/org/dspace/health/InfoCheck.java new file mode 100644 index 0000000000..23e1efa9e9 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/health/InfoCheck.java @@ -0,0 +1,69 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + * + * by lindat-dev team + */ +package org.dspace.health; + +import java.text.SimpleDateFormat; +import org.apache.commons.io.FileUtils; +import org.dspace.core.ConfigurationManager; + +import java.io.File; +import java.util.Date; + +public class InfoCheck extends Check { + + @Override + public String run( ReportInfo ri ) { + StringBuilder sb = new StringBuilder(); + + sb.append("Generated: ").append( + new Date().toString() + ).append("\n"); + + sb.append("From - Till: ").append( + new SimpleDateFormat("MM/dd/yyyy").format(ri.from().getTime()) + ).append(" - ").append( + new SimpleDateFormat("MM/dd/yyyy").format(ri.till().getTime()) + ).append("\n"); + + sb.append("Url: ").append( + ConfigurationManager.getProperty("dspace.url") + ).append("\n"); + sb.append("\n"); + + for (String[] ss : new String[][] { + new String[] { + ConfigurationManager.getProperty("assetstore.dir"), + "Assetstore size: ", }, + new String[] { + ConfigurationManager.getProperty("search.dir"), + "Search dir size: ", }, + new String[] { + ConfigurationManager.getProperty("log.dir"), + "Log dir size: ", }, }) + { + try { + File dir = new File(ss[0]); + if (dir.exists()) { + long dir_size = FileUtils.sizeOfDirectory(dir); + sb.append(String.format("%s: %s\n", ss[1], + FileUtils.byteCountToDisplaySize(dir_size)) + ); + } else { + sb.append(String.format("Directory [%s] does not exist!\n", ss[0])); + } + }catch(Exception e) { + error(e, "directory - " + ss[0]); + } + } + + return sb.toString(); + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/health/ItemCheck.java b/dspace-api/src/main/java/org/dspace/health/ItemCheck.java new file mode 100644 index 0000000000..6c06c93717 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/health/ItemCheck.java @@ -0,0 +1,71 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + * + * by lindat-dev team + */ +package org.dspace.health; + + +import org.dspace.storage.rdbms.TableRow; + +import java.sql.SQLException; +import java.util.Map; + +public class ItemCheck extends Check { + + @Override + public String run( ReportInfo ri ) { + String ret = ""; + int tot_cnt = 0; + try { + for (Map.Entry name_count : Core.getCommunities()) { + ret += String.format("Collection [%s]: %d\n", + name_count.getKey(), name_count.getValue()); + tot_cnt += name_count.getValue(); + } + } catch (SQLException e) { + error(e); + } + + try { + ret += "\nCollection sizes:\n"; + ret += Core.getCollectionSizesInfo(); + } catch (SQLException e) { + error(e); + } + + ret += String.format( + "\nPublished items (archived, not withdrawn): %d\n", tot_cnt); + try { + ret += String.format( + "Withdrawn items: %d\n", Core.getWithdrawnItemsCount()); + ret += String.format( + "Not published items (in workspace or workflow mode): %d\n", + Core.getNotArchivedItemsCount()); + + for (TableRow row : Core.getWorkspaceItemsRows()) { + ret += String.format("\tIn Stage %s: %s\n", + row.getIntColumn("stage_reached"), + row.getLongColumn("cnt")); + } + + ret += String.format( + "\tWaiting for approval (workflow items): %d\n", + Core.getWorkflowItemsCount()); + + } catch (SQLException e) { + error(e); + } + + try { + ret += Core.getObjectSizesInfo(); + } catch (SQLException e) { + error(e); + } + return ret; + } +} diff --git a/dspace-api/src/main/java/org/dspace/health/LogAnalyserCheck.java b/dspace-api/src/main/java/org/dspace/health/LogAnalyserCheck.java new file mode 100644 index 0000000000..bc80cc7242 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/health/LogAnalyserCheck.java @@ -0,0 +1,70 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + * + * by lindat-dev team + */ +package org.dspace.health; + + +import org.dspace.app.statistics.LogAnalyser; +import org.dspace.core.Context; + +import java.text.SimpleDateFormat; +import java.util.HashMap; +import java.util.Map; + +public class LogAnalyserCheck extends Check { + + final static private String[][] interesting_fields = new String[][] { + new String[] { "exceptions", "Exceptions" }, + new String[] { "warnings", "Warnings" }, + new String[] { "action.browse", "Archive browsed" }, + new String[] { "action.search", "Archive searched" }, + new String[] { "action.login", "Logged in" }, + new String[] { "action.oai_request", "OAI requests" }, + }; + + @Override + public String run( ReportInfo ri ) { + StringBuilder sb = new StringBuilder(); + + Map info_map = new HashMap<>(); + for (String[] info : interesting_fields) { + info_map.put(info[0], "unknown"); + } + + try { + Context c = new Context(); + // parse logs + String report = LogAnalyser.processLogs( + c, null, null, null, null, ri.from(), ri.till(), false); + + // we have to deal with string report... + for (String line : report.split("\\r?\\n")) { + String[] parts = line.split("="); + if (parts.length == 2) { + info_map.put(parts[0], parts[1]); + } + } + + // create report + for (String[] info : interesting_fields ) { + sb.append( String.format("%-17s: %s\n", info[1], info_map.get(info[0])) ); + } + sb.append( String.format("Items added since [%s] (db): %s\n", + new SimpleDateFormat("MM/dd/yyyy").format(ri.from().getTime()), + LogAnalyser.getNumItems(c))); + + c.complete(); + + } catch (Exception e) { + error(e); + } + + return sb.toString(); + } +} diff --git a/dspace-api/src/main/java/org/dspace/health/Report.java b/dspace-api/src/main/java/org/dspace/health/Report.java new file mode 100644 index 0000000000..37f2cffb48 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/health/Report.java @@ -0,0 +1,211 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + * + * by lindat-dev team + */ +package org.dspace.health; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; +import org.apache.commons.cli.PosixParser; +import org.apache.commons.lang.exception.ExceptionUtils; +import org.apache.log4j.Logger; +import org.dspace.core.ConfigurationManager; +import org.dspace.core.Email; +import org.dspace.core.PluginManager; + +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.Map.Entry; + +public class Report { + + private static Logger log = Logger.getLogger(Report.class); + public static final String EMAIL_PATH = "config/emails/healthcheck"; + // store the individual check reports + private StringBuilder summary_; + + // ctor + // + public Report() { + summary_ = new StringBuilder(); + } + + // run checks + // + public void run(List to_perform, ReportInfo ri) { + + int pos = -1; + for (Entry check_entry : checks().entrySet()) { + ++pos; + if ( null != to_perform && !to_perform.contains(pos) ) { + continue; + } + String check_name = check_entry.getKey(); + Check check = check_entry.getValue(); + + log.info(String.format("#%d. Processing [%s] at [%s]", + pos, check_name, new SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss.SSS").format(new Date()))); + + try { + // do the stuff + check.report(ri); + store(check_name, check.took_, check.report_); + + }catch( Exception e ) { + store( + check_name, + -1, + "Exception occurred when processing report - " + ExceptionUtils.getStackTrace(e) + ); + } + } + } + + // create check list + public static LinkedHashMap checks() { + LinkedHashMap checks = new LinkedHashMap<>(); + String check_names[] = ConfigurationManager.getProperty("healthcheck", "checks").split(","); + for ( String check_name : check_names ) { + Check check = (Check) PluginManager.getNamedPlugin( + "healthcheck", Check.class, check_name); + if ( null != check ) { + checks.put(check_name, check); + }else { + log.warn( String.format( + "Could not find implementation for [%s]", check_name) ); + } + } + return checks; + } + + public String toString() { + return summary_.toString(); + } + + // + private void store(String name, long took, String report) { + name += String.format(" [took: %ds] [# lines: %d]", + took / 1000, + new StringTokenizer(report, "\r\n").countTokens() + ); + + String one_summary = String.format( + "\n#### %s\n%s\n\n###############################\n", + name, + report.replaceAll("\\s+$", "") + ); + summary_.append(one_summary); + + // output it + System.out.println(one_summary); + + } + + // main + // + + public static void main(String[] args) { + log.info("Starting healthcheck report..."); + + final String option_help = "h"; + final String option_email = "e"; + final String option_check = "c"; + final String option_last_n = "f"; + final String option_verbose = "v"; + + // command line options + Options options = new Options(); + options.addOption(option_help, "help", false, + "Show available checks and their index."); + options.addOption(option_email, "email", true, + "Send report to this email address."); + options.addOption(option_check, "check", true, + "Perform only specific check (use index starting from 0)."); + options.addOption(option_last_n, "for", true, + "For last N days."); + options.addOption(option_verbose, "verbose", false, + "Verbose report."); + + CommandLine cmdline = null; + try { + cmdline = new PosixParser().parse(options, args); + } catch (ParseException e) { + log.fatal("Invalid command line " + e.toString(), e); + System.exit(1); + } + + if ( cmdline.hasOption(option_help) ) { + String checks_summary = ""; + int pos = 0; + for (String check_name: checks().keySet()) { + checks_summary += String.format( "%d. %s\n", pos++, check_name ); + } + System.out.println( "Available checks:\n" + checks_summary ); + return; + } + + // what to perform + List to_perform = null; + if ( null != cmdline.getOptionValues(option_check)) { + to_perform = new ArrayList<>(); + for (String s : cmdline.getOptionValues('c')) { + to_perform.add(Integer.valueOf(s)); + } + } + + try { + + // last n days + int for_last_n_days = ConfigurationManager.getIntProperty( + "healthcheck", "last_n_days"); + if ( cmdline.hasOption(option_last_n) ) { + for_last_n_days = Integer.getInteger( + cmdline.getOptionValue(option_last_n)); + } + ReportInfo ri = new ReportInfo( for_last_n_days ); + if ( cmdline.hasOption(option_verbose) ) { + ri.verbose( true ); + } + + // run report + Report r = new Report(); + r.run(to_perform, ri); + log.info("reports generated..."); + + // send/output the report + if (cmdline.hasOption(option_email)) { + String to = cmdline.getOptionValue(option_email); + if ( !to.contains("@") ) { + to = ConfigurationManager.getProperty(to); + } + try { + String dspace_dir = ConfigurationManager.getProperty("dspace.dir"); + String email_path = dspace_dir.endsWith("/") ? dspace_dir + : dspace_dir + "/"; + email_path += Report.EMAIL_PATH; + log.info(String.format( + "Looking for email template at [%s]", email_path)); + Email email = Email.getEmail(email_path); + email.addRecipient(to); + email.addArgument(r.toString()); + email.send(); + } catch (Exception e) { + log.fatal("Error sending email:", e); + System.exit(1); + } + } + + } catch (Exception e) { + log.fatal(e); + e.printStackTrace(); + } + } + +} diff --git a/dspace-api/src/main/java/org/dspace/health/ReportInfo.java b/dspace-api/src/main/java/org/dspace/health/ReportInfo.java new file mode 100644 index 0000000000..1fb0705d71 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/health/ReportInfo.java @@ -0,0 +1,54 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + * + * by lindat-dev team + */ +package org.dspace.health; + +import java.util.Date; +import java.util.GregorianCalendar; + +import static java.util.Calendar.DAY_OF_MONTH; +import static java.util.Calendar.MONTH; +import static java.util.Calendar.YEAR; + +/** + * Information about a report run accessible by each check. + */ +public class ReportInfo { + + private boolean verbose_; + private GregorianCalendar from_ = null; + private GregorianCalendar till_ = null; + + public ReportInfo(int for_last_n_days) { + GregorianCalendar cal = new GregorianCalendar(); + till_ = new GregorianCalendar( + cal.get(YEAR), cal.get(MONTH), cal.get(DAY_OF_MONTH) + ); + // get info from the last n days + from_ = (GregorianCalendar)till_.clone(); + from_.add(DAY_OF_MONTH, -for_last_n_days); + // filter output + verbose_ = false; + } + + public void verbose( boolean verbose ) { + verbose_ = verbose; + } + public boolean verbose() { + return verbose_; + } + + public Date from() { + return from_.getTime(); + } + + public Date till() { + return till_.getTime(); + } +} diff --git a/dspace-api/src/main/java/org/dspace/health/UserCheck.java b/dspace-api/src/main/java/org/dspace/health/UserCheck.java new file mode 100644 index 0000000000..a5f5ede2d2 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/health/UserCheck.java @@ -0,0 +1,97 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + * + * by lindat-dev team + */ +package org.dspace.health; + + +import org.apache.commons.lang.StringUtils; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; + +import java.sql.SQLException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class UserCheck extends Check { + + @Override + public String run( ReportInfo ri ) { + String ret = ""; + Map info = new HashMap(); + try { + Context context = new Context(); + EPerson[] epersons = EPerson.findAll(context, EPerson.LASTNAME); + info.put("Count", epersons.length); + info.put("Can log in (password)", 0); + info.put("Have email", 0); + info.put("Have 1st name", 0); + info.put("Have 2nd name", 0); + info.put("Have lang", 0); + info.put("Have netid", 0); + info.put("Self registered", 0); + + for (EPerson e : epersons) { + if (e.getEmail() != null && e.getEmail().length() > 0) + info.put("Have email", info.get("Have email") + 1); + if (e.canLogIn()) + info.put("Can log in (password)", + info.get("Can log in (password)") + 1); + if (e.getFirstName() != null && e.getFirstName().length() > 0) + info.put("Have 1st name", info.get("Have 1st name") + 1); + if (e.getLastName() != null && e.getLastName().length() > 0) + info.put("Have 2nd name", info.get("Have 2nd name") + 1); + if (e.getLanguage() != null && e.getLanguage().length() > 0) + info.put("Have lang", info.get("Have lang") + 1); + if (e.getNetid() != null && e.getNetid().length() > 0) + info.put("Have netid", info.get("Have netid") + 1); + if (e.getNetid() != null && e.getNetid().length() > 0) + info.put("Self registered", info.get("Self registered") + 1); + } + context.complete(); + + } catch (SQLException e) { + error(e); + } + + ret += String.format( + "Users: %d\n", info.get("Count")); + ret += String.format( + "Have email: %d\n", info.get("Have email")); + for (Map.Entry e : info.entrySet()) { + if (!e.getKey().equals("Count") && !e.getKey().equals("Have email")) { + ret += String.format("%s: %s\n", e.getKey(), + String.valueOf(e.getValue())); + } + } + + try { + // empty group + List egs = Core.getEmptyGroups(); + ret += String.format( + " Empty groups: #%d\n %s\n", + egs.size(), StringUtils.join(egs, ",\n ")); + + List subs = Core.getSubscribers(); + ret += String.format( + "Subscribers: #%d [%s]\n", + subs.size(), StringUtils.join(subs, ", ")); + + subs = Core.getSubscribedCollections(); + ret += String.format( + "Subscribed cols.: #%d [%s]\n", + subs.size(), StringUtils.join(subs, ", ")); + + } catch (SQLException e) { + error(e); + } + + return ret; + } +} diff --git a/dspace/config/emails/healthcheck b/dspace/config/emails/healthcheck new file mode 100644 index 0000000000..b15a7a7cf9 --- /dev/null +++ b/dspace/config/emails/healthcheck @@ -0,0 +1,7 @@ +Subject: ${dspace.name}: Repository healthcheck +{0} + + +_____________________________________ +${dspace.name}, +WWW: ${dspace.url} \ No newline at end of file diff --git a/dspace/config/launcher.xml b/dspace/config/launcher.xml index 61492ac64f..ff8115d8d0 100644 --- a/dspace/config/launcher.xml +++ b/dspace/config/launcher.xml @@ -7,6 +7,13 @@ org.dspace.storage.bitstore.BitStoreMigrate + + healthcheck + Create health check report + + org.dspace.health.Report + + checker Run the checksum checker diff --git a/dspace/config/modules/healthcheck.cfg b/dspace/config/modules/healthcheck.cfg new file mode 100644 index 0000000000..da8e3cef95 --- /dev/null +++ b/dspace/config/modules/healthcheck.cfg @@ -0,0 +1,20 @@ +### Healthcheck module config + +# names must match plugin.named below +checks = General Information,\ + Checksum,\ + Embargo items,\ + Item summary,\ + User summary,\ + Log Analyser Check + +plugin.named.org.dspace.health.Check = \ + org.dspace.health.InfoCheck = General Information,\ + org.dspace.health.ChecksumCheck = Checksum,\ + org.dspace.health.EmbargoCheck = Embargo items,\ + org.dspace.health.ItemCheck = Item summary,\ + org.dspace.health.UserCheck = User summary,\ + org.dspace.health.LogAnalyserCheck = Log Analyser Check + +# report from the last N days (where dates are applicable) +last_n_days = 7 \ No newline at end of file