development

순환 복잡성을 최소화하는 조건부 로깅

big-blog 2020. 11. 24. 08:15
반응형

순환 복잡성을 최소화하는 조건부 로깅


" 순환 복잡도에 대한 좋은 한계는 무엇입니까? "를 읽은 후 , 많은 동료들이 프로젝트에 대한 이 새로운 QA 정책에 상당히 짜증이났다는 것을 알게되었습니다 . 함수 당 순환 복잡도가 10 개 이상은 아닙니다 .

의미 : 10 개 이하의 'if', 'else', 'try', 'catch'및 기타 코드 워크 플로 분기 문. 권리. ' 비공개 방법을 테스트합니까? ', 그러한 정책에는 많은 좋은 부작용이 있습니다.

그러나 : 우리 (200 명-7 년 길이) 프로젝트를 시작할 때, 우리는 행복하게 로깅했습니다 (아니요, 로그에 대한 일종의 ' Aspect-oriented programming '접근 방식에 쉽게 위임 할 수 없습니다 ).

myLogger.info("A String");
myLogger.fine("A more complicated String");
...

그리고 시스템의 첫 번째 버전이 실행되었을 때 로깅 (한 지점에서 꺼진 상태) 때문이 아니라 항상 계산 된 다음 전달되는 로그 매개 변수 (문자열)로 인해 엄청난 메모리 문제가 발생 했습니다. 'info ()'또는 'fine ()'함수는 로깅 수준이 'OFF'이고 로깅이 발생하지 않았 음을 확인하기 위해서만 사용됩니다!

그래서 QA가 돌아와 프로그래머들에게 조건부 로깅을하도록 촉구했습니다. 항상.

