Membuat kode dapat ditemukan dengan menggunakan ID pesan unik global

39

Pola umum untuk menemukan bug mengikuti skrip ini:

  1. Amati keanehan, misalnya, tidak ada output atau program menggantung.
  2. Temukan pesan yang relevan di log atau output program, misalnya, "Tidak dapat menemukan Foo". (Berikut ini hanya relevan jika ini adalah jalur yang diambil untuk menemukan bug. Jika tumpukan jejak atau informasi debug lainnya tersedia, itu cerita lain.)
  3. Temukan kode tempat pesan dicetak.
  4. Debug kode antara tempat pertama Foo masuk (atau harus masuk) gambar dan di mana pesan dicetak.

Langkah ketiga adalah tempat proses debugging sering terhenti karena ada banyak tempat dalam kode di mana "Tidak dapat menemukan Foo" (atau string templated Could not find {name}) dicetak. Bahkan, beberapa kali kesalahan ejaan membantu saya menemukan lokasi sebenarnya jauh lebih cepat daripada yang saya lakukan sebelumnya - itu membuat pesan unik di seluruh sistem dan sering di seluruh dunia, sehingga mesin pencari yang relevan langsung mengenai.

Kesimpulan yang jelas dari ini adalah bahwa kita harus menggunakan ID pesan unik secara global dalam kode, mengkodekannya sebagai bagian dari string pesan, dan mungkin memverifikasi bahwa hanya ada satu kemunculan setiap ID dalam basis kode. Dalam hal pemeliharaan, apa yang menurut komunitas ini merupakan pro dan kontra yang paling penting dari pendekatan ini, dan bagaimana Anda akan menerapkan ini atau memastikan bahwa penerapannya tidak pernah diperlukan (dengan asumsi bahwa perangkat lunak akan selalu memiliki bug)?

l0b0
sumber
54
Manfaatkan jejak tumpukan Anda sebagai gantinya. Jejak tumpukan tidak hanya akan memberi tahu Anda dengan tepat di mana kesalahan terjadi, tetapi juga setiap fungsi yang memanggil setiap fungsi yang menyebutnya. Catat seluruh jejak saat pengecualian terjadi, jika perlu. Jika Anda menggunakan bahasa yang tidak memiliki pengecualian, seperti C, itu cerita yang berbeda.
Robert Harvey
6
@ l0b0 saran kecil tentang kata-kata. "Apa yang dipikirkan komunitas ini ... pro dan kontra" adalah frasa yang mungkin dianggap terlalu luas. Ini adalah situs yang memungkinkan untuk pertanyaan "subyektif yang baik", dan sebagai imbalan untuk memungkinkan jenis pertanyaan ini, Anda sebagai OP diharapkan untuk melakukan pekerjaan "penggembalaan" komentar dan jawaban menuju konsensus yang bermakna.
rwong
@ rwong Terima kasih! Saya merasa bahwa pertanyaannya telah menerima tanggapan yang sangat baik dan langsung, meskipun ini mungkin lebih baik ditanyakan di forum. Saya menarik kembali tanggapan saya terhadap komentar Robert Harvey setelah membaca tanggapan klarifikasi JohnWu, jika itu yang Anda maksud. Jika tidak, apakah Anda memiliki kiat penggembalaan khusus?
l0b0
1
Pesan saya terlihat seperti "Tidak dapat menemukan Foo saat panggilan ke bilah ()". Masalah terpecahkan. Mengangkat bahu. Kelemahannya adalah sedikit bocor untuk dilihat oleh pelanggan, tetapi kami cenderung menyembunyikan detail pesan kesalahan dari mereka, membuatnya hanya tersedia untuk sysadmin yang tidak bisa memberikan monyet sehingga mereka bisa melihat beberapa nama fungsi. Jika gagal, ya, ID / kode unik kecil yang bagus akan melakukan triknya.
Lightness Races dengan Monica
1
Ini SANGAT berguna ketika pelanggan menelepon Anda, dan komputer mereka tidak berjalan dalam bahasa Inggris! Apalagi masalah hari ini karena kita sekarang memiliki file email dan log .....
Ian

Jawaban:

12

Secara keseluruhan ini adalah strategi yang valid dan berharga. Inilah beberapa pemikiran.

