전역 적으로 고유 한 메시지 ID를 사용하여 코드를 찾을 수 있도록 만들기


39

버그를 찾는 일반적인 패턴은 다음과 같습니다.

  1. 출력이 없거나 교수형 프로그램과 같은 이상한 점을 관찰하십시오.
  2. 로그 또는 프로그램 출력에서 ​​관련 메시지를 찾으십시오 (예 : "Foo를 찾을 수 없음"). (다음은 버그를 찾는 경로 인 경우에만 해당됩니다. 스택 추적 또는 기타 디버깅 정보를 쉽게 사용할 수있는 경우 이는 또 다른 이야기입니다.)
  3. 메시지가 인쇄되는 코드를 찾으십시오.
  4. Foo가 그림을 입력하는 첫 번째 장소와 메시지가 인쇄되는 위치 사이의 코드를 디버그하십시오.

세 번째 단계는 코드에서 "Foo를 찾을 수 없음"(또는 템플릿 문자열 Could not find {name})이 인쇄 되는 곳이 많기 때문에 디버깅 프로세스가 자주 중단되는 단계입니다 . 실제로 철자 실수로 여러 번 실제 위치를 찾을 때보 다 훨씬 빠르게 실제 위치를 찾는 데 도움이되었습니다. 전체 시스템과 전 세계에서 메시지를 독특하게 만들어 관련 검색 엔진에 즉시 영향을 미쳤습니다.

이것의 확실한 결론은 코드에서 전역 적으로 고유 한 메시지 ID를 사용하여 메시지 문자열의 일부로 하드 코딩하고 코드베이스에 각 ID가 한 번만 발생하는지 확인해야한다는 것입니다. 유지 관리 측면에서이 커뮤니티는이 접근 방식의 가장 중요한 장단점을 어떻게 생각하며이를 구현하거나 구현할 필요가 없는지 (소프트웨어에 항상 버그가 있다고 가정) 어떻게해야합니까?


54
대신 스택 추적을 사용하십시오. 스택 추적은 오류가 발생한 위치와 오류를 호출 한 모든 함수를 호출 한 모든 함수를 정확하게 알려줍니다. 필요한 경우 예외가 발생하면 전체 추적을 기록하십시오. C와 같이 예외가없는 언어로 작업하는 경우 다른 이야기입니다.
Robert Harvey

6
@ l0b0 문구에 대한 작은 조언. "이 공동체가 생각하는 것 ... 장단점"은 너무 광범위하게 보일 수있는 문구입니다. 이 사이트는 "좋은 주관적인"질문을 허용하는 사이트이며, 이러한 유형의 질문을 허용 한 대가로 OP가 의미있는 합의에 대한 의견과 답변을 "수렴"하는 작업을 수행해야합니다.
rwong

@rwong 감사합니다! 나는 포럼에서 질문이 더 좋았지 만 질문이 이미 매우 훌륭하고 적절한 답변을 받았다고 생각합니다. JohnWu의 명확한 답변을 읽은 후에 RobertHarvey의 의견에 대한 답변을 철회했습니다. 그렇지 않은 경우 특정 목자 팁이 있습니까?
l0b0

1
내 메시지는 "bar ()를 호출하는 동안 Foo를 찾을 수 없습니다"와 같습니다. 문제 해결됨. 어깨를 으쓱하다. 단점은 고객이보기에는 약간 누수가 있지만 어쨌든 오류 메시지의 세부 사항을 숨기고 원숭이에게 일부 기능 이름을 볼 수없는 시스템 관리자에게만 제공되는 경향이 있습니다. 실패하면 예를 들어 멋진 고유 ID / 코드가 트릭을 수행합니다.
Monica와의 가벼움 경주

1
고객이 전화를 걸고 컴퓨터가 영어로 작동하지 않을 때 매우 유용합니다! 우리가 지금 이메일과 로그 파일을 가지고 있기 때문에 요즘 훨씬 적은 문제 .....
Ian

