Logowanie warunkowe z minimalną złożonością cyklomatyczną

Po przeczytaniu " Jaki jest twój / dobry limit złożoności cyklomatycznej?", zdaję sobie sprawę, że wielu moich kolegów było dość zirytowanych tą nową Polityką QA w naszym projekcie: nigdy więcej 10 złożoności cyklomatycznej na funkcję.

Znaczenie: nie więcej niż 10 "if", "else", "try", "catch" i inne instrukcje rozgałęziające przepływ pracy kodu. Racja. Jak wyjaśniłem w ' czy testujecie prywatną metodę?', taka polityka ma wiele dobrych skutków ubocznych.

Ale: na początku z naszego projektu (200 osób - 7 lat) z radością logowaliśmy się (i nie, nie możemy łatwo delegować tego do pewnego rodzaju "Aspect-oriented programming " approach for logs).

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

I kiedy pierwsze wersje naszego systemu zostały uruchomione, doświadczyliśmy ogromnego problemu z pamięcią nie z powodu logowania (które w pewnym momencie było wyłączone), ale z powodu parametrów dziennika (ciągi znaków), które są zawsze obliczane, a następnie przekazywane do funkcji ' info () 'lub' fine ()', tylko po to, aby odkryć, że poziom logowania był "wyłączony", i że żadne logowanie nie miało miejsca!

Więc QA wróciła i namówiła naszych programistów do warunkowego logowania. Zawsze.

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

Ale teraz, z tym 'can-not-be-moved' 10 cyclomatic complexity level per function limit, twierdzą oni, że różne dzienniki, które umieszczają w swojej funkcji, są odczuwane jako ciężar, ponieważ każde" if(isLoggable ()) " jest liczone jako +1 cyclomatic complexity!

Więc jeśli funkcja ma 8 'if', 'else' i tak dalej, w jednym ściśle powiązanym, niełatwym do udostępnienia algorytmie oraz 3 krytyczne działania dziennika... naruszają limit, nawet jeśli warunkowe dzienniki mogą nie być naprawdę częścią wspomnianej złożoności tej funkcji...

Jak zajmiesz się tą sytuacją ?
Widziałem kilka ciekawych zmian w kodowaniu (z powodu tego 'konfliktu') w moim projekcie, ale najpierw chcę poznać wasze przemyślenia.


Dziękuję za wszystkie odpowiedzi.
Muszę nalegać, że problem jest nie związane z formatowaniem, ale z oceną argumentów (ewaluacja, która może być bardzo kosztowna, tuż przed wywołaniem metody, która nic nie zrobi)
Więc kiedy napisano powyżej" a String", miałem na myśli aFunction(), z afunction() zwracającym Łańcuch znaków i będącym wywołaniem skomplikowanej metody gromadzącej i obliczającej wszelkiego rodzaju dane dziennika, które mają być wyświetlane przez rejestrator... lub nie (stąd problem, a obowiązek używania logowania warunkowego, stąd faktyczne wydanie sztuczny wzrost "złożoności cyklomatycznej"...)

Dostaję teraz punkt 'variadic function' rozwinięty przez niektórych z was(Dziękuję John).
Uwaga: szybki test w java6 pokazuje, że moja funkcja varargs ocenia swoje argumenty przed wywołaniem, więc nie może być zastosowana do wywołania funkcji, ale do "obiektu Log retriever" (lub "wrappera funkcji"), na którym funkcja toString() będzie wywoływana tylko w razie potrzeby. Rozumiem.

Zamieściłem teraz swoje doświadczenie na tej temat.
Zostawię go tam do przyszłego wtorku na głosowanie, a następnie wybieram jedną z twoich odpowiedzi.
Jeszcze raz dziękuję za wszystkie sugestie:)

Author: VonC, 2008-09-20

12 answers