Strategi ini juga dikenal sebagai "telemetri" dalam arti bahwa ketika semua informasi tersebut digabungkan, mereka membantu "melakukan pelacakan" jejak eksekusi dan memungkinkan pemecah masalah untuk memahami apa yang coba dicapai oleh pengguna / aplikasi, dan apa yang sebenarnya terjadi .

Beberapa data penting yang harus dikumpulkan (yang kita semua tahu) adalah:

  • Lokasi kode, yaitu tumpukan panggilan dan garis perkiraan kode
    • "Perkiraan garis kode" tidak diperlukan jika fungsi didekomposisi menjadi unit kecil yang sesuai.
  • Setiap potongan data yang berkaitan dengan keberhasilan / kegagalan fungsi
  • "Perintah" tingkat tinggi yang dapat menentukan apa yang coba dicapai oleh pengguna manusia / agen eksternal / pengguna API.
    • Idenya adalah bahwa perangkat lunak akan menerima dan memproses perintah yang datang dari suatu tempat.
    • Selama proses ini, puluhan hingga ratusan hingga ribuan panggilan fungsi mungkin telah terjadi.
    • Kami ingin agar telemetri yang dihasilkan selama proses ini dapat dilacak kembali ke perintah tingkat tertinggi yang memicu proses ini.
    • Untuk sistem berbasis web, permintaan HTTP asli dan datanya akan menjadi contoh dari "informasi permintaan tingkat tinggi"
    • Untuk sistem GUI, pengguna mengklik sesuatu akan cocok dengan deskripsi ini.

Seringkali, pendekatan logging tradisional gagal, karena kegagalan untuk melacak pesan log tingkat rendah kembali ke perintah tingkat tertinggi yang memicu itu. Jejak tumpukan hanya menangkap nama-nama fungsi yang lebih tinggi yang membantu menangani perintah tingkat tertinggi, bukan rincian (data) yang kadang-kadang diperlukan untuk mengkarakterisasi perintah itu.

Biasanya perangkat lunak tidak ditulis untuk mengimplementasikan persyaratan keterlacakan semacam ini. Ini membuat menghubungkan pesan tingkat rendah ke perintah tingkat tinggi menjadi lebih sulit. Masalahnya terutama lebih buruk dalam sistem multi-utas secara bebas, di mana banyak permintaan dan respons dapat tumpang tindih, dan pemrosesan mungkin diturunkan ke utas yang berbeda dari utas penerima-permintaan semula.

Dengan demikian, untuk mendapatkan nilai maksimal dari telemetri, perubahan arsitektur perangkat lunak secara keseluruhan akan diperlukan. Sebagian besar antarmuka dan panggilan fungsi perlu dimodifikasi untuk menerima dan menyebarkan argumen "pelacak".

Bahkan fungsi utilitas perlu menambahkan argumen "pelacak", sehingga jika gagal, pesan log akan membiarkan dirinya dikorelasikan dengan perintah tingkat tinggi tertentu.

Kegagalan lain yang akan membuat penelusuran telemetri menjadi sulit adalah referensi objek yang hilang (null pointer atau referensi). Ketika sebagian data penting hilang, mungkin mustahil melaporkan apa pun yang berguna untuk kegagalan tersebut.

Dalam hal menulis pesan log:

  • Beberapa proyek perangkat lunak mungkin memerlukan pelokalan (terjemahan ke bahasa asing) bahkan untuk pesan log yang hanya ditujukan untuk administrator.
  • Beberapa proyek perangkat lunak mungkin memerlukan pemisahan yang jelas antara data sensitif dan data tidak sensitif, bahkan untuk tujuan logging, dan bahwa administrator tidak akan memiliki kesempatan untuk secara tidak sengaja melihat data sensitif tertentu.
  • Jangan mencoba mengaburkan pesan kesalahan. Itu akan merusak kepercayaan pelanggan. Administrator pelanggan berharap untuk membaca log tersebut dan memahami itu. Jangan membuat mereka merasa bahwa ada beberapa rahasia hak milik yang harus disembunyikan dari administrator pelanggan.
  • Berharap bahwa pelanggan akan membawa sepotong log telemetri dan memanggang staf dukungan teknis Anda. Mereka berharap tahu. Latih staf dukungan teknis Anda untuk menjelaskan log telemetri dengan benar.