답변:


12

전반적으로 이것은 유효하고 귀중한 전략입니다. 여기 몇 가지 생각이 있습니다.

이러한 전략은 이러한 모든 정보가 결합 될 때 실행 추적을 "삼각형 화"하는 데 도움이되고 문제 해결사가 사용자 / 응용 프로그램이 수행하려는 작업과 실제로 발생한 작업을 이해할 수있게한다는 의미에서 "텔레 메 트리"라고도합니다. .

수집해야 할 필수 데이터는 다음과 같습니다 (모두 알고 있음).

  • 코드 위치, 즉 호출 스택 및 대략적인 코드 줄
    • 기능이 적당히 작은 단위로 분해되는 경우 "대략적인 코드 라인"은 필요하지 않습니다.
  • 기능의 성공 / 실패와 관련된 모든 데이터
  • 휴먼 사용자 / 외부 에이전트 / API 사용자가 달성하려고하는 것을 적을 수있는 고급 "명령".
    • 아이디어는 소프트웨어가 어딘가에서 오는 명령을 받아들이고 처리한다는 것입니다.
    • 이 과정에서 수십에서 수백에서 수천의 함수 호출이 발생했을 수 있습니다.
    • 이 프로세스 전체에서 생성 된 원격 측정을이 프로세스를 트리거하는 최상위 명령으로 추적 할 수 있기를 원합니다.
    • 웹 기반 시스템의 경우 원래 HTTP 요청 및 해당 데이터는 "고수준 요청 정보"의 예입니다.
    • GUI 시스템의 경우 사용자가 무언가를 클릭하면이 설명에 적합합니다.

낮은 수준의 로그 메시지를 트리거하는 최고 수준의 명령으로 다시 추적하지 못해 전통적인 로깅 방식이 부족한 경우가 종종 있습니다. 스택 추적은 최고 수준의 명령을 처리하는 데 도움이 된 상위 기능의 이름 만 캡처하며 해당 명령을 특성화하는 데 필요한 세부 정보 (데이터)는 아닙니다.

일반적으로 소프트웨어는 이러한 종류의 추적 요구 사항을 구현하기 위해 작성되지 않았습니다. 이것은 하위 레벨 메시지를 상위 레벨 명령과 연관시키는 것을 더 어렵게 만듭니다. 많은 요청과 응답이 겹칠 수 있고 처리가 원래 요청 수신 스레드와 다른 스레드로 오프로드 될 수있는 자유 멀티 스레드 시스템에서는 특히 문제가 더 심각합니다.

따라서 원격 측정을 최대한 활용하려면 전체 소프트웨어 아키텍처를 변경해야합니다. "tracer"인수를 승인하고 전파하려면 대부분의 인터페이스 및 함수 호출을 수정해야합니다.

유틸리티 함수조차도 "추적자"인수를 추가해야합니다. 실패하면 로그 메시지 자체가 특정 상위 레벨 명령과 상관 될 수 있습니다.

원격 측정 추적을 어렵게 만드는 또 다른 실패는 개체 참조 (널 포인터 또는 참조)가 누락 된 것입니다. 중요한 데이터가 누락되면 실패에 유용한 정보를보고 할 수 없습니다.

로그 메시지 작성 측면에서 :

  • 일부 소프트웨어 프로젝트는 관리자 전용 로그 메시지의 경우에도 현지화 (외국어로 번역)가 필요할 수 있습니다.
  • 일부 소프트웨어 프로젝트는 로깅 목적으로도 민감한 데이터와 중요하지 않은 데이터를 명확하게 분리해야 할 수 있으며 관리자가 실수로 특정 민감한 데이터를 볼 가능성이 없습니다.
  • 오류 메시지를 난독 처리하지 마십시오. 그것은 고객의 신뢰를 약화시킬 것입니다. 고객 관리자는 해당 로그를 읽고 이해해야합니다. 고객 관리자로부터 숨겨져 야 할 독점 비밀이 있다고 느끼게하지 마십시오.
  • 고객이 원격 측정 로그를 가져와 기술 지원 담당자를 석방 할 것으로 기대하십시오. 그들은 알고 싶어합니다. 원격 지원 로그를 올바르게 설명하도록 기술 지원 담당자를 교육하십시오.