W Pythonie przekazujesz sformatowane wartości jako parametry do funkcji logowania. Formatowanie łańcuchów jest stosowane tylko wtedy, gdy logowanie jest włączone. Wciąż istnieje narzut wywołania funkcji, ale jest to niewielkie w porównaniu z formatowaniem.

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

Możesz zrobić coś takiego dla dowolnego języka z różnymi argumentami (C/C++, C#/Java, itp.).


Nie jest to tak naprawdę przeznaczone, gdy argumenty są trudne do odzyskania, ale gdy formatowanie ich do łańcuchów jest drogie. Na przykład, jeśli twój kod zawiera już listę liczb, możesz chcieć zarejestrować tę listę w celu debugowania. Wykonanie mylist.toString() nie przyniesie żadnych korzyści, ponieważ wynik zostanie odrzucony. Przekazujesz więc mylist jako parametr do funkcji logowania i pozwalasz jej obsługiwać formatowanie łańcuchów. W ten sposób formatowanie będzie wykonywane tylko w razie potrzeby.


Ponieważ w pytaniu OP konkretnie mowa jest o Javie, oto jak można użyć powyższego:

Muszę nalegać, aby problem nie jest związany z formatowaniem, ale z ewaluacją argumentów (ewaluacja, która może być bardzo kosztowna, tuż przed wywołaniem metody, która nic nie zrobi)

[9]}sztuką jest posiadanie obiektów, które nie będą wykonywać kosztownych obliczeń, dopóki nie będą absolutnie potrzebne. Jest to łatwe w językach takich jak Smalltalk lub Python, które obsługują lambda i zamknięcia, ale nadal jest wykonalne w Javie z odrobiną wyobraźni.

Powiedz, że masz funkcję get_everything(). Pobierze każdy obiekt z bazy danych na listę. Nie chcesz tego wywoływać, jeśli wynik zostanie odrzucony, oczywiście. Tak więc zamiast używać wywołania tej funkcji bezpośrednio, definiujesz wewnętrzną klasę o nazwie 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());
    }
}

W tym kodzie, wywołanie getEverything() jest zawinięte tak, że nie zostanie wykonane, dopóki nie będzie potrzebne. Funkcja logowania wykona toString() na swoich parametrach tylko wtedy, gdy jest włączone debugowanie. W ten sposób Twój kod będzie cierpieć tylko na koszty wywołania funkcji zamiast pełnego Zadzwoń.

 30
Author: John Millikin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-07-15 19:48:34

[16]} z bieżącymi frameworkami logowania, pytanie jest dyskusyjne

Obecne struktury logowania, takie jak slf4j lub log4j 2, w większości przypadków nie wymagają instrukcji guard. Używają parametryzowanej instrukcji log tak, że zdarzenie może być bezwarunkowo rejestrowane, ale formatowanie wiadomości występuje tylko wtedy, gdy zdarzenie jest włączone. Budowa wiadomości jest wykonywana w miarę potrzeb przez rejestrator, a nie wcześniej przez aplikację.

Jeśli musisz skorzystać z antycznej biblioteki logowania, możesz przeczytać na aby uzyskać więcej tła i sposób na doposażenie starej biblioteki w sparametryzowane wiadomości.

Czy oświadczenia strażnicze naprawdę dodają złożoności?

Należy rozważyć wyłączenie instrukcji logging guards z obliczeń złożoności cyklomatycznej.

Można argumentować, że ze względu na swoją przewidywalną formę, warunkowe kontrole logowania naprawdę nie przyczyniają się do złożoności kodu.

Nieelastyczne metryki mogą sprawić, że dobry programista stanie się zły. Be ostrożnie!

Zakładając, że Twoje narzędzia do obliczania złożoności nie mogą być dostosowane do tego stopnia, poniższe podejście może zaoferować obejście.

Potrzeba warunkowego logowania

Zakładam, że Twoje Oświadczenia strażnicze zostały wprowadzone, ponieważ miałeś taki kod:

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;
}

