Зробити код змінним за допомогою використання глобально унікальних ідентифікаторів повідомлень


39

За цим сценарієм розповсюджується загальна схема пошуку помилки:

  1. Дотримуйтесь дивнощів, наприклад, відсутність виводу або вивішування програми.
  2. Знайдіть відповідне повідомлення у журналі або програмі, наприклад, "Не вдалося знайти Foo". (Наступне є актуальним лише у тому випадку, якщо це шлях, який потрібно знайти, щоб знайти помилку. Якщо трак стека чи інша інформація про налагодження легко доступна, це вже інша історія.)
  3. Знайдіть код, де друкується повідомлення.
  4. Налагоджуйте код між першим місцем, коли Foo вводить (або повинен вводити) зображення та місцем друку повідомлення.

На цьому третьому кроці процес налагодження часто переривається, оскільки в коді є багато місць, де Could not find {name}друкується "Не вдалося знайти Foo" (або шаблонна рядок ). Насправді, кілька разів орфографічна помилка допомогла мені знайти фактичне місце розташування набагато швидше, ніж я б інакше - це зробило повідомлення унікальним у всій системі та часто в усьому світі, в результаті чого відповідна пошукова система негайно потрапила.

Очевидним висновком з цього є те, що ми повинні використовувати в коді унікальні ідентифікатори повідомлення, жорстко кодуючи його як частину рядка повідомлення і, можливо, перевіряючи, що в базі коду є лише одне виникнення кожного ідентифікатора. Що стосується ремонтопридатності, що, на думку цієї громади, є найважливішими плюсами та мінусами цього підходу, і як би ви реалізували це чи іншим чином гарантували, що його реалізація ніколи не стане необхідною (якщо припустити, що у програмному забезпеченні завжди будуть помилки)?


54
Використовуйте замість своїх слідів стека. Трасування стека не тільки точно покаже вам, де сталася помилка, але і кожну функцію, яка викликала кожну функцію, яка її викликала. Запишіть весь слід, якщо виникає виняток, якщо це необхідно. Якщо ви працюєте мовою, яка не має винятків, як-от C, це вже інша історія.
Роберт Харві

6
@ l0b0 невелика порада щодо формулювання. "що думає ця громада ..." за і проти "- це фрази, які можна сприймати як занадто широкі. Це веб-сайт, який дозволяє «добрі суб’єктивні» запитання, а взамін на те, щоб дозволити такий тип запитань, ви, як ОП, очікували б виконати роботу з «пастирства» коментарів та відповідей до змістовного консенсусу.
rwong

@rwong Дякую! Я вважаю, що питання вже отримало дуже хороший та точний відповідь, хоча це, можливо, було б краще задати на форумі. Я відкликав свою відповідь на коментар Роберта Гарвея, прочитавши уточнюючу відповідь JohnWu, якщо це саме ви маєте на увазі. Якщо ні, чи є у вас якісь конкретні поради щодо вівчарства?
l0b0

1
Мої повідомлення виглядають так: "Не вдалося знайти Foo під час дзвінка в бар ()". Проблема вирішена. Похити плечима. Даунсайд - це дещо проникливий для того, щоб його бачили клієнти, але ми, як правило, приховуємо деталі повідомлень про помилки від них так чи інакше, роблячи це доступним лише для сисадмінів, які не могли дати мавпам, що вони отримують, щоб побачити деякі назви функцій. Якщо цього не зробити, приємний маленький унікальний ідентифікатор / код дозволить зробити це.
Гонки легкості з Монікою

1
Це ДУЖЕ корисно, коли клієнт телефонує вам, а комп'ютер не працює англійською мовою! Набагато менше проблем у ці дні, оскільки зараз у нас є файли електронної пошти та журналів .....
Ян

Відповіді:


12

В цілому, це дійсна і цінна стратегія. Ось кілька думок.

Ця стратегія також відома як "телеметрія" в тому сенсі, що коли вся така інформація поєднується, вони допомагають "тріангулювати" слід виконання та дозволяють засобу усунення несправностей зрозуміти, що намагається виконати користувач / додаток та що насправді сталося .