1
실제로 AOP는 코드베이스에 대한 최소한의 침입으로 모든 관련 호출에 Tracer를 추가하여이 문제를 해결할 수있는 고유 한 기능을 주로 선전했습니다.
감독

또한 "로그 메시지 작성"목록에 "무엇"이 아닌 "왜"및 "수정 방법"이라는 관점에서 실패를 특성화하는 것이 중요하다고 덧붙입니다.
감독

58

코드의 수백 곳에서 사용되는 간단한 유틸리티 기능이 있다고 상상해보십시오.

decimal Inverse(decimal input)
{
    return 1 / input;
}

우리가 제안한대로해야한다면

decimal Inverse(decimal input)
{
    try 
    {
        return 1 / input;
    }
    catch(Exception ex)
    {
        log.Write("Error 27349262 occurred.");
    }
}

입력이 0 인 경우 발생할 수있는 오류입니다. 이로 인해 0으로 나누기 예외가 발생합니다.

출력 또는 로그에 27349262가 있다고 가정 해 봅시다. 0 값을 전달한 코드를 어디에서 찾습니까? 고유 한 ID를 가진 기능은 수백 곳에서 사용됩니다. 따라서 0으로 나누기가 발생했다는 것을 알 수 있지만 누구인지 알 수 없습니다 0.

메시지 ID를 기록하지 않으려면 스택 추적을 기록하십시오.

스택 추적의 세부 정보가 귀찮은 경우 런타임에서 제공하는 방식으로 문자열로 덤프하지 않아도됩니다. 사용자 정의 할 수 있습니다. 예를 들어 약식 스택 추적을 n레벨 로만 원한다면 다음과 같이 작성할 수 있습니다 (c #을 사용하는 경우).

static class ExtensionMethods
{
    public static string LimitedStackTrace(this Exception input, int layers)
    {
        return string.Join
        (
            ">",
            new StackTrace(input)
                .GetFrames()
                .Take(layers)
                .Select
                (
                    f => f.GetMethod()
                )
                .Select
                (
                    m => string.Format
                    (
                        "{0}.{1}", 
                        m.DeclaringType, 
                        m.Name
                    )
                )
                .Reverse()
        );
    }
}

그리고 이것을 다음과 같이 사용하십시오 :

public class Haystack
{
    public static void Needle()
    {
        throw new Exception("ZOMG WHERE DID I GO WRONG???!");
    }

    private static void Test()
    {
        Needle();
    }

    public static void Main()
    {
        try
        {
            Test();
        }
        catch(System.Exception e)
        {
            //Get 3 levels of stack trace
            Console.WriteLine
            (
                "Error '{0}' at {1}", 
                e.Message, 
                e.LimitedStackTrace(3)
            );  
        }
    }
}

산출:

Error 'ZOMG WHERE DID I GO WRONG???!' at Haystack.Main>Haystack.Test>Haystack.Needle

메시지 ID를 유지하는 것보다 쉽고 유연합니다.

DotNetFiddle에서 내 코드를 훔치십시오


32
흠 나는 분명히 내 요점을 충분히 밝히지 않았다고 생각한다. 코드 위치 마다 고유 한 Robert라는 것을 알고 있습니다. 코드 경로 마다 고유하지 않습니다 . 실제 문제가 입력이 올바르게 설정되지 않은 경우와 같이 위치를 아는 것은 종종 쓸모가 없습니다. 강조하기 위해 언어를 약간 편집했습니다.
John Wu

1
둘 다 좋은 지적입니다. 스택 추적에 다른 문제가 있습니다. 상황에 따라 거래 중단이 될 수도 있고 아닐 수도 있습니다. 특히 일부 언어와 같이 단축 버전이 아닌 전체 스택 추적 을 포함하려는 경우 크기가 메시지를 sw 게 할 수 있습니다. 기본적으로 수행하십시오. 대안으로 스택 추적 로그를 별도로 작성하고 해당 로그에 번호가 지정된 인덱스를 응용 프로그램 출력에 포함시킬 수 있습니다.
l0b0

12
I / O가 쇄도 할까봐 염려되는 것들이 너무 많으면 심각한 문제가 있습니다. 아니면 그냥 인색하고 있습니까? 실제 성능 저하는 아마도 스택 해제 일 것입니다.
John Wu

9
3.5 플로피에 로그를 쓰는 경우 스택 추적을 단축하기위한 솔루션으로 편집;)
John Wu