W języku Java każde z poleceń log tworzy nową metodę StringBuilder i wywołuje metodę toString() na każdym obiekcie połączonym z łańcuchem znaków. Te toString() metody z kolei mogą tworzyć StringBuilder własne instancje i wywołują metody toString() swoich członków, itd., na potencjalnie dużym wykresie obiektów. (Przed Javą 5 była jeszcze droższa, ponieważ StringBuffer była używana, a wszystkie jej operacje są zsynchronizowane.)

Może to być stosunkowo kosztowne, zwłaszcza jeśli instrukcja log znajduje się w mocno wykonanej ścieżce kodu. I, napisane jak wyżej, to drogie formatowanie wiadomości występuje nawet wtedy, gdy rejestrator jest zobowiązany do odrzucenia wyniku, ponieważ poziom dziennika jest zbyt wysoko.

Prowadzi to do wprowadzenia twierdzeń strażniczych w postaci:

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

Z tym zabezpieczeniem, ocena argumentów d i w oraz konkatenacja łańcucha jest wykonywana tylko wtedy, gdy jest to konieczne.

Rozwiązanie do prostego, wydajnego logowania

Jednakże, jeśli logger (lub wrapper, który piszesz wokół wybranego pakietu logowania) pobiera formater i argumenty dla formatera, Budowa wiadomości może zostać opóźniona, dopóki nie będzie pewne, że będzie on używany, eliminując jednocześnie deklaracje strażnicze i ich cyklomatyczną złożoność.

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;
  }

}

Teraz, żadne kaskadowe toString() wywołania z ich alokacją buforów nie wystąpią chyba że są konieczne! To skutecznie eliminuje uderzenie wydajności, które doprowadziło do oświadczeń straży. Jedną małą karą, w Javie, byłoby auto-Boks wszelkich prymitywnych argumentów typu przekazywanych do loggera.

Kod dokonujący logowania jest prawdopodobnie jeszcze czystszy niż kiedykolwiek, ponieważ niechlujna konkatenacja sznurków przepadła. Może być jeszcze czystsze, jeśli ciągi formatowe są uzewnętrzniane (za pomocą ResourceBundle), co może również pomóc w utrzymaniu lub lokalizacji oprogramowania.

Dalsze ulepszenia

Zauważ również, że w języku Java obiekt MessageFormat może być używany zamiast "formatu" String, który daje dodatkowe możliwości, takie jak wybór formatu, aby lepiej obsługiwać liczby kardynalne. Inną alternatywą byłoby zaimplementowanie własnego formatowania możliwość, która wywołuje jakiś interfejs, który zdefiniujesz dla "oceny", a nie podstawową metodę toString().

 52
Author: erickson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-07-10 17:58:50

W językach obsługujących wyrażenia lambda lub bloki kodu jako parametry, jednym z rozwiązań tego problemu byłoby podanie właśnie tego metodzie logging. Że można ocenić konfigurację i tylko w razie potrzeby faktycznie wywołać / wykonać dostarczony blok lambda / code. Jeszcze nie próbowałem.

Teoretycznie jest to możliwe. Nie chciaĹ 'bym go uĺźywaä ‡ w produkcji ze wzglÄ ™ du na problemy wydajnoĹ" ci, jakich oczekujÄ ™ przy tak duĹźym uĹźyciu lamd/blokĂłw kodu do logowania.

Ale jako zawsze: w razie wątpliwości przetestuj go i zmierz wpływ na obciążenie procesora i pamięć.

 6
Author: pointernil,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-19 21:56:23

Dziękuję za wszystkie odpowiedzi! You guys rock:)

Teraz moja opinia nie jest tak prosta jak twoja:

Tak, dla jednego projektu (jak w "jednym programie wdrożonym i działającym samodzielnie na jednej platformie produkcyjnej"), przypuszczam, że można przejść wszystkie techniczne na mnie:

  • dedykowane obiekty 'Log Retriever', które mogą być przekazane do wrappera Loggera tylko wywołanie ToString() jest konieczne
  • używane w połączeniu z logowaniem funkcja zmienna (lub zwykły obiekt[] array!)