rwong
sumber
1
Memang, AOP telah menggembar-gemborkan, terutama, kemampuan yang melekat untuk memecahkan masalah ini - menambahkan Tracer untuk setiap panggilan yang relevan - dengan invasi minimal ke basis kode.
Uskup
Saya juga akan menambahkan ke daftar "menulis pesan log" yang penting untuk menggambarkan kegagalan dalam hal "mengapa" dan "bagaimana cara memperbaikinya" alih-alih hanya "apa" yang terjadi.
Uskup
58

Bayangkan Anda memiliki fungsi utilitas sepele yang digunakan di ratusan tempat dalam kode Anda:

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

Jika kami melakukan seperti yang Anda sarankan, kami mungkin menulis

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

Kesalahan yang bisa terjadi adalah jika inputnya nol; ini akan menghasilkan pembagian dengan pengecualian nol.

Jadi katakanlah Anda melihat 27349262 dalam output atau log Anda. Di mana Anda melihat untuk menemukan kode yang melewati nilai nol? Ingat, function-- dengan ID uniknya-- digunakan di ratusan tempat. Jadi Anda sementara Anda tahu bahwa pembagian dengan nol terjadi, Anda tidak tahu siapa 0itu.

Menurut saya jika Anda akan repot-repot mencatat ID pesan, Anda sebaiknya juga mencatat jejak stack.

Jika verbosity dari jejak stack adalah apa yang mengganggu Anda, Anda tidak perlu membuangnya sebagai string seperti yang diberikan runtime kepada Anda. Anda dapat menyesuaikannya. Misalnya, jika Anda ingin jejak stack disingkat hanya pergi ke nlevel, Anda bisa menulis sesuatu seperti ini (jika Anda menggunakan 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()
        );
    }
}

Dan gunakan seperti ini:

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

Keluaran:

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

Mungkin lebih mudah daripada mempertahankan ID pesan, dan lebih fleksibel.

Curi kode saya dari DotNetFiddle

John Wu
sumber
32
Hmm, kurasa aku tidak cukup menjelaskan maksudku. Saya tahu mereka unik Robert - per lokasi kode . Mereka tidak unik per jalur kode . Mengetahui lokasi seringkali tidak berguna, misalnya jika masalah sebenarnya adalah input tidak diatur dengan benar. Saya sedikit mengedit bahasa saya untuk menekankan.
John Wu
1
Poin bagus, kalian berdua. Ada masalah yang berbeda dengan jejak tumpukan, yang mungkin atau tidak mungkin menjadi pemecah kesepakatan tergantung pada situasinya: Ukurannya dapat mengakibatkan mereka membanjiri pesan, terutama jika Anda ingin menyertakan seluruh jejak tumpukan daripada versi yang diperpendek seperti beberapa bahasa lakukan secara default. Mungkin alternatifnya adalah menulis log jejak tumpukan secara terpisah dan memasukkan indeks bernomor ke log itu dalam output aplikasi.
l0b0
12
Jika Anda mendapatkan banyak dari ini sehingga Anda khawatir akan membanjiri I / O Anda, ada sesuatu yang sangat salah. Atau Anda hanya pelit? Hit performa nyata mungkin adalah tumpukan bersantai.
John Wu
9
Diedit dengan solusi untuk memperpendek jejak tumpukan, jika Anda menulis log ke floppy 3,5;)
John Wu
7
@ JohnWu Dan juga jangan lupa "IOException 'File not Found' at [...]" yang memberi tahu Anda tentang lima puluh lapisan tumpukan panggilan tetapi tidak memberi tahu file berdarah apa yang sebenarnya tidak ditemukan.
Joker_vD
6

SAP NetWeaver melakukan ini selama beberapa dekade.

Ini telah terbukti menjadi alat yang berharga ketika kesalahan pemecahan masalah dalam raksasa kode raksasa yang merupakan sistem SAP ERP khas.

Pesan kesalahan dikelola dalam repositori pusat tempat setiap pesan diidentifikasi berdasarkan kelas pesan dan nomor pesannya.

Saat Anda ingin mengeluarkan pesan kesalahan, Anda hanya menyatakan variabel kelas, jumlah, tingkat keparahan dan spesifik pesan. Representasi teks dari pesan dibuat saat runtime. Anda biasanya melihat kelas dan nomor pesan dalam konteks apa pun di mana pesan muncul. Ini memiliki beberapa efek rapi:

  • Anda dapat secara otomatis menemukan baris kode apa pun di basis kode ABAP yang membuat pesan kesalahan tertentu.

  • Anda dapat mengatur breakpoint debugger dinamis yang memicu ketika pesan kesalahan tertentu dihasilkan.

  • Anda dapat mencari kesalahan dalam artikel basis pengetahuan SAP dan mendapatkan hasil pencarian yang lebih relevan daripada jika Anda mencari "Tidak dapat menemukan Foo".

  • Representasi teks dari pesan dapat diterjemahkan. Jadi dengan mendorong penggunaan pesan, bukan string, Anda juga mendapatkan kemampuan i18n.