7
@JohnWu 그리고 "[...]에서"IOException 'File not Found' "를 잊지 마십시오. 콜 스택의 약 50 개의 레이어를 알려주지 만 정확히 피의 파일이 발견되지 않은 것을 알려주지는 않습니다.
Joker_vD

6

SAP NetWeaver는 수십 년 동안이 작업을 수행하고 있습니다.

일반적인 SAP ERP 시스템 인 대규모 코드 거대 오류를 해결할 때 유용한 도구로 입증되었습니다.

오류 메시지는 각 메시지가 메시지 클래스 및 메시지 번호로 식별되는 중앙 저장소에서 관리됩니다.

오류 메시지를 출력하려면 클래스, 숫자, 심각도 및 메시지 특정 변수 만 명시하십시오. 메시지의 텍스트 표현은 런타임에 작성됩니다. 일반적으로 메시지가 나타나는 모든 컨텍스트에서 메시지 클래스와 번호를 볼 수 있습니다. 이것은 몇 가지 깔끔한 효과가 있습니다 :

  • ABAP 코드베이스에서 특정 오류 메시지를 생성하는 모든 코드 줄을 자동으로 찾을 수 있습니다.

  • 특정 오류 메시지가 생성 될 때 트리거되는 동적 디버거 중단 점을 설정할 수 있습니다.

  • "Foo를 찾을 수 없음"을 찾는 경우보다 SAP 기술 자료 문서에서 오류를 찾아보다 관련성이 높은 검색 결과를 얻을 수 있습니다.

  • 메시지의 텍스트 표현은 번역 가능합니다. 따라서 문자열 대신 메시지 사용을 장려함으로써 i18n 기능도 얻을 수 있습니다.

메시지 번호가 포함 된 오류 팝업 예 :

error1

오류 저장소에서 해당 오류를 찾으십시오.

error2

코드베이스에서 찾으십시오.

error3

그러나 단점이 있습니다. 보시다시피, 이러한 코드 줄은 더 이상 자체 문서화되지 않습니다. 소스 코드를 읽고 MESSAGE위의 스크린 샷과 같은 문장을 볼 때 실제 의미를 컨텍스트에서만 유추 할 수 있습니다. 또한 때때로 사람들은 런타임에 메시지 클래스와 번호를받는 사용자 지정 오류 처리기를 구현합니다. 이 경우 오류를 자동으로 찾을 수 없거나 오류가 실제로 발생한 위치에서 찾을 수 없습니다. 첫 번째 문제의 해결 방법은 독자에게 메시지의 의미를 알려주는 주석을 항상 소스 코드에 추가하는 습관을들이는 것입니다. 두 번째는 자동 메시지 조회가 작동하도록 데드 코드를 추가하여 해결됩니다. 예:

" Do not use special characters
my_custom_error_handler->post_error( class = 'EU' number = '271').
IF 1 = 2.
   MESSAGE e271(eu).