Деякі важливі фрагменти даних, які необхідно зібрати (які ми всі знаємо):

  • Розташування коду, тобто стека виклику та приблизного рядка коду
    • "Приблизний рядок коду" не потрібен, якщо функції розумно розкладені на відповідно невеликі одиниці.
  • Будь-які фрагменти даних, які стосуються успіху / відмови функції
  • "Команда" високого рівня, яка може визначити, що намагається виконати користувач / зовнішній агент / користувач API.
    • Ідея полягає в тому, що програмне забезпечення буде приймати і обробляти команди, що надходять звідкись.
    • Під час цього процесу можуть відбуватися десятки до сотень до тисяч функціональних дзвінків.
    • Ми хотіли б, щоб будь-яка телеметрія, створена протягом цього процесу, була відстежена до команди найвищого рівня, яка запускає цей процес.
    • Для веб-систем оригінальний запит HTTP та його дані будуть прикладом такої "інформації про запит високого рівня"
    • Для систем GUI користувач, який натиснув щось, відповідатиме цьому опису.

Часто підходи до традиційного ведення журналу виявляються недостатніми, через те, що не вдалося простежити повідомлення журналу низького рівня до команди найвищого рівня, яка його запускає. Трасування стека фіксує лише імена функцій вищого рівня, які допомагали керувати командою вищого рівня, а не деталі (дані), які іноді потрібні для характеристики цієї команди.

Зазвичай програмне забезпечення не було написане для реалізації такого роду вимог простеження. Це ускладнює співвіднесення повідомлення низького рівня з командою високого рівня. Проблема особливо гостріша у вільно багатопотокових системах, де багато запитів і відповідей можуть перекриватися, а обробка може бути завантажена в інший потік, ніж початковий потік, що приймає запит.

Таким чином, щоб отримати максимальну цінність від телеметрії, знадобляться зміни в загальній архітектурі програмного забезпечення. Більшість інтерфейсів та функціональних викликів потрібно буде модифікувати, щоб прийняти та поширити аргумент "прослідковувач".

Навіть функції утиліти потрібно буде додати аргумент "прослідковувач", так що якщо воно не виходить, повідомлення журналу дозволить співвіднести себе з певною командою високого рівня.

Інший збій, який ускладнить відстеження телеметрії, - це відсутні посилання на об'єкти (нульові вказівники або посилання). Якщо відсутня якась важлива частина даних, неможливо повідомити про щось корисне для відмови.

Що стосується написання журнальних повідомлень:

  • Деякі програмні проекти можуть вимагати локалізації (перекладу на іноземну мову) навіть для журнальних повідомлень, призначених лише для адміністраторів.
  • Деякі програми програмного забезпечення можуть потребувати чіткого поділу між чутливими та нечутливими даними, навіть з метою реєстрації, і щоб адміністратори не мали шансу випадково побачити певні конфіденційні дані.
  • Не намагайтеся придушити повідомлення про помилку. Це може підірвати довіру клієнтів. Адміністратори клієнтів розраховують прочитати ці журнали та мати сенс у цьому. Не дайте їм відчути, що є якась власна таємниця, яку необхідно приховати від адміністраторів клієнтів.
  • Очікуйте, що клієнти принесуть шматочок телеметричного журналу та оброблять вашу службу технічної підтримки. Вони очікують знати. Навчіть свій персонал з технічної підтримки, щоб правильно пояснити журнал телеметрії.

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.");
    }
}

Помилка, яка може статися, якщо вхід був нульовим; це призведе до поділу на нульовий виняток.

Отже, скажімо, ви бачите 27349262 у своєму висновку чи ваших журналах. Де ви шукаєте, щоб знайти код, який передав нульове значення? Пам'ятайте, що функція - з її унікальним ідентифікатором - використовується у сотнях місць. Тож ви, хоча ви можете знати, що відбувся поділ на нуль, ви не маєте поняття, чиє 0це.

Мені здається, якщо ви збираєтеся турбуватись із реєстрацією ідентифікаторів повідомлення, ви також можете записати слід стека.

Якщо багатослівність сліду стека - це те, що вас турбує, вам не доведеться скидати його як рядок так, як час його виконання дає вам. Ви можете налаштувати його. Наприклад, якщо ви хочете, щоб скорочений слід стека переходив лише на 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

Можливо, простіше, ніж зберігати ідентифікатори повідомлень, і більш гнучким.

Викрасти мій код у DotNetFiddle


32
Хм, я думаю, я не висловився досить чітко. Я знаю, що вони унікальні Роберт - за кодовим місцем. Вони не є унікальними для кодового шляху. Знання місця часто марно, наприклад, якщо справжня проблема полягає в тому, що введення не було встановлено належним чином. Я трохи відредагував свою мову, щоб наголосити.
Джон Ву