Contoh sembulan galat dengan nomor pesan:

error1

Mencari kesalahan itu di repositori kesalahan:

error2

Temukan di basis kode:

error3

Namun, ada kekurangannya. Seperti yang Anda lihat, baris kode ini tidak mendokumentasikan diri sendiri lagi. Saat Anda membaca kode sumber dan melihat MESSAGEpernyataan seperti yang ada di tangkapan layar di atas, Anda hanya dapat menyimpulkan dari konteks apa arti sebenarnya. Juga, terkadang orang menerapkan penangan kesalahan khusus yang menerima kelas dan nomor pesan pada saat runtime. Dalam hal ini kesalahan tidak dapat ditemukan secara otomatis atau tidak dapat ditemukan di lokasi di mana kesalahan terjadi. Solusi untuk masalah pertama adalah membuatnya menjadi kebiasaan untuk selalu menambahkan komentar dalam kode sumber memberitahu pembaca apa arti pesan tersebut. Yang kedua diselesaikan dengan menambahkan beberapa kode mati untuk memastikan pencarian pesan otomatis berfungsi. Contoh:

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

Tetapi ada beberapa situasi di mana ini tidak mungkin. Misalnya ada beberapa alat pemodelan proses bisnis berbasis UI di mana Anda dapat mengonfigurasi pesan kesalahan agar muncul ketika aturan bisnis dilanggar. Implementasi alat-alat itu sepenuhnya didorong oleh data, sehingga kesalahan ini tidak akan muncul dalam daftar tempat-digunakan. Itu berarti terlalu mengandalkan daftar tempat digunakan ketika mencoba menemukan penyebab kesalahan bisa menjadi ikan haring merah.

Philipp
sumber
Katalog pesan juga telah menjadi bagian dari GNU / Linux - dan UNIX umumnya sebagai standar POSIX - untuk beberapa waktu.
Uskup
@ bishop Saya biasanya tidak pemrograman khusus untuk sistem POSIX, jadi saya tidak terbiasa dengannya. Mungkin Anda bisa memposting jawaban lain yang menjelaskan katalog pesan POSIX dan apa yang bisa dipelajari OP dari implementasinya.
Philipp
3
Saya adalah bagian dari proyek yang melakukan ini di masa lalu. Satu masalah yang kami hadapi adalah, bersama dengan semua yang lain, kami menempatkan pesan manusia untuk "tidak dapat terhubung ke database" dalam database.
JimmyJames
5

Masalah dengan pendekatan itu adalah bahwa itu mengarah ke penebangan yang lebih rinci. 99,9999% darinya Anda tidak akan pernah melihatnya.

Sebagai gantinya, saya sarankan menangkap negara pada awal proses Anda dan keberhasilan / kegagalan proses.

Ini memungkinkan Anda untuk mereproduksi bug secara lokal, melangkah melalui kode dan membatasi login Anda ke dua tempat per proses. misalnya.

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

Sekarang saya dapat menggunakan kondisi yang sama persis pada mesin dev saya untuk mereproduksi kesalahan, melangkah melalui kode di debugger saya dan menulis unit test baru untuk mengkonfirmasi perbaikan.

Selain itu, saya dapat jika diminta menghindari lebih banyak pencatatan dengan hanya mencatat kegagalan atau mempertahankan status di tempat lain (basis data? Antrian pesan?)

Jelas kami harus ekstra hati-hati dalam mencatat data sensitif. Jadi ini berfungsi dengan baik jika solusi Anda menggunakan antrian pesan atau pola penyimpanan acara. Sebagai log hanya perlu mengatakan "Pesan xyz Gagal"