if(myLogger.isLoggable(Level.INFO) { myLogger.info("A String");
if(myLogger.isLoggable(Level.FINE) { myLogger.fine("A more complicated String");
...

그러나 이제는 '이동할 수 없음'이라는 10 가지 순환 복잡도 수준의 함수 제한을 사용하여 함수에 넣는 다양한 로그가 부담으로 느껴진다 고 주장합니다. 왜냐하면 각 "if (isLoggable ())"는 +1 순환 복잡성으로 계산됩니다!

따라서 함수에 8 개의 'if', 'else'등이 있고 하나의 밀접하게 결합 된 쉽게 공유 할 수없는 알고리즘과 3 개의 중요한 로그 작업이 있으면 조건부 로그가 실제로 는 그렇지 않더라도 제한을 위반합니다. 그 기능의 복잡성의 일부 ...

이 상황을 어떻게 해결 하시겠습니까?
내 프로젝트에서 몇 가지 흥미로운 코딩 진화 ( '충돌'로 인해)를 보았지만 먼저 여러분의 생각을 듣고 싶습니다.


모든 답변에 감사드립니다.
나는 문제가 '형식화'와 관련된 것이 아니라 '인수 평가'와 관련이 있다고 주장해야한다 (아무것도하지 않는 메서드를 호출하기 직전에 수행하는 데 매우 비용이 많이들 수있는 평가).
따라서 "A String"위에 썼을 때, 나는 실제로 로거 ... 여부 (따라서 문제 및 표시 할 수있는 문자열을 반환하고, 복잡한 방법 수집에 대한 호출되는 및 로그 데이터의 모든 종류의 계산) aFunction와 aFunction (), (의미 의무 에 조건부 로깅을 사용하므로 '순환 적 복잡성'이 인위적으로 증가하는 실제 문제 ...)

나는 이제 여러분 중 일부가 진보 한 ' 가변 함수'포인트를 얻었습니다 (존에게 감사합니다).
참고 : java6의 빠른 테스트는 내 varargs 함수 가 호출되기 전에 인수를 평가하므로 함수 호출에는 적용 할 수 없지만 '로그 검색기 객체'(또는 '함수 래퍼')에 대해서는 toString ( )는 필요한 경우에만 호출됩니다. 알았다.

이제이 주제에 대한 경험을 게시했습니다.
투표를 위해 다음 주 화요일까지 남겨두고 답변 중 하나를 선택하겠습니다.
다시 한 번 모든 제안에 감사드립니다. :)


Python에서는 형식화 된 값을 로깅 함수에 매개 변수로 전달합니다. 문자열 형식은 로깅이 활성화 된 경우에만 적용됩니다. 여전히 함수 호출의 오버 헤드가 있지만 형식화에 비해 미미합니다.

log.info ("a = %s, b = %s", a, b)

가변 인수 (C / C ++, C # / Java 등)가있는 모든 언어에 대해 이와 같은 작업을 수행 할 수 있습니다.


이것은 인수를 검색하기 어려운 경우를위한 것은 아니지만 문자열로 형식을 지정하는 데 비용이 많이 듭니다. 예를 들어 코드에 이미 숫자 목록이있는 경우 디버깅을 위해 해당 목록을 기록 할 수 있습니다. mylist.toString()결과가 버려 지므로 실행하는 데 시간이 오래 걸리지 않습니다. 따라서 mylist로깅 함수에 매개 변수로 전달 하고 문자열 형식을 처리하도록합니다. 이렇게하면 필요한 경우에만 포맷이 수행됩니다.


OP의 질문은 Java를 구체적으로 언급하므로 위의 사용 방법은 다음과 같습니다.

나는 문제가 '형식화'와 관련된 것이 아니라 '인수 평가'와 관련이 있다고 주장해야한다 (아무것도하지 않을 메서드를 호출하기 직전에 수행하는 데 비용이 많이들 수있는 평가).

비결은 절대적으로 필요할 때까지 값 비싼 계산을 수행하지 않는 객체를 갖는 것입니다. 이것은 람다와 클로저를 지원하는 스몰 토크 나 파이썬과 같은 언어에서는 쉽지만, 약간의 상상력으로 자바에서는 여전히 가능합니다.

기능이 있다고 가정 해 보겠습니다 get_everything(). 데이터베이스의 모든 개체를 목록으로 검색합니다. 분명히 결과가 삭제되면 이것을 호출하고 싶지 않습니다. 따라서 해당 함수에 대한 호출을 직접 사용하는 대신 다음과 같은 내부 클래스를 정의합니다 LazyGetEverything.

public class MainClass {
    private class LazyGetEverything { 
        @Override
        public String toString() { 
            return getEverything().toString(); 
        }
    }

    private Object getEverything() {
        /* returns what you want to .toString() in the inner class */
    }

    public void logEverything() {
        log.info(new LazyGetEverything());
    }
}

이 코드에서 호출 getEverything()은 필요할 때까지 실제로 실행되지 않도록 래핑됩니다. 로깅 기능은 toString()디버깅이 활성화 된 경우에만 해당 매개 변수에서 실행 됩니다. 이렇게하면 코드가 전체 getEverything()호출이 아닌 함수 호출의 오버 헤드 만 겪게됩니다 .


현재 로깅 프레임 워크에서 문제는 의문의 여지가 있습니다.

slf4j 또는 log4j 2와 같은 현재 로깅 프레임 워크는 대부분의 경우 가드 문이 필요하지 않습니다. 매개 변수화 된 로그 문을 사용하여 이벤트를 무조건 기록 할 수 있지만 메시지 형식은 이벤트가 활성화 된 경우에만 발생합니다. 메시지 구성은 애플리케이션이 선제 적으로 수행하는 것이 아니라 로거에서 필요에 따라 수행됩니다.

골동품 로깅 라이브러리를 사용해야하는 경우 더 많은 배경 정보와 매개 변수화 된 메시지로 이전 라이브러리를 개조하는 방법을 읽어 볼 수 있습니다.

가드 문이 정말 복잡해 지나요?

순환 복잡도 계산에서 로깅 가드 문을 제외하는 것을 고려하십시오.

예측 가능한 형식으로 인해 조건부 로깅 검사는 실제로 코드의 복잡성에 기여하지 않는다고 주장 할 수 있습니다.

유연성이없는 메트릭은 좋은 프로그래머를 나쁘게 만들 수 있습니다. 조심해!

복잡성을 계산하는 도구를 그 정도에 맞출 수 없다고 가정하면 다음 접근 방식이 해결 방법을 제공 할 수 있습니다.

조건부 로깅의 필요성

다음과 같은 코드가 있기 때문에 가드 문이 도입되었다고 가정합니다.

private static final Logger log = Logger.getLogger(MyClass.class);

Connection connect(Widget w, Dongle d, Dongle alt) 
  throws ConnectionException
{
  log.debug("Attempting connection of dongle " + d + " to widget " + w);
  Connection c;
  try {
    c = w.connect(d);
  } catch(ConnectionException ex) {
    log.warn("Connection failed; attempting alternate dongle " + d, ex);
    c = w.connect(alt);
  }
  log.debug("Connection succeeded: " + c);
  return c;
}

Java에서 각 로그 문은 새를 만들고 문자열에 연결된 각 개체 StringBuilder에서 toString()메서드를 호출 합니다. 이러한 toString()메서드는 차례로 StringBuilder자신의 인스턴스 를 만들고 toString()잠재적으로 큰 개체 그래프에서 멤버 메서드 등을 호출 할 수 있습니다. (Java 5 이전 StringBuffer에는 사용 되었기 때문에 훨씬 더 비쌌 으며 모든 작업이 동기화되었습니다.)

이는 특히 로그 문이 과도하게 실행되는 코드 경로에있는 경우 상대적으로 비용이 많이들 수 있습니다. 그리고 위와 같이 작성하면 로그 레벨이 너무 높아 로거가 결과를 버리도록 바운드 되어도 비용이 많이 드는 메시지 형식화가 발생합니다.

이로 인해 다음과 같은 형식의 가드 문이 도입됩니다.

  if (log.isDebugEnabled())
    log.debug("Attempting connection of dongle " + d + " to widget " + w);

이 가드를 사용하면 인수 dw문자열 연결 평가가 필요한 경우에만 수행됩니다.

간단하고 효율적인 로깅을위한 솔루션

그러나 로거 (또는 선택한 로깅 패키지에 대해 작성하는 래퍼)가 포맷터 및 포맷터에 대한 인수를 사용하는 경우 메시지 구성은 사용되는 것이 확실 할 때까지 지연 될 수 있으며 보호 문과 해당 순환 복잡성.

public final class FormatLogger
{

  private final Logger log;

  public FormatLogger(Logger log)
  {
    this.log = log;
  }

  public void debug(String formatter, Object... args)
  {
    log(Level.DEBUG, formatter, args);
  }

  … &c. for info, warn; also add overloads to log an exception …

  public void log(Level level, String formatter, Object... args)
  {
    if (log.isEnabled(level)) {
      /* 
       * Only now is the message constructed, and each "arg"
       * evaluated by having its toString() method invoked.
       */
      log.log(level, String.format(formatter, args));
    }
  }

}

class MyClass 
{

  private static final FormatLogger log = 
     new FormatLogger(Logger.getLogger(MyClass.class));

  Connection connect(Widget w, Dongle d, Dongle alt) 
    throws ConnectionException
  {
    log.debug("Attempting connection of dongle %s to widget %s.", d, w);
    Connection c;
    try {
      c = w.connect(d);
    } catch(ConnectionException ex) {
      log.warn("Connection failed; attempting alternate dongle %s.", d);
      c = w.connect(alt);
    }
    log.debug("Connection succeeded: %s", c);
    return c;
  }

}

이제 필요한 경우 아니면 버퍼 할당이있는 계단식 toString()호출이 발생 하지 않습니다! 이것은 가드 진술로 이어진 성능 저하를 효과적으로 제거합니다. Java에서 한 가지 작은 패널티는 로거에 전달하는 모든 기본 유형 인수의 자동 박싱입니다.

어수선한 문자열 연결이 사라졌기 때문에 로깅을 수행하는 코드는 그 어느 때보 다 깨끗합니다. 형식 문자열이 외부화되면 (를 사용하여 ResourceBundle) 더 깔끔해질 수 있으며 , 이는 소프트웨어의 유지 관리 또는 현지화에도 도움이 될 수 있습니다.

추가 향상

또한 Java에서는 MessageFormat"형식"대신 객체를 사용할 수 있으며 String, 이는 기본 번호를보다 깔끔하게 처리 할 수있는 선택 형식과 같은 추가 기능을 제공합니다. 또 다른 대안은 기본 toString()방법이 아닌 "평가"를 위해 정의한 인터페이스를 호출하는 고유 한 형식화 기능을 구현하는 것 입니다.


람다 식 또는 코드 블록을 매개 변수로 지원하는 언어에서 이에 대한 한 가지 해결책은 로깅 메서드에 제공하는 것입니다. 구성을 평가하고 필요한 경우에만 제공된 람다 / 코드 블록을 실제로 호출 / 실행할 수 있습니다. 그래도 아직 시도하지 않았습니다.

이론적으로 이것은 가능합니다. 로깅을 위해 람다 / 코드 블록을 많이 사용하는 것으로 예상되는 성능 문제로 인해 프로덕션에서 사용하고 싶지 않습니다.

그러나 항상 그렇듯이 의심스러운 경우 테스트하고 CPU로드 및 메모리에 미치는 영향을 측정하십시오.


모든 답변에 감사드립니다! 너희들 락 :)

이제 내 의견은 귀하만큼 간단하지 않습니다.

예, 하나의 프로젝트 ( '단일 프로덕션 플랫폼에서 자체적으로 배포되고 실행되는 하나의 프로그램'에서와 같이)에 대해 모든 기술을 나에게 적용 할 수 있다고 가정합니다.

  • 로거 래퍼로 전달할 수있는 전용 '로그 리트리버'개체는 toString () 만 호출해야합니다.
  • 로깅 가변 함수 (또는 일반 Object [] 배열!) 와 함께 사용

@John Millikin과 @erickson의 설명대로 거기에 있습니다.

그러나이 문제로 인해 '왜 정확히 우리가 처음에 로그인 했습니까?'에 대해 조금 생각하게되었습니다.
우리 프로젝트는 비동기식 통신 요구 사항과 중앙 버스 아키텍처가있는 다양한 프로덕션 플랫폼에 배치 된 30 개의 서로 다른 프로젝트 (각각 5-10 명)입니다.
질문에 설명 된 간단한 로깅 은 처음 (5 년 전) 각 프로젝트 대해 괜찮 았지만 그 이후로 한 단계 더 나아가 야합니다. KPI를 입력합니다 .

로거에게 무엇이든 기록하도록 요청하는 대신 자동으로 생성 된 객체 (KPI라고 함)에 이벤트 등록을 요청합니다. 이것은 간단한 호출 (myKPI.I_am_signaling_myself_to_you ())이며 조건부 일 필요가 없습니다 ( '순환 복잡도의 인공적인 증가'문제를 해결 함).

이 KPI 개체는 누가 호출하는지 알고 있으며 응용 프로그램 시작부터 실행되기 때문에 이전에 로깅 할 때 그 자리에서 계산했던 많은 데이터를 검색 할 수 있습니다.
또한 KPI 개체를 독립적으로 모니터링하고 요청시 단일 및 별도의 게시 버스에 정보를 계산 / 게시 할 수 있습니다.
이렇게하면 각 클라이언트가 올바른 로그 파일을 찾고 비밀 문자열을 찾는 대신 자신이 실제로 원하는 정보 (예 : '내 프로세스가 시작 되었습니까?')를 요청할 수 있습니다.

실제로 '왜 정확히 우리가 처음에 로그인 했습니까?' 우리는 프로그래머와 그의 유닛 또는 통합 테스트를 위해서만 로깅하는 것이 아니라 최종 클라이언트 자체를 포함하여 훨씬 더 광범위한 커뮤니티를 위해 로깅한다는 것을 깨달았습니다. 우리의 '보고'메커니즘은 연중 무휴 중앙 집중식 비동기식이어야했습니다.

해당 KPI 메커니즘의 구체적인 내용은이 질문의 범위를 벗어납니다. 적절한 보정은 우리가 직면하고있는 가장 복잡한 비 기능적 문제입니다. 여전히 때때로 시스템이 무릎을 꿇고 있습니다! 그러나 적절히 보정하면 생명의 은인입니다.

다시 한 번 모든 제안에 감사드립니다. 간단한 로깅이 여전히 제자리에있을 때 시스템의 일부 부분에서이를 고려할 것입니다.
그러나이 질문의 다른 요점은 훨씬 더 크고 복잡한 맥락에서 특정 문제를 설명하는 것이 었습니다.
당신이 그것을 좋아하기를 바랍니다. 다음 주 말에 KPI에 대해 질문 할 수 있습니다 (믿거 나 말거나 지금까지 SOF에 대한 질문이 아닙니다!).

다음 주 화요일까지 투표를 위해이 답변을 남겨두고 답변을 선택하겠습니다 (당연히이 답변이 아님;))


너무 간단 할 수도 있지만 가드 절 주위에 "추출 방법"리팩토링을 사용하는 것은 어떻습니까? 이에 대한 예제 코드 :

public void Example()
{
  if(myLogger.isLoggable(Level.INFO))
      myLogger.info("A String");
  if(myLogger.isLoggable(Level.FINE))
      myLogger.fine("A more complicated String");
  // +1 for each test and log message
}

이렇게됩니다 :

public void Example()
{
   _LogInfo();
   _LogFine();
   // +0 for each test and log message
}

private void _LogInfo()
{
   if(!myLogger.isLoggable(Level.INFO))
      return;

   // Do your complex argument calculations/evaluations only when needed.
}

private void _LogFine(){ /* Ditto ... */ }

C 또는 C ++에서는 조건부 로깅을 위해 if 문 대신 전처리기를 사용합니다.


Pass the log level to the logger and let it decide whether or not to write the log statement:

//if(myLogger.isLoggable(Level.INFO) {myLogger.info("A String");
myLogger.info(Level.INFO,"A String");

UPDATE: Ah, I see that you want to conditionally create the log string without a conditional statement. Presumably at runtime rather than compile time.

I'll just say that the way we've solved this is to put the formatting code in the logger class so that the formatting only takes place if the level passes. Very similar to a built-in sprintf. For example:

myLogger.info(Level.INFO,"A String %d",some_number);   

That should meet your criteria.


Conditional logging is evil. It adds unnecessary clutter to your code.

You should always send in the objects you have to the logger:

Logger logger = ...
logger.log(Level.DEBUG,"The foo is {0} and the bar is {1}",new Object[]{foo, bar});

and then have a java.util.logging.Formatter that uses MessageFormat to flatten foo and bar into the string to be output. It will only be called if the logger and handler will log at that level.

For added pleasure you could have some kind of expression language to be able to get fine control over how to format the logged objects (toString may not always be useful).


alt text
(source: scala-lang.org)

Scala has a annontation @elidable() that allows you to remove methods with a compiler flag.

With the scala REPL:

C:>scala

Welcome to Scala version 2.8.0.final (Java HotSpot(TM) 64-Bit Server VM, Java 1. 6.0_16). Type in expressions to have them evaluated. Type :help for more information.

scala> import scala.annotation.elidable import scala.annotation.elidable

scala> import scala.annotation.elidable._ import scala.annotation.elidable._

scala> @elidable(FINE) def logDebug(arg :String) = println(arg)

logDebug: (arg: String)Unit

scala> logDebug("testing")

scala>

With elide-beloset

C:>scala -Xelide-below 0

Welcome to Scala version 2.8.0.final (Java HotSpot(TM) 64-Bit Server VM, Java 1. 6.0_16). Type in expressions to have them evaluated. Type :help for more information.

scala> import scala.annotation.elidable import scala.annotation.elidable

scala> import scala.annotation.elidable._ import scala.annotation.elidable._

scala> @elidable(FINE) def logDebug(arg :String) = println(arg)

logDebug: (arg: String)Unit

scala> logDebug("testing")

testing

scala>

See also Scala assert definition


As much as I hate macros in C/C++, at work we have #defines for the if part, which if false ignores (does not evaluate) the following expressions, but if true returns a stream into which stuff can be piped using the '<<' operator. Like this:

LOGGER(LEVEL_INFO) << "A String";

I assume this would eliminate the extra 'complexity' that your tool sees, and also eliminates any calculating of the string, or any expressions to be logged if the level was not reached.


Here is an elegant solution using ternary expression

logger.info(logger.isInfoEnabled() ? "Log Statement goes here..." : null);


Consider a logging util function ...

void debugUtil(String s, Object… args) {
   if (LOG.isDebugEnabled())
       LOG.debug(s, args);
   }
);

Then make the call with a "closure" round the expensive evaluation that you want to avoid.

debugUtil(“We got a %s”, new Object() {
       @Override String toString() { 
       // only evaluated if the debug statement is executed
           return expensiveCallToGetSomeValue().toString;
       }
    }
);

참고URL : https://stackoverflow.com/questions/105852/conditional-logging-with-minimal-cyclomatic-complexity

반응형