1
Гарні бали, ви обоє. Існує інша проблема зі слідами стека, яка може бути, а може і не бути переривником угод, залежно від ситуації: їх розмір може призвести до того, що вони переповнюють повідомлення, особливо якщо ви хочете включити весь слід стека, а не скорочену версію, як деякі мови робити за замовчуванням. Можливо, альтернативою було б написати журнал слідів стека окремо і включити пронумеровані індекси до цього журналу у висновку програми.
l0b0

12
Якщо у вас так багато таких, що ви переживаєте затоплення вводу / виводу, щось серйозно не так. Або ти просто скупий? Справжній хіт на виставу - це, мабуть, стек.
Джон Ву

9
Відредаговано рішення для скорочення слідів стека, якщо ви пишете журнали на дискет 3.5;)
Джон Ву

7
@JohnWu А також не забувайте "IOException" Файл не знайдено "в [...]", який розповідає про п’ятдесят шарів стека викликів, але не повідомляє, який саме кривавий файл не знайдено.
Joker_vD

6

SAP NetWeaver займається цим десятиліттями.

Він виявився цінним інструментом для усунення несправностей у масовому кодовому бегемоті, який є типовою системою SAP ERP.

Повідомленнями про помилки керують у центральному сховищі, де кожне повідомлення ідентифікується за класом повідомлень та номером повідомлення.

Коли ви хочете витіснити повідомлення про помилку, ви вказуєте лише змінні класу, числа, суворості та певного повідомлення. Текстове представлення повідомлення створюється під час виконання. Зазвичай ви бачите клас та номер повідомлення в будь-якому контексті, де відображаються повідомлення. Це має кілька акуратних ефектів:

  • Ви можете автоматично знайти будь-які рядки коду в кодовій базі ABAP, які створюють певне повідомлення про помилку.

  • Ви можете встановити динамічні точки відключення налагодження, які запускаються, коли генерується певне повідомлення про помилку.

  • Ви можете шукати помилки в статтях бази знань SAP і отримувати більш відповідні результати пошуку, ніж якщо шукати "Не вдалося знайти Foo".

  • Текстові подання повідомлень є перекладними. Таким чином, заохочуючи використання повідомлень замість рядків, ви також отримуєте можливості i18n.

Приклад спливаючої помилки з номером повідомлення:

помилка1

Пошук цієї помилки в сховищі помилок:

помилка2

Знайдіть його в кодовій базі:

помилка3

Однак є і недоліки. Як бачите, ці рядки коду вже не самодокументовані. Коли ви читаєте вихідний код і бачите MESSAGEтвердження, подібне до наведеного на скріншоті, ви можете лише з контексту зробити висновок про те, що він насправді означає. Крім того, іноді люди реалізують спеціальні обробники помилок, які отримують клас повідомлення та номер повідомлення під час виконання. У цьому випадку помилку неможливо знайти автоматично або її неможливо знайти в тому місці, де помилка фактично сталася. Вирішення першої проблеми полягає в тому, щоб звичка завжди додавати коментарі до вихідного коду, щоб повідомити читачеві, що означає повідомлення. Другий вирішується шляхом додавання мертвого коду, щоб переконатися, що функція автоматичного пошуку повідомлень працює. Приклад:

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

Але є деякі ситуації, коли це неможливо. Наприклад, є кілька інструментів моделювання бізнес-процесів на основі інтерфейсу, де ви можете налаштувати повідомлення про помилки, які відображатимуться при порушенні бізнес-правил. Реалізація цих інструментів повністю керована даними, тому ці помилки не відображатимуться у списку, де використовуються. Це означає, що занадто багато покладатися на список, де використовується, коли намагаються знайти причину помилки, може бути червона оселедець.


Каталоги повідомлень також деякий час були частиною GNU / Linux - і, як правило, UNIX як стандарт POSIX .
єпископ

@bishop Я зазвичай не програмую спеціально для POSIX систем, тому я не знайомий з цим. Можливо, ви можете опублікувати ще одну відповідь, в якій пояснюються каталоги повідомлень POSIX та про те, що ОП може дізнатися від їх реалізації.
Філіп

3
Я був частиною проекту, який зробив це ще в оуті. Одне з проблем, з яким ми стикалися, - це те, що поряд із усім іншим ми поміщаємо людське повідомлення про те, що "не вдалося підключитися до бази даних" у базі даних.
JimmyJames

