-
Notifications
You must be signed in to change notification settings - Fork 2.2k
CustomCheckStyleRules: Good Logging Practice #3359
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
162ffdc
7dbcf10
609940a
5f6f36e
408a372
c342be4
d6704ee
fcdc012
cf277b4
b5c0225
e2531ff
ebdcb13
77225db
32793cd
36390d8
66c99db
95a00bd
8479eea
6b1c866
694370c
c2fa02f
1187938
55bbbf9
caa685e
239f621
9781106
2fb3108
06cf258
95e4545
87d45ed
adcb2a9
871e182
6c449c3
f5c4090
b43780f
10af7b6
bd73a70
8adfc66
e7a3dc5
98a3227
044c19c
9937e0d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,181 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT License. | ||
|
|
||
| package com.azure.tools.checkstyle.checks; | ||
|
|
||
| import com.puppycrawl.tools.checkstyle.api.AbstractCheck; | ||
| import com.puppycrawl.tools.checkstyle.api.DetailAST; | ||
| import com.puppycrawl.tools.checkstyle.api.FullIdent; | ||
| import com.puppycrawl.tools.checkstyle.api.TokenTypes; | ||
| import com.puppycrawl.tools.checkstyle.utils.TokenUtil; | ||
|
|
||
| import java.util.ArrayDeque; | ||
| import java.util.Arrays; | ||
| import java.util.Collections; | ||
| import java.util.Deque; | ||
| import java.util.HashSet; | ||
| import java.util.Set; | ||
|
|
||
| /** | ||
| * Good Logging Practice: | ||
| * <ol> | ||
| * <li>A non-static instance logger.</li> | ||
| * <li>ClientLogger in public API should all named 'logger', public API classes are those classes that are declared | ||
| * as public and that do not exist in an implementation package or subpackage.</li> | ||
| * <li>Should not use any external logger class, only use ClientLogger. No slf4j, log4j, or other logging imports are allowed.</li> | ||
| * <li>'System.out' and 'System.err' is not allowed as well.</li> | ||
| * </ol> | ||
| */ | ||
| public class GoodLoggingCheck extends AbstractCheck { | ||
| private static final String CLIENT_LOGGER_PATH = "com.azure.core.util.logging.ClientLogger"; | ||
| private static final String CLIENT_LOGGER = "ClientLogger"; | ||
| private static final String LOGGER = "logger"; | ||
|
|
||
| private static final String LOGGER_NAME_ERROR = "ClientLogger instance naming: use ''%s'' instead of ''%s'' for consistency."; | ||
| private static final String STATIC_LOGGER_ERROR = "ClientLogger should not be static. Remove static modifier."; | ||
| private static final String NOT_CLIENT_LOGGER_ERROR = "Do not use %s class. Use ''%s'' as a logging mechanism instead of ''%s''."; | ||
|
|
||
| // Boolean indicator that indicates if the java class imports ClientLogger | ||
| private boolean hasClientLoggerImported; | ||
| // A container stores the class names, pop top element if exist the class name AST node | ||
| private Deque<String> classNameDeque = new ArrayDeque<>(); | ||
| // Collection of Invalid logging packages | ||
| private static final Set<String> INVALID_LOGS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( | ||
| "org.slf4j", "org.apache.logging.log4j", "java.util.logging" | ||
| ))); | ||
|
|
||
| @Override | ||
| public int[] getDefaultTokens() { | ||
| return getRequiredTokens(); | ||
| } | ||
|
|
||
| @Override | ||
| public int[] getAcceptableTokens() { | ||
| return getRequiredTokens(); | ||
| } | ||
|
|
||
| @Override | ||
| public int[] getRequiredTokens() { | ||
| return new int[] { | ||
| TokenTypes.IMPORT, | ||
| TokenTypes.CLASS_DEF, | ||
| TokenTypes.LITERAL_NEW, | ||
| TokenTypes.VARIABLE_DEF, | ||
| TokenTypes.METHOD_CALL | ||
| }; | ||
| } | ||
|
|
||
| @Override | ||
| public void finishTree(DetailAST ast) { | ||
| hasClientLoggerImported = false; | ||
| } | ||
|
|
||
| @Override | ||
| public void leaveToken(DetailAST ast) { | ||
| if (ast.getType() == TokenTypes.CLASS_DEF) { | ||
| classNameDeque.pollFirst(); | ||
conniey marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public void visitToken(DetailAST ast) { | ||
| switch (ast.getType()) { | ||
| case TokenTypes.IMPORT: | ||
| final String importClassPath = FullIdent.createFullIdentBelow(ast).getText(); | ||
| hasClientLoggerImported = hasClientLoggerImported || importClassPath.equals(CLIENT_LOGGER_PATH); | ||
|
|
||
| INVALID_LOGS.forEach(item -> { | ||
| if (importClassPath.startsWith(item)) { | ||
| log(ast, String.format(NOT_CLIENT_LOGGER_ERROR, "external logger", CLIENT_LOGGER_PATH, item)); | ||
| } | ||
| }); | ||
| break; | ||
| case TokenTypes.CLASS_DEF: | ||
| classNameDeque.addFirst(ast.findFirstToken(TokenTypes.IDENT).getText()); | ||
conniey marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| break; | ||
| case TokenTypes.LITERAL_NEW: | ||
| checkLoggerInstantiation(ast); | ||
| break; | ||
| case TokenTypes.VARIABLE_DEF: | ||
| checkLoggerNameMatch(ast); | ||
| break; | ||
| case TokenTypes.METHOD_CALL: | ||
| final DetailAST dotToken = ast.findFirstToken(TokenTypes.DOT); | ||
| if (dotToken == null) { | ||
| return; | ||
| } | ||
| final String methodCallName = FullIdent.createFullIdentBelow(dotToken).getText(); | ||
| if (methodCallName.startsWith("System.out") || methodCallName.startsWith("System.err")) { | ||
| log(ast, String.format(NOT_CLIENT_LOGGER_ERROR, "Java System", CLIENT_LOGGER_PATH, methodCallName)); | ||
| } | ||
| break; | ||
| default: | ||
| // Checkstyle complains if there's no default block in switch | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Check if the VARIABLE_DEF AST node type is 'ClientLogger'. | ||
| * | ||
| * @param varDefAST VARIABLE_DEF AST node | ||
| * @return true if the variable type is 'ClientLogger'. | ||
| */ | ||
| private boolean isTypeClientLogger(DetailAST varDefAST) { | ||
| final DetailAST typeAST = varDefAST.findFirstToken(TokenTypes.TYPE); | ||
| if (typeAST == null) { | ||
| return false; | ||
| } | ||
| return TokenUtil.findFirstTokenByPredicate(typeAST, node -> | ||
| node.getType() == TokenTypes.IDENT && node.getText().equals(CLIENT_LOGGER) | ||
| ).isPresent(); | ||
| } | ||
|
|
||
| /** | ||
| * Check if instantiating a matched class name for the same class. | ||
| * | ||
| * @param literalNewToken LITERAL_NEW node | ||
| */ | ||
| private void checkLoggerInstantiation(DetailAST literalNewToken) { | ||
| final DetailAST identToken = literalNewToken.findFirstToken(TokenTypes.IDENT); | ||
| // Not ClientLogger instance | ||
| if (identToken == null || !identToken.getText().equals(CLIENT_LOGGER)) { | ||
| return; | ||
| } | ||
| // LITERAL_NEW node always has ELIST node below | ||
| TokenUtil.findFirstTokenByPredicate(literalNewToken.findFirstToken(TokenTypes.ELIST), exprToken -> { | ||
| // Skip check if not EXPR node or if has no DOT node below. EXPR always has children below | ||
| if (exprToken.getType() != TokenTypes.EXPR || exprToken.getFirstChild().getType() != TokenTypes.DOT) { | ||
| return false; | ||
| } | ||
| // Check instantiation of ClientLogger | ||
| final String containerClassName = FullIdent.createFullIdent(exprToken.getFirstChild()).getText(); | ||
| // Add suffix of '.class' at the end of class name | ||
| final String className = classNameDeque.peekFirst(); | ||
| if (!containerClassName.equals(className + ".class")) { | ||
| log(exprToken, String.format("Not newing a ClientLogger with matching class name. Use ''%s.class'' instead of ''%s''", className, containerClassName)); | ||
conniey marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| return true; | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Check if the given ClientLogger named 'logger' | ||
| * | ||
| * @param varToken VARIABLE_DEF node | ||
| */ | ||
| private void checkLoggerNameMatch(DetailAST varToken) { | ||
| if (!hasClientLoggerImported || !isTypeClientLogger(varToken)) { | ||
| return; | ||
| } | ||
| // Check if the Logger instance named as 'logger'. | ||
| final DetailAST identAST = varToken.findFirstToken(TokenTypes.IDENT); | ||
| if (identAST != null && !identAST.getText().equals(LOGGER)) { | ||
| log(varToken, String.format(LOGGER_NAME_ERROR, LOGGER, identAST.getText())); | ||
| } | ||
| // Check if the Logger is static instance, log as error if it is static instance logger. | ||
| if (TokenUtil.findFirstTokenByPredicate(varToken, node -> node.getType() == TokenTypes.MODIFIERS && node.branchContains(TokenTypes.LITERAL_STATIC)).isPresent()) { | ||
| log(varToken, STATIC_LOGGER_ERROR); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -98,15 +98,15 @@ | |
| <suppress checks="com.azure.tools.checkstyle.checks.EnforceFinalFieldsCheck" files=".*\BlobOutputStream.java"/> | ||
|
|
||
| <!-- Don't apply custom Checkstyle rules to files under test package --> | ||
| <suppress checks="com\.azure\.tools\.checkstyle\.checks\.(ExternalDependencyExposedCheck|NoImplInPublicAPI|ServiceClientInstantiationCheck|ServiceClientBuilderCheck|ServiceInterfaceCheck|HttpPipelinePolicyCheck|JavaDocFormatting|JavadocThrowsChecks|EnforceFinalFieldsCheck|ThrowFromClientLoggerCheck)" | ||
| <suppress checks="com\.azure\.tools\.checkstyle\.checks\.(ExternalDependencyExposedCheck|NoImplInPublicAPI|ServiceClientInstantiationCheck|ServiceClientBuilderCheck|ServiceInterfaceCheck|HttpPipelinePolicyCheck|JavaDocFormatting|JavadocThrowsChecks|EnforceFinalFieldsCheck|ThrowFromClientLoggerCheck|GoodLoggingCheck)" | ||
| files=".*[/\\]src[/\\]test[/\\]java[/\\].*\.java"/> | ||
|
|
||
| <!-- Don't apply custom Checkstyle rules to files under implementation package --> | ||
| <suppress checks="com\.azure\.tools\.checkstyle\.checks\.(ExternalDependencyExposedCheck|NoImplInPublicAPI|ServiceClientInstantiationCheck|ServiceClientBuilderCheck|ServiceInterfaceCheck|JavaDocFormatting|JavadocThrowsChecks|EnforceFinalFieldsCheck|ThrowFromClientLoggerCheck)" | ||
| files=".*[/\\]implementation[/\\].*\.java"/> | ||
|
|
||
| <!-- Don't apply custom Checkstyle rules to files under samples package --> | ||
| <suppress checks="com\.azure\.tools\.checkstyle\.checks\.(ExternalDependencyExposedCheck|HttpPipelinePolicyCheck|EnforceFinalFieldsCheck|ThrowFromClientLoggerCheck)" | ||
| <suppress checks="com\.azure\.tools\.checkstyle\.checks\.(ExternalDependencyExposedCheck|HttpPipelinePolicyCheck|EnforceFinalFieldsCheck|ThrowFromClientLoggerCheck|GoodLoggingCheck)" | ||
| files=".*[/\\]samples[/\\].*\.java"/> | ||
|
|
||
| <!-- Don't check for JavaDocPackage in samples or tests --> | ||
|
|
@@ -139,4 +139,11 @@ | |
| <!-- CodeSnippet Suppression for now, which need code owner's attention --> | ||
| <suppress checks="com.azure.tools.checkstyle.checks.JavadocCodeSnippetCheck" files="com.azure.data.appconfiguration.ConfigurationAsyncClient.java"/> | ||
| <suppress checks="com.azure.tools.checkstyle.checks.JavadocCodeSnippetCheck" files="com.azure.messaging.eventhubs.EventData.java"/> | ||
|
|
||
| <!-- ClientLogger class suppression --> | ||
| <suppress checks="com.azure.tools.checkstyle.checks.GoodLoggingCheck" files="ClientLogger.java"/> | ||
| <!-- Azure Core Test --> | ||
| <suppress checks="com.azure.tools.checkstyle.checks.GoodLoggingCheck" files=".*[/\\]core[/\\]test[/\\].*\.java"/> | ||
mssfang marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| <!-- Public API already released --> | ||
| <suppress checks="com.azure.tools.checkstyle.checks.GoodLoggingCheck" files="CertificateUtil.java"/> | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm curious why we added this suppression? It is in an implementation package, and the logger itself is private. 🤔
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In order to use logger in static method, the logger has to be static. Not a problem of private, the methods of the classes are public static methods. Can't change these method.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, the API is public, but we could remove the logger and it wouldn't break this API. So I was wondering about the "Public API already release" reason... since it doesn't match up. |
||
| </suppressions> | ||
Uh oh!
There was an error while loading. Please reload this page.