ENDIF.    

그러나 이것이 불가능한 상황이 있습니다. 예를 들어 비즈니스 규칙을 위반할 때 표시되는 오류 메시지를 구성 할 수있는 UI 기반 비즈니스 프로세스 모델링 도구가 있습니다. 이러한 도구의 구현은 완전히 데이터 중심이므로 이러한 오류는 사용처 목록에 표시되지 않습니다. 즉, 오류의 원인을 찾으려고 할 때 사용처 목록에 너무 많이 의존하면 빨간색 청어가 될 수 있습니다.


메시지 카탈로그는 한동안 GNU / Linux의 일부였으며 UNIX는 일반적으로 POSIX 표준 으로 사용되었습니다 .
감독

@bishop 나는 일반적으로 POSIX 시스템을 위해 특별히 프로그래밍하지 않기 때문에 익숙하지 않습니다. POSIX 메시지 카탈로그와 OP가 구현에서 배울 수있는 내용을 설명하는 다른 답변을 게시 할 수 있습니다.
Philipp

3
나는 이것을 oughties에서 다시 한 프로젝트의 일부였다. 우리가 겪은 한 가지 문제는 다른 모든 것들과 함께 데이터베이스에 "데이터베이스에 연결할 수 없습니다"라는 메시지를 넣었다는 것입니다.
JimmyJames

5

이 접근법의 문제점은 더 자세한 로깅으로 이어진다는 것입니다. 99.9999 %는 결코 보지 않을 것입니다.

대신 프로세스 시작시 프로세스의 성공 / 실패 상태를 캡처하는 것이 좋습니다.

이를 통해 코드를 단계별로 버그를 로컬로 재현하고 로깅을 프로세스 당 두 곳으로 제한 할 수 있습니다. 예.

OrderPlaced {id:xyz; ...order data..}
OrderPlaced {id:xyz; ...Fail, ErrorMessage..}

이제 내 dev 컴퓨터에서 똑같은 상태를 사용하여 오류를 재현하고 디버거의 코드를 단계별로 실행하고 수정을 확인하기 위해 새로운 단위 테스트를 작성할 수 있습니다.

또한 필요한 경우 로깅 실패 만하거나 다른 위치 (데이터베이스? 메시지 큐?)를 유지하여 더 많은 로깅을 피할 수 있습니다.

우리는 민감한 데이터를 기록 할 때 특히주의해야합니다. 따라서 솔루션에서 메시지 큐 또는 이벤트 저장소 패턴을 사용하는 경우 특히 효과적입니다. 로그에는 "메시지 xyz 실패"만 표시하면됩니다.


중요한 데이터를 대기열에 넣는 것은 여전히 ​​기록 중입니다. 암호화 형태가없는 DB에 민감한 입력을 저장하는 것과 같이 이것은 권장되지 않습니다.
jpmc26

시스템이 큐 또는 db를 실행하면 데이터가 이미 존재하므로 보안도 마찬가지입니다. 너무 많은 로깅은 로그가 보안 제어 범위를 벗어나는 경향이 있기 때문에 나쁩니다.
Ewan

맞아 요, 그게 요점입니다. 데이터는 영구적이고 일반적으로 완전한 텍스트로 유지 되므로 권장 하지 않습니다. 민감한 데이터의 경우 위험을 감수하지 않고 데이터를 저장하는 위치를 최소화 한 다음 데이터를 저장하는 방법을 매우 잘 알고주의하는 것이 좋습니다.
jpmc26

파일에 쓰고 있기 때문에 전통적으로 영구적입니다. 그러나 오류 대기열은 일시적입니다.
Ewan

