/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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. */ import org.apache.tools.ant.BuildException import static java.util.concurrent.TimeUnit.SECONDS // Checks logging calls to keep from using patterns that might be expensive // either in CPU or unnecessary object creation configure(rootProject) { } allprojects { plugins.withType(JavaPlugin) { task validateLogCalls(type: ValidateLogCallsTask) { description "Checks that log calls are either validated or conform to efficient patterns." group "verification" doFirst { if (project.hasProperty('srcDir')) { srcDir.addAll(project.getProperty('srcDir').split(',')) } else { // Remove this later, make it optional //TODO throw new BuildException(String.format(Locale.ENGLISH, '''Until we get all the calls cleaned up, you MUST specify -PsrcDir=relative_path, e.g. "-PsrcDir=solr/core/src/java/org/apache/solr/core". This task will recursively check all "*.java" files under that directory''')) } checkPlus = Boolean.valueOf(propertyOrDefault('checkPlus', 'false')) } } } // // Attach ecjLint to check. // check.dependsOn ecjLint // What does the below mean? // // Each validation task should be attached to check but make sure // // precommit() as a whole is a dependency on rootProject.check // check.dependsOn precommit } //} class ValidateLogCallsTask extends DefaultTask { @Input List srcDir = [] @Input boolean checkPlus // TODO, remove when you go to project-based checking. Set dirsToCheck = [ "solr/core/src/java/org/apache/solr/analysis" , "solr/core/src/java/org/apache/solr/api" , "solr/core/src/java/org/apache/solr/client" , "solr/core/src/java/org/apache/solr/cloud" // 120 , "solr/core/src/java/org/apache/solr/cloud/api" , "solr/core/src/java/org/apache/solr/cloud/autoscaling" , "solr/core/src/java/org/apache/solr/cloud/cdcr" , "solr/core/src/java/org/apache/solr/cloud/hdfs" , "solr/core/src/java/org/apache/solr/cloud/overseer" , "solr/core/src/java/org/apache/solr/cloud/rule" , "solr/core/src/java/org/apache/solr/core" , "solr/core/src/java/org/apache/solr/filestore" , "solr/core/src/java/org/apache/solr/handler/admin" , "solr/core/src/java/org/apache/solr/handler/component" , "solr/core/src/java/org/apache/solr/handler/export" , "solr/core/src/java/org/apache/solr/handler/loader" , "solr/core/src/java/org/apache/solr/handler/tagger" , "solr/core/src/java/org/apache/solr/highlight" , "solr/core/src/java/org/apache/solr/index" , "solr/core/src/java/org/apache/solr/internal" , "solr/core/src/java/org/apache/solr/legacy" , "solr/core/src/java/org/apache/solr/logging" , "solr/core/src/java/org/apache/solr/metrics" , "solr/core/src/java/org/apache/solr/packagemanager" , "solr/core/src/java/org/apache/solr/parser" , "solr/core/src/java/org/apache/solr/pkg" , "solr/core/src/java/org/apache/solr/query" , "solr/core/src/java/org/apache/solr/request" , "solr/core/src/java/org/apache/solr/response" , "solr/core/src/java/org/apache/solr/rest" , "solr/core/src/java/org/apache/solr/schema" , "solr/core/src/java/org/apache/solr/search" , "solr/core/src/java/org/apache/solr/security" , "solr/core/src/java/org/apache/solr/servlet" , "solr/core/src/java/org/apache/solr/spelling" , "solr/core/src/java/org/apache/solr/store" , "solr/core/src/java/org/apache/solr/uninverting" , "solr/core/src/java/org/apache/solr/update" , "solr/core/src/java/org/apache/solr/util" , "solr/solrj" , "solr/core/src/java/org/apache/solr/handler" , "solr/core/src/test/org/apache/solr/cloud/api" , "solr/core/src/test/org/apache/solr/cloud/autoscaling" // , "solr/core/src/test/org/apache/solr/cloud" // , "solr/core/src/test/org/apache/solr/cloud/cdcr" // , "solr/core/src/test/org/apache/solr/handler" // , "solr/core/src/test/org/apache/solr/metrics" // , "solr/core/src/test/org/apache/solr/request" // , "solr/core/src/test/org/apache/solr/response" // , "solr/core/src/test/org/apache/solr/schema" // , "solr/core/src/test/org/apache/solr/search" // , "solr/core/src/test/org/apache/solr/security" // , "solr/core/src/test/org/apache/solr/spelling" // , "solr/core/src/test/org/apache/solr" // , "solr/core/src/test/org/apache/solr/update" // , "solr/core/src/test/org/apache/solr/util" // , "solr/core/src/test" // , "solr/core" ] //TODO REMOVE ME! Really! and the check for bare parens. Several times I've put in () when I meant {} and only // caught it by chance. So for this mass edit, I created a check with a a lot of false positives. This is a list of // them and we won't report them. Map> parenHack = [ "AddReplicaCmd.java" : [99] , "Assign.java" : [329] , "CloudSolrClientTest.java" : [1083] , "CommitTracker.java" : [135] , "DeleteReplicaCmd.java" : [75] , "DirectUpdateHandler2.java" : [838, 859] , "ManagedIndexSchemaFactory.java": [284] , "MoveReplicaCmd.java" : [75] , "PeerSync.java" : [704] , "RecordingJSONParser.java" : [76] , "SliceMutator.java" : [61] , "SolrDispatchFilter.java" : [150, 205, 242] , "Suggester.java" : [147, 181] , "TestSimTriggerIntegration.java" : [710, 713] , "TestSolrJErrorHandling.java" : [289] , "TriggerIntegrationTest.java" : [427, 430] , "UpdateLog.java" : [1976] , "V2HttpCall.java" : [158] // checking against 8x in master, take these out usually. // , "CoreContainer.java" : [1096] // , "ConcurrentLFUCache.java" : [700, 911] // , "ConcurrentLRUCache.java" : [911] // , "DirectUpdateHandler2.java" : [844, 865] // , "PeerSync.java" : [697] // , "SolrIndexWriter.java" : [356] // , "UpdateLog.java" : [1973] ] def logLevels = ["log.trace", "log.debug", "log.info", "log.warn", "log.error", "log.fatal"] def errsFound = 0; def violations = new ArrayList(); def reportViolation(String msg) { violations.add(System.lineSeparator + msg); errsFound++; } // We have a log.something line, check for patterns we're not fond of. def checkLine(File file, String line, int lineNumber, String previous) { boolean violation = false def bareParens = (line =~ /".*?"/).findAll() bareParens.each({ part -> if (part.contains("()")) { List hack = parenHack.get(file.name) if (hack == null || hack.contains(lineNumber) == false) { violation = true } } }) // If the line has been explicitly checked, skip it. if (line.replaceAll("\\s", "").toLowerCase().contains("//logok")) { return } // Strip all of the comments, things in quotes and the like. def level = "" def lev = (line =~ "log\\.(.*?)\\(") if (lev.find()) { level = lev.group(1).toLowerCase().trim() } def stripped = line.replaceFirst("//.*", " ") // remove comment to EOL. Again, fragile due to the possibility of embedded double slashes .replaceFirst(/.*?\(/, " ") // Get rid of "log.info(" .replaceFirst(/\);/, " ") // get rid of the closing ");" .replaceFirst("/\\*.*?\\*/", " ") // replace embedded comments "/*....*/" .replaceAll(/".*?"/, '""') // remove anything between quotes. This is a bit fragile if there are embedded double quotes. .replaceAll(/timeLeft\(.*?\)/, " ") // used all over tests, it's benign .replaceAll(/TimeUnit\..*?\.convert\(.*?\)/, " ") // again, a pattern that's efficient .replaceAll("\\s", "") def m = stripped =~ "\\(.*?\\)" def hasParens = m.find() // The compiler will pre-assemble patterns like 'log.info("string const1 {}" + " string const2 {}", obj1, obj2)' // to log.info("string const1 {} string const2 {}", obj1, obj2)', so don't worry about any plus preceeded and // followed by double quotes. def hasPlus = false for (int idx = 0; idx < stripped.length(); ++idx) { if (stripped.charAt(idx) == '+') { if (idx == 0 || idx == stripped.length() - 1 || stripped.charAt(idx - 1) != '"' || stripped.charAt(idx + 1) != '"') { hasPlus = true break } } } // Check that previous line isn't an if statement for always-reported log levels. Arbitrary decision: we don't // really care about checking for awkward constructions for WARN and above, so report a violation if the previous // line contains an if for those levels. boolean dontReportLevels = level.equals("fatal") || level.equals("error") || level.equals("warn") // There's a convention to declare a member variable for whether a level is enabled and check that rather than // isDebugEnabled so we need to check both. String prevLine = previous.replaceAll("\\s+", "").toLowerCase() boolean prevLineNotIf = ((prevLine.contains("if(log.is" + level + "enabled") == false && prevLine.contains("if(" + level + ")") == false)) if (dontReportLevels) { // Only look (optionally) for plusses if surrounded by an if isLevelEnabled clause // Otherwise, we're always OK with logging the message. if (hasPlus && checkPlus) { violation = true } } else { // less severe than warn, check for parens and plusses and correct if (hasParens && prevLineNotIf) { violation = true } if (hasPlus && checkPlus) { violation = true } if (hasPlus && prevLineNotIf) { violation = true } } // Always report toString(). Note, this over-reports some constructs // but just add //logOK if it's really OK. if (line.contains("toString(") == true) { if (line.replaceAll(/Arrays.toString\(/, "").contains("toString(") && prevLineNotIf) { violation = true } } if (violation) { reportViolation(String.format("Suspicious logging call, Parameterize and possibly surround with 'if (log.is*Enabled) {..}'. Help at: 'gradlew helpValidateLogCalls' %s %s:%d" , System.lineSeparator, file.getAbsolutePath(), lineNumber)) } return } // Require all our logger definitions lower case "log", except a couple of special ones. def checkLogName(File file, String line) { // It's many times faster to do check this way than use a regex if (line.contains("static ") && line.contains("getLogger") && line.contains(" log ") == false) { switch (file.name) { case "LoggerFactory.java": break case "SolrCore.java": // Except for two know log files with a different name. if (line.contains("requestLog") || line.contains("slowLog")) { break } case "StartupLoggingUtils.java": if (line.contains("getLoggerImplStr")) { break; } default: reportViolation("Change the logger name to lower-case 'log' in " + file.name + " " + line) break; } } } def checkFile(File file) { int state = 0 // 0 == not collecting a log line, 1 == collecting a log line, 2 == just collected the last. int lineNumber = 0 StringBuilder sb = new StringBuilder(); // We have to assemble line-by-line to get the full log statement. We require that all loggers (except requestLog // and slowLog) be exactly "log". That will be checked as well. String prevLine = "" file.eachLine { String line -> lineNumber++ checkLogName(file, line) switch (state) { case 0: logLevels.each { if (line.contains(it)) { if (line.contains(");")) { state = 2 } else { state = 1 } sb.setLength(0) sb.append(line) } } break case 1: // collecting if (line.contains(");")) { state = 2 } sb.append(line) break default: reportViolation("Bad state, should be 0-1 in the switch statement") // make people aware the code sucks break } switch (state) { // It's just easier to do this here rather than read another line in the switch above. case 0: prevLine = line.toLowerCase(); break; case 1: break; case 2: checkLine(file, sb.toString(), lineNumber, prevLine) state = 0 break; default: break; } } return } @TaskAction def checkLogLines() { // println srcDir dirsToCheck.addAll(srcDir) //TODO. This is here to check 8x on another branch since I can't run Gradle // over 8x. Used periodically as a sanity check. // new File("/Users/Erick/apache/solrJiras/master").traverse(type: groovy.io.FileType.FILES, nameFilter: ~/.*\.java/) { File it -> // if (dirsToCheck.any { dir -> // it.getCanonicalPath().contains(dir) // }) { // if (checkFile(it)) { // println(it.getAbsolutePath()) // // TODO. This just makes it much easier to get to the files during this mass migration! // } // } // new File("/Users/Erick/apache/SolrEOEFork").traverse(type: groovy.io.FileType.FILES, nameFilter: ~/.*\.java/) { File it -> // if (checkFile(it)) { // println(it.getAbsolutePath()) // } // } // //TODO // println project // This is the real stuff project.sourceSets.each { srcSet -> srcSet.java.each { f -> if (srcDir.contains("all")) { // TODO checkFile(f) } else if (dirsToCheck.any { f.getCanonicalPath().contains(it) }) { checkFile(f) } } } if (errsFound > 0) { throw new BuildException(String.format(Locale.ENGLISH, 'Found %d violations in source files (%s).', errsFound, violations.join(', '))); } } }