5

Проблема такого підходу полягає в тому, що він призводить до все детальнішого ведення журналу. 99,9999% з яких ви ніколи не подивитеся.

Натомість я рекомендую зафіксувати стан на початку вашого процесу та його успіх / невдачу.

Це дозволяє відтворювати помилку локально, переглядаючи код і обмежує вхід у систему на два місця за процес. напр.

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

Тепер я можу використовувати такий самий стан на своїй машині розробки, щоб відтворити помилку, перебираючи код у моїй налагоджувальній машині та записуючи новий тест модуля для підтвердження виправлення.

Крім того, я можу, якщо потрібно, уникати більшої кількості журналів лише помилками журналу або збереженням стану в іншому місці (база даних? Черга повідомлень?)

Очевидно, що ми повинні бути дуже обережними щодо реєстрації конфіденційних даних. Тож це особливо добре спрацьовує, якщо ваше рішення використовує черги повідомлень чи шаблон магазину подій. Оскільки в журналі потрібно сказати лише "Повідомлення xyz не вдалося"


Якщо розмістити в черзі чутливі дані, це все ще реєструється. Це недоцільно, подібно до того, як зберігати в БД чутливі входи без якоїсь форми криптографії.
jpmc26

якщо у вашій системі вичерпуються черги або db, тоді дані вже є, а також безпека. Занадто велика кількість журналів - це лише погано, оскільки журнал, як правило, не входить у ваш контроль безпеки.
Еван

Правильно, але в цьому справа. Це недоцільно, оскільки ці дані зберігаються постійно і, як правило, повністю зрозумілими текстами. Для конфіденційних даних краще просто не ризикувати і мінімізувати, де ви зберігаєте цей період, а потім бути дуже обізнаним і дуже уважним, як ви їх зберігаєте.
jpmc26

Він традиційно постійний, оскільки ви пишете у файл. Але черга помилок є тимчасовою.
Еван

Я б сказав, що, ймовірно, залежить від реалізації (а можливо, навіть від налаштувань) черги. Ви не можете просто скинути її в будь-яку чергу і очікувати, що вона буде безпечною. А що станеться після того, як черга споживається? Журнали все ще повинні бути десь, щоб хтось їх переглянув. Крім того, це не додатковий вектор атаки, який я хотів би відкрити навіть тимчасово. Якщо в результаті атаки виявиться, що там є чутливі дані, навіть найсвіжіші записи можуть бути цінними. І тоді є ризик того, що хтось не пізнає і переключить перемикач, щоб він також почав входити на диск. Це просто банка глистів.
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 може бути нульовим при відновленні, і це нормально, нам потрібно виправити викликові сайти:

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 і ви не можете впоратися з тим, що його немає, тоді, здається, краще вийти з ладу, ніж намагатися продовжувати шлях, який може пошкодити вашу систему.


0

Власні бібліотеки журналів дійсно забезпечують механізми розширення, тож якщо ви хочете знати метод, звідки виникло повідомлення журналу, вони можуть це робити поза вікном. Це має вплив на виконання, оскільки процес вимагає генерування сліду стека та проходження його, поки ви не виходите з бібліотеки журналів.

При цьому, це дійсно залежить від того, що ви хочете зробити для себе вашим посвідченням особи:

  • Пов’яжіть повідомлення про помилки, надані користувачеві вашим журналам?
  • Укажіть позначення того, який код виконувався під час генерації повідомлення?
  • Слідкуйте за назвою машини та примірником служби?
  • Слідкуйте за ідентифікатором потоку?

Усі ці речі можна зробити з вікна за допомогою відповідного програмного забезпечення для ведення журналів (тобто немає Console.WriteLine()або Debug.WriteLine()).

Особисто важливіше - це можливість реконструювати шляхи виконання. Ось для чого створені такі інструменти, як Zipkin . Один ідентифікатор для відстеження поведінки однієї дії користувача у всій системі. Помістивши ваші журнали в центральну пошукову систему, ви зможете не тільки знайти найдовші дії, але й викликати журнали, які застосовуються до тієї самої дії (як стек ELK ).

Непрозорі ідентифікатори, які змінюються з кожним повідомленням, не дуже корисні. Послідовний ідентифікатор, який використовується для відстеження поведінки через цілий набір мікросервісів ... надзвичайно корисний.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.