나는 아마도 대기열의 구현 (및 설정)에 달려 있다고 말할 것입니다. 당신은 어떤 대기열에 그것을 덤프하고 안전하다고 기대할 수 없습니다. 그리고 대기열이 소비 된 후 어떻게됩니까? 다른 사람이 볼 수 있도록 로그는 여전히 어딘가에 있어야합니다. 또한, 그것은 일시적으로 열어보고 싶은 추가 공격 벡터가 아닙니다. 공격이 민감한 데이터가 있다는 것을 알게되면 가장 최근의 항목조차도 가치가있을 수 있습니다. 그리고 누군가가 스위치를 모르고 뒤집어 놓을 위험이 있으므로 디스크에도 로깅을 시작합니다. 그것은 단지 벌레의 수일뿐입니다.
jpmc26

1

로깅은이 문제를 해결하는 방법이 아니라 오히려이 상황이 예외적으로 간주되어 프로그램을 잠그고 예외가 발생해야한다고 제안합니다. 코드가 다음과 같다고 가정 해보십시오.

public Foo GetFoo() {

     //Expecting that this should never by null.
     var aFoo = ....;

     if (aFoo == null) Log("Could not find Foo.");

     return aFoo;
}

호출 코드가 Foo가 존재하지 않으며 실제로 다음과 같이 될 수 있다는 사실을 처리하도록 설정되지 않은 것 같습니다.

public Foo GetFooById(int id) {
     var aFoo = ....;

     if (aFoo == null) throw new ApplicationException("Could not find Foo for ID: " + id);

     return aFoo;
}

그리고 이것은 디버깅을 돕는 데 사용될 수있는 예외와 함께 스택 추적을 반환합니다.

다른 방법으로, 검색 할 때 Foo가 null 일 수 있다고 생각되면 호출 사이트를 수정해야합니다.

void DoSomeFoo(Foo aFoo) {

    //Guard checks on your input - complete with stack trace!
    if (aFoo == null) throw new ArgumentNullException(nameof(aFoo));

    ... operations on Foo...
}

예기치 않은 상황에서 소프트웨어가 멈추거나 '이상하게'작동한다는 사실은 나에게 잘못 된 것 같습니다 .Foo가 필요하고 거기에서 처리하지 못하는 경우 Foo가 필요한 경로를 따라 진행하는 것보다 충돌하는 것이 좋습니다. 시스템이 손상되었습니다.


0

적절한 로깅 라이브러리는 확장 메커니즘을 제공하므로 로그 메시지가 생성 된 방법을 알고 싶다면 즉시 사용할 수 있습니다. 프로세스가 스택 추적을 생성하고 로깅 라이브러리에서 벗어날 때까지 통과해야하므로 실행에 영향을 미칩니다.

즉, 그것은 당신이 원하는 ID에 따라 다릅니다.

  • 사용자에게 제공되는 오류 메시지를 로그와 연관 시킵니까?
  • 메시지가 생성 될 때 어떤 코드가 실행되고 있는지에 대한 표기법을 제공합니까?
  • 머신 이름과 서비스 인스턴스를 추적 하시겠습니까?
  • 스레드 ID를 추적 하시겠습니까?

이러한 모든 작업은 적절한 로깅 소프트웨어를 사용하여 즉시 수행 할 수 있습니다 ( Console.WriteLine()또는 아닙니다 Debug.WriteLine()).

개인적으로 더 중요한 것은 실행 경로를 재구성하는 능력입니다. 이것이 Zipkin 과 같은 도구 가 달성하도록 설계된 것입니다. 시스템 전체에서 하나의 사용자 작업 동작을 추적하는 하나의 ID 로그를 중앙 검색 엔진에두면 가장 오래 실행되는 작업을 찾을 수있을뿐만 아니라 ELK 스택 과 같은 해당 작업에 적용되는 로그를 불러올 수 있습니다 .

모든 메시지와 함께 변경되는 불투명 한 ID는 그다지 유용하지 않습니다. 마이크로 서비스 전체를 통해 행동을 추적하는 데 사용되는 일관된 ID ... 매우 유용합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.