I masz to, jak wyjaśniają @John Millikin i @erickson.

Jednak ten problem zmusił nas do zastanowienia się nad "dlaczego w ogóle logowaliśmy się ?'
Nasz projekt to w rzeczywistości 30 różnych projektów (od 5 do 10 osób każdy) wdrożonych na różnych platformach produkcyjnych, z potrzebami komunikacji asynchronicznej i architekturą Centralnej Magistrali.
Proste logowanie opisane w pytaniu było dobre dla każdego projektu na początku (5 lat temu), ale od tego czasu musimy się postarać. Wprowadź KPI .

Zamiast zwracać się do rejestratora o zarejestrowanie czegokolwiek, prosimy automatycznie utworzony obiekt (o nazwie KPI) o zarejestrowanie zdarzenia. Jest to proste wezwanie (myKPI.I_am_signaling_myself_to_you ()) i nie musi być warunkowa(co rozwiązuje problem 'sztucznego zwiększenia złożoności cyklomatycznej').

Ten obiekt KPI wie kto go nazywa i ponieważ działa od początku aplikacja, jest w stanie odzyskać wiele danych, które wcześniej przetwarzaliśmy na miejscu, gdy logowaliśmy.
Dodatkowo obiekt KPI może być monitorowany niezależnie i obliczać/publikować na żądanie jego informacje na pojedynczej i oddzielnej szynie publikacji.
W ten sposób każdy klient może poprosić o informacje, których rzeczywiście chce (np. " czy mój proces się rozpoczął, a jeśli tak, to od kiedy ?'), zamiast szukać poprawnego pliku dziennika i szukania zaszyfrowanego ciągu znaków...

Rzeczywiście, pytanie: dlaczego w ogóle się logowaliśmy ?"uświadomiło nam, że nie logujemy się tylko dla programisty i jego testów jednostkowych lub integracyjnych, ale dla znacznie szerszej społeczności, w tym dla samych klientów końcowych. Nasz mechanizm "raportowania" musiał być scentralizowany, asynchroniczny, 24/7.

Specyfika tego mechanizmu KPI wykracza poza zakres tego pytania. Wystarczy powiedzieć, że jego właściwa kalibracja jest zdecydowanie najbardziej skomplikowana problem niefunkcjonalny, z którym mamy do czynienia. To wciąż przynosi system na kolanach od czasu do czasu! Odpowiednio skalibrowany jest jednak ratownikiem życia.

Jeszcze raz dziękuję za wszystkie sugestie. Rozważymy je dla niektórych części naszego systemu, gdy proste logowanie jest jeszcze na miejscu.
Ale innym punktem tego pytania było zilustrowanie Państwu konkretnego problemu w znacznie większym i bardziej skomplikowanym kontekście.
Mam nadzieję, że ci się podobało. Mogę zadać pytanie na temat KPI (które, jak sądzę lub Nie, Nie jest w żadnym Pytaniu na SOF do tej pory!) w przyszłym tygodniu.

Zostawiam tę odpowiedź do głosowania do przyszłego wtorku, wtedy wybieram odpowiedź (Nie tą oczywiście;))

 4
Author: VonC,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-20 07:41:30

Może to zbyt proste, ale co z użyciem refaktoryzacji "Extractor method" wokół klauzuli ochronnej? Twój przykładowy kod tego:

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
}

Staje się to:

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 ... */ }
 4
Author: flipdoubt,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-28 01:07:10

W C lub c++ użyłbym preprocesora zamiast instrukcji if do warunkowego logowania.

 3
Author: Tom Ritter,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-19 21:42:18

Przekaż poziom dziennika do loggera i pozwól mu zdecydować, czy napisać instrukcję log:

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

UPDATE: ach, widzę, że chcesz warunkowo utworzyć łańcuch dziennika bez instrukcji warunkowej. Prawdopodobnie w czasie wykonywania, a nie kompilacji.