Ewan
sumber
Menempatkan data sensitif dalam antrian masih mencatatnya. Ini keliru, sama seperti menyimpan input sensitif dalam DB tanpa beberapa bentuk kriptografi.
jpmc26
jika sistem Anda kehabisan antrian atau db maka data sudah ada di sana, dan demikian pula keamanannya. Terlalu banyak log hanya buruk karena log cenderung berada di luar kendali keamanan Anda.
Ewan
Benar, tapi itu intinya. Sangat tidak disarankan karena data tetap ada secara permanen dan biasanya dalam teks yang benar-benar jelas. Untuk data sensitif, lebih baik tidak mengambil risiko dan meminimalkan di mana Anda menyimpannya, dan kemudian menjadi sangat sadar dan sangat berhati-hati tentang bagaimana Anda menyimpannya.
jpmc26
Secara tradisional permanen karena Anda menulis ke file. Tetapi antrian kesalahan bersifat sementara.
Ewan
Saya akan mengatakan bahwa mungkin tergantung pada implementasi (dan mungkin bahkan pengaturan) dari antrian. Anda tidak bisa membuangnya dalam antrian apa pun dan mengharapkannya aman. Dan apa yang terjadi setelah antrian dikonsumsi? Log harus tetap berada di suatu tempat bagi seseorang untuk dilihat. Selain itu, itu bukan vektor serangan ekstra yang ingin saya buka sementara waktu. Jika serangan menemukan ada data sensitif yang masuk ke sana, bahkan entri terbaru mungkin berharga. Dan kemudian ada risiko seseorang tidak mengetahui dan membalik sakelar sehingga mulai masuk ke disk juga. Itu hanya sekaleng cacing.
jpmc26
1

Saya akan menyarankan bahwa logging bukan cara untuk menyelesaikan hal ini, melainkan bahwa keadaan ini dianggap luar biasa (ini mengunci program Anda) dan pengecualian harus dilemparkan. Katakan kode Anda:

public Foo GetFoo() {

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

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

     return aFoo;
}

Sepertinya Anda memanggil kode tidak diatur untuk berurusan dengan fakta bahwa Foo tidak ada dan Anda mungkin berpotensi:

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

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

     return aFoo;
}

Dan ini akan mengembalikan jejak stack bersama dengan pengecualian yang dapat digunakan untuk membantu debugging.

Sebagai alternatif, jika kami berharap bahwa Foo dapat menjadi nol ketika diambil kembali dan itu baik-baik saja, kami perlu memperbaiki situs yang memanggil:

void DoSomeFoo(Foo aFoo) {

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

    ... operations on Foo...
}

Fakta bahwa perangkat lunak Anda hang atau bertindak 'aneh' dalam keadaan yang tidak terduga tampaknya salah bagi saya - jika Anda memerlukan Foo dan tidak dapat menanganinya tidak ada di sana, maka tampaknya lebih baik untuk crash daripada mencoba melanjutkan sepanjang jalan yang mungkin merusak sistem Anda.

Padi
sumber
0

Pustaka logging yang benar menyediakan mekanisme ekstensi, jadi jika Anda ingin mengetahui metode asal pesan log, mereka bisa melakukannya di luar kotak. Itu memang berdampak pada eksekusi karena proses ini membutuhkan menghasilkan jejak stack dan melewatinya sampai Anda keluar dari perpustakaan logging.

Yang mengatakan, itu benar-benar tergantung pada apa yang ingin ID Anda lakukan untuk Anda:

  • Mengkorelasikan pesan kesalahan yang diberikan kepada pengguna ke log Anda?
  • Berikan notasi tentang kode apa yang dieksekusi ketika pesan dibuat?
  • Melacak contoh nama mesin dan layanan?
  • Melacak id utas?

Semua hal ini dapat dilakukan di luar kotak dengan perangkat lunak logging yang tepat (yaitu tidak Console.WriteLine()atau Debug.WriteLine()).

Secara pribadi, yang lebih penting adalah kemampuan merekonstruksi jalur eksekusi. Itulah yang ingin dicapai oleh alat seperti Zipkin . Satu ID untuk melacak perilaku satu tindakan pengguna di seluruh sistem. Dengan meletakkan log Anda di mesin pencari pusat, Anda tidak hanya dapat menemukan tindakan berjalan terpanjang, tetapi memanggil log yang berlaku untuk satu tindakan (seperti tumpukan ELK ).

ID buram yang berubah dengan setiap pesan tidak terlalu berguna. ID yang konsisten digunakan untuk melacak perilaku melalui seluruh rangkaian layanan ... sangat berguna.

Berin Loritsch
sumber