Powiem tylko, że sposób, w jaki to rozwiązaliśmy, polega na umieszczeniu kodu formatującego w klasie logger, tak aby formatowanie miało miejsce tylko wtedy, gdy poziom przejdzie. Bardzo podobny do wbudowanego sprintf. Na przykład:

myLogger.info(Level.INFO,"A String %d",some_number);   
To powinno spełniać Twoje kryteria.
 3
Author: ,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-19 22:06:28

Alt text http://www.scala-lang.org/sites/default/files/newsflash_logo.png

Scala posiada annontację @elidable () , która pozwala na usunięcie metod z flagą kompilatora.

Z REPL scala:

C: > scala

Witamy w Scali w wersji 2.8.0.final (Java HotSpot (TM) 64-Bit Server VM, Java 1. 6.0_16). Wpisz wyrażenia, aby je ocenić. Wpisz: pomoc aby uzyskać więcej informacji.

Scala > import scala.adnotacja./ align = "left" / import Scali.adnotacja.elidable

Scala > import scala.adnotacja./ align = "left" / _ import Scali.adnotacja./ align = "left" / _

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

LogDebug: (arg: String)Unit

Scala> logDebug ("testowanie")

Scala >

Z elide-beloset

C:>scala-Xelide-below 0

Witamy w Scali w wersji 2.8.0.final (Java HotSpot (TM) 64-bitowy Serwer VM, Java 1. 6.0_16). Wpisz wyrażenia, aby je ocenić. Wpisz: pomoc aby uzyskać więcej informacji.

Scala > import scala.adnotacja./ align = "left" / import Scali.adnotacja.elidable

Scala > import scala.adnotacja./ align = "left" / _ import Scali.adnotacja./ align = "left" / _

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

LogDebug: (arg: String)Unit

Scala> logDebug ("testowanie")

Testowanie

Scala >

Zobacz też Scala assert definition

 2
Author: oluies,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:34:35

Warunkowe logowanie jest złem. Dodaje niepotrzebny bałagan do kodu.

Należy zawsze wysyłać do loggera obiekty, które posiadasz:

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

A potem mieć Javę.util.logowanie.Formatter, który używa MessageFormat do spłaszczenia foo i paska do ciągu, który ma być wyprowadzony. Zostanie wywołana tylko wtedy, gdy logger i handler będą logować się na tym poziomie.

Dla dodatkowej przyjemności można mieć jakiś rodzaj języka wyrażeń, aby móc uzyskać dokładną kontrolę nad formatowaniem rejestrowane obiekty (toString nie zawsze może być użyteczny).

 2
Author: simon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-07-02 22:15:39

Jak bardzo nienawidzę makr w C / C++, w pracy mamy # definiuje dla części if, która jeśli false ignoruje (nie ocenia) następujące wyrażenia, ale jeśli true zwraca strumień, do którego rzeczy mogą być rurociągiem za pomocą operatora'

LOGGER(LEVEL_INFO) << "A String";

Zakładam, że wyeliminuje to dodatkową "złożoność", którą widzi Twoje narzędzie, a także eliminuje wszelkie obliczenia ciągu znaków lub wyrażenia, które mają być rejestrowane, jeśli poziom nie został osiągnięty.

 1
Author: quamrana,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-27 18:27:10

Oto eleganckie rozwiązanie z użyciem wyrażenia trójkowego

Logger.info (logger.isInfoEnabled ()? "Log Statement idzie tutaj...": null);

 1
Author: Muhammad Atif Riaz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-09-16 05:38:59

Rozważmy funkcję logowania util ...

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

Następnie wykonaj połączenie z "zamknięciem" wokół kosztownej oceny, której chcesz uniknąć.

debugUtil(“We got a %s”, new Object() {
       @Override String toString() { 
       // only evaluated if the debug statement is executed
           return expensiveCallToGetSomeValue().toString;
       }
    }
);
 1
Author: johnlon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-11-22 14:05:03