You say “cave dweller debugging”, I say debug logging
sicpers.info
sicpers.info
But the real problem, and top-level issue, is that you are constantly rooting around the very lowest level of your code, and it is difficult and labor-intensive to get a higher level view of what's going on.
On the other hand, well-designed logging code is there when you need it, can provide a view at any level of abstraction that you'd like (if you've written the logging code), and allows you to go forward and backward in time easily and repeatedly.
The project that really pushed me in this direction was working on a cluster. My piece of the system would start on one of the nodes, and delegate work to a fixed number of other nodes, or to all nodes. Out of necessity, I put a lot of work into debugging code, taking care to include the right information in each line (node id, time to nsec, source id, line number), carefully formatted to enable sorting and filtering, log rotation to make sure that I could focus on recent events or a longer timescale, and a lot of attention to the information included in each line and how it was formatted.
Because this was a long-running distributed system, I would often need to gather logs from across the cluster, going back hours or days, and merge and sort the files, and then debugging could start.
Going along with logging was heavy use of assertions, which were enabled even in production. I definitely did not want my code going forward doing random unanticipated crazy things past the point of an assertion failure.
So? What do you do? Logging at the right level + turn up logging. Metrics - proper metrics. Core dumps. Debugging turns into an exercise of grepping/inspecting the logs/metrics and/or working with the dump. Anyone that tells you differently is massaging the truth.
For debugging multithreaded code, you can run a gdb script in batch/non-interactive mode, and direct the gdb output to some out-of-band channel, like a file or other terminal session.
For debugging JNI, you can try running Java under gdb and set a breakpoint on the extension library entry function before it’s loaded.
I don't understand the extreme attachment to interactive debugging that I see in some of the comments here. It's like people are so attached to this idea that they will do anything to make it work as each complication is added.
Furthermore, the amount of attention and manual labor needed to use a debugger effectively is just staggering. One mistake, e.g. you step over a function you should have stepped into, and you've got to start over.
Or you could just log.
Yes, writing logging code is no fun. But you don't even have to do it all at once. In fact, you probably shouldn't. I often found that I didn't know what to log until I had to investigate some problems turned up in integration testing. So I added logging to solve the problem, and then benefitted from it forever.
But I am reminded of the diddy: "If a system has a Powerful Debugger, what does that say about the apparent need for a Powerful Debugger?" --- I read that as: if you have a kick-a* debugger, you have a really complicated system. Maybe the answer is to make the system less complicated.
Extracting simplicity from complexity is hard, and that is why most systems are unnecessarily complex. And I mean: 'unnecessarily' in that there exists simpler systems that perform the same function with less complexity.
I'm late-stage career here, so I've seen (most of) it all. There seems to be additional complexity piled on 'just because' so often. Occasionally, I see a system and I can't think of a simpler way to do it. That's rare. I bet it's rare for you, too.
Not with rr (rr-project.org). With rr you can step right back where you were. rr also lets you debug after the fact without interrupting your program, and lets you collect a recording on a cluster machine and then debug the recording elsewhere.
How so? Can't you just attach to each process? Do you mean at production scale?
You could, I suppose, but I'm not sure why you would want to. Going back and forth to two debuggers (at least) seems like torture. And with multiple processes, timing and synchronization issues could make interactive debugging a real nightmare. Why? Why would you do this?
Interactive debugging doesn't scale in any dimension.
All the usual problems, but doubled. Oops, I stepped too far, start over. Oops, the other one stepped too far, start over. Oops, start over, oops, start over.
Never again.
I start with basic lifecycle logging, at an INFO level: The FooBar is created, the FooBar is destroyed, and major states in between. If a FooBar manages a set of things, then I might also add DEBUG level logging for operations on those things. But mostly, detailed logging comes later, as I investigate problems. You really can't anticipate too much logging, because you don't know what's going to need it.
Debugging a show-stopper synchronization flaw in the bootstrapping of a parallel job, I had to tell the job system to launch each of 64 nodes wrapped in gdb wrapped in xterm with remote X display back to a laptop. It was something that had "worked in test" reliably, but that was always on a smaller number of nodes or simulating a larger number with time-sharing. It seemed to need real parallel hardware allocations to show up.
So, I was able to launch the job, allow all nodes to run until distributed deadlock, then interrupt and show all thread stacks on one debugger after another until I found the odd process which was out of phase with the rest. As soon as I saw the stacks, I had no further need for the debuggers. Just knowing the "impossible" state configuration happened was the necessary clue.
I have seen lots of satisfactory use of logs for diagnostics, but this is a case where I think the debuggers were almost essential. The debuggers gave a distributed state snapshot due to the deadlock-induced quiescence of the whole system. To have logs show the same snapshot would require a perfect arrangement of unbuffered logging so that the final state would be visible and not caught up in RAM of some or all of the stalled processes.
I've used that a lot black-box debugging java processes in production.
I also wrote such a signal handler for a ruby app.
There are some of cute hacks to get a remote debugger which I haven't used in years.
https://github.com/sassoftware/epdb can start and connect to a remote debug instance
But worse, we were debugging our library linked into someone else's application. So, we would have needed their cooperation to add signal handlers. And, we naively thought our library had already been sufficiently tested and did not anticipate the need for last-minute debugging when our user moved their application to a different computing resource that was not available during prior months of preparation and prototypes.
So, the ugly app-in-gdb-in-xterm with X over Internet addressed all that with adhoc instrumentation and communications that could be folded into the existing parallel, distributed job on short notice.
This was also in the days of pthreads in C. The other valuable use of gdb I recall was for memory watchpoints to help track down unexpected changes to certain data. The other tool we got a lot of use out of in those days was Purify to help audit for use of uninitialized memory and for memory leaks.
I was working on a cluster and often needed to do the same thing on all the nodes, run some database query or os command, and gather and integrate the results. I wrote yet another Python stream-objects-instead-of-strings shell (https://geophile.com/osh), which included features for distribution: Run the same command on all nodes, bring back the results, merge streams, gather files, distribute files, etc.
That was several years ago, and it wasn't a full shell. Since then, I've done an improved system that actually is a shell (https://marceltheshell.org, https://geophile.com/marcel).
With multiple processes or machines, it's helpful to pass around a trace ID and then have all the debug logs print the trace ID + timestamp at each event. This is a rather manual process; I wish there were automatic ways of doing this. At Google, the best we get is filtering logs by the RPC being processed at the time; but this ends at a particular process. Cross-process RPC traces are mostly sampled "stack traces"; you don't get to inject custom debug messages in those.
With debug symbols you can even pinpoint those events into code.
The only competition I know to these workflows is using Java alongside Flight Recorder.
Luckily its not performance-oriented but more reliability-oriented so I can get away with more logging than usually necessary. It seems obsolete and over-the-top till it saves everyone's a*
That tells me where I need to add metrics, logging, etc for the distributed deploy.
Bugs and logs and testing are all related. You obviously (should) do unit testing, integration testing, system testing before production. As you do these things, you will find and fix bugs. Ideally, you would add logging to help you diagnose the bugs you are fixing. Congratulations, you now have logging in code known to have been buggy, which will serve you well in production if bugs not caught by your tests turn up.
But bugs in previously reliable code do slip through, and this gives rise to the need for previously unanticipated logging. If either of these things happens a lot, the problem is your testing leading up to production. In other words, if you often find yourself needing dynamically deployed logging, then the lack of this feature is not the problem you should be paying attention to.
Also, dynamically deployed logging is basically shipping untested code, no?
This is painful to do just to add a log which might not include all the information I need.
It also comes with PII reduction and block lists so you won't just log user passwords in the login code...
This solves the problem of over-logging: https://www.reddit.com/r/devops/comments/udgohy/there_is_no_...
Obviously, depending on the business and maturity level of the company, this could be a big no-no. In general, I find those "wild west" environments more fun. Waiting for an hour long build, deploys, and "approvals" to add a couple of prints is a big drag.
These are true of plain-old-gdb but they're not inherent to interactive debugging. rr and Pernosco address most of these issues and with more work we could do even better.
In particular: rr is great for debugging multithreaded code; record a race once and then debug the replay as much as you want until you've figured it out. rr will also record multiprocess code and let you debug whichever process you want; Pernosco goes further and supports seamless debugging across all processes. https://pernos.co/about/javascript/ shows how Pernosco can support debugging high-level code (JS) and C++ code at the same time. rr can record processes on multiple machines; we don't have an integrated debugging experience for distributed systems yet, but it's not hard to see how that could be done.
> But the real problem, and top-level issue, is that you are constantly rooting around the very lowest level of your code, and it is difficult and labor-intensive to get a higher level view of what's going on.
That depends on the debugger. Interactive debuggers can do higher-level things if we want them to, e.g https://pernos.co/about/expressions/
Although I'm not a full time developer, I do some small coding and I'm also sort of responsible for the dev team. I rarely use a debugger. I do most of debugging through logging. There are 2 advantages that I see:
- it forces me to add the logs where they are needed to understand the flow of the application
- it forces me to make those logs actually usable, readable and understandable
Developer's first response on a bug report is - give me logs. But I don't have them, because they were not coded. Or they are unusable because they are not logging the actual problem. Developer then insists the problem is fine because he can't replicate it in his debug environment instead of figuring out the issue from the logs. Making logs during development also forces developers to make a conscious effort of finding a balance of what and when to log something, create proper debug levels not to overwhelm the logs, while still providing some useful information...The need for this balance is why I don't completely agree with the article.
If I'm debugging something with log messages, I put way more detail into it than I want to see in production even at LogLevel.DEBUG.
Maybe I'm just bad at it.
As I write this I realise that the "right" solution is that my IDE also folds away log calls of below some settable priority... Then I need a linter to prevent any code with side effects from running in a log line... Then I need to work in a language where that kind of static analysis is definitely possible... Maybe it's not so simple.
I mean, there is LogLevel.TRACE when you need it. For those not familiar, this level is exactly for “I’m writing new buggy code where anything can go wrong so stupidest details matter”-time. You can use it to separate normal debug logging like “request body is {…}” from sandbox logging like “loop counter just became [object Object]”.
The stages of understanding go like this, in my experience:
1. Log nothing.
2. Upon being told to log things, write mostly useless logs: "Entering f() function", "leaving f() function". When f() fails, who knows what happened?
3. Upon being taught to add context so that logs can actually be used to troubleshoot problems, "entering f() with id 123", "id is 123", "writing 123", etc.
4. Enlightenment usually comes when logs include necessary info but are not redundant, are not noisy, and can actually be used to track down problems. Of course, improvement at step 4 is always ongoing, for all of us.
They experience the problem with their own logging and realize through debugging their code what information they really wanted all along.
However, in development environments you would be foolish not to use the (vastly superior) tooling available.
I think many developers shy away from debuggers simply because they haven't taken the time to understand how to use them effectively, whereas everyone knows how to write a log statement. In previous roles I have given guidance on usage to fellow team members. It makes a vast difference on the resolution time of reported issues.
The debugger being vastly superior is highly dependant on what environment you are running in.
Until and unless such a thing appears, debug logging is pretty much what I've got.
(And given the complexity of the project in question, and the fact that I'm basically its only developer, debug logging is still pretty vital for running down the bugs the users run into in production. Good thing it's just a free hobby project!)
Fast, lightweight logging can be a necessary tool.
A) When developing for a microcontroller (however large or small). Stepping debuggers are limited, and sometimes unavailable at all. Most are clunky. ARM is the best here.
B) Running real time things. E.g. you can't step through a Bluetooth comms process because the moment you hit a breakpoint you aren't servicing a link and everything falls over.
There are some good tools like reverse time debuggers (e.g. https://undo.io/) that deal with the real time thing, but nothing deals with the combination. Plus a well maintained set of log outputs often tells you exactly what went wrong with just a cursory glance through the logs.
C) debugging signal handlers is somewhere between tricky and impossible depending on the scenario because of how debuggers work.
D) debugging optimized code. -O2 or -O3 code will have everything executing out of order, all variables are optimizes out, everything is inlined, etc.
For some operations, running them in debug mode means they are 10 times slower, turning a 30sec operation (such as parsing big files into memory) into 5 minutes. If I'm relatively certain of what the problem is and I solve it in less than ~5 tries, I saved time.
You can also just import the debugger where you need to use it, in which case you shouldn't get any slowdown.
I don't know. Learning and using a debugger adds an additional layer of cognitive load. Instead of thinking about the bug, you're thinking about using the debugger and trying to think about the bug. I have found their value to be limited with the code I normally write.
My app formats the trace log as HTML tables, and uses color to signal errors or invalid cases. Makes debugging fun.
Also, if diagnosing a problem on a customer's inaccessible machine, a log is indispensable.
Tip: close & re-open the file for each write, so the last and most important tail end of the log doesn't vanish if the app crashes.
That seems very expensive. Why not just use line buffering or even flush from a signal handler?
It sounds like you're looking for fsync[0]
Imagine you develop in multiple environments and in prod costs of keeping logs can only let you have few percent of it. Guess what would be there? Right, most spammy message that no one of team leads would recognize as useful. Checking what actually is in logs in prod env is a hard work and usually no volunteers.
Logs can mask problems especially concurrency related. I dont want to give examples here to protect innocent but i can say in 20 years i have seen cases.
Logs can be huge pain in the neck of your performance analysis team. Especially if they have to use instrumented environment for their work instead of prod. Even "disabled" things that don't produce output might still be doing enough work to make any prod and qc environment results completely not comparable.
Logs can be pain in the neck of security and risk teams if you happen to put your code into untrusted hands. Logs often used for reverse engineering your smart solutions with ease. My particular experience with game clients is especially bad in this regard.
PS. My personal best logs in development let you track single element of data from a whole lot of others. It takes time and lots of work to design such logging in complex systems. I have not met many systems done like that. People tend to just spam everything and hope for the best with text filtering. Then the stories of crashing text editors while opening giga sized logs are born xD.
just my 2c.
Often I'll identify a wrong value, or bad object, and then search for that through the logs to see what is touching it or where it first appeared. If it's interacting with another value I'll search for that, maybe in a longer chain. Eventually you figure out where the interaction is going wrong, and more often than not it's a subtle error that looks right but isn't. You make a small change (the classic is an off-by-one error) and suddenly everything works perfectly.
It also depends on how many things you are interested in. If you care about, say, a complex object with many properties, then the interactivity of a debugger trumps logging.
I feel that it is the opposite. It is where the path is obscure that logging works really well. It can be iteratively refined and repeated.
FATAL = The system is basically non-functional, even in some partially-working way. Alerts fire.
ERROR = Something went wrong in some part of the system. The system may be mostly or partially working, but an important thing is broken and should be fixed. Depending on the where the error is, it may need to be immediately addressed or at least very soon.
WARNING = Something isn't working right, but it's possibly transient and the system has was a handling this via retry or other workarounds. This should be addressed at some point if possible, but shouldn't require immediate intervention.
INFO = This is the log level that the system is designed to run at. It provides enough human-readable context to see how the system is running. Most importantly, it provides just enough information provide context to WARNINGS/ERRORS/FATALS that may occur somewhere in a given request. The goal is to balance information w/performance.
DEBUG = All the things, all the time. Default mode for developers, and when diagnosing issues that require much more depth of information than provided at INFO level. It is VERY RARE to run at this level in production, as INFO logs should provide enough request context for a developer to reproduce most issues, but can be enabled for short bursts in production on one server in a cluster, or some other limited form to capture necessary information.
A guiding philosophy on all logs is to persist them in a central location (and locally w/rollover just in case persistence is broken), and is to flow some request or context ID through the system so logs from a single request can be easily identified and related log statements correlated with each other.
At my last startup, it was almost a year effort to clean up the logs. Such things like app pool crash on startup were logged at DEBUG, and the application starting successfully was logged at ERROR. Combined with an effort to improve overall quality, hit a first milestone of going 3 months without the ops team requiring escalation to developers outside of business hours (i.e. they could identify and fix things based on logs). Eventually, out of business hours alerts because a "I don't remember the last one" thing.
When I fix a problem gnarly enough to require lots of logging everywhere, once I've tidied up I write a detailed comment or other documentation, and/or use unit tests to make sure the problem stays solved. The logging code doesn't get committed, which also helps keep the fixed code clear in the diff.
For the kinds of programs I usually write, I've settled on only using printing/logging for major events and for things that aren't errors but might require developer attention.
On this particular aspect - I find that log lines can do double-duty as code comments. When I see some comment that explains the what-and-why of a section of code - "// We also need to check the Foo state for consistency before committing Bar" - I might actually change it to a log line like - "Verifying state of Foo ${foo.id} before committing Bar ${bar.id}".
The article says to use logging and not actual printf debugging, which solves this. If your logging framework is even somewhat capable and modern it understands all of:
- named priorities: I want that FATAL log highlighted, but I probably don't want the DEBUG log shown at all unless I am currently debugging this
- automatic categories: os.mutex.internal and account.forex.hack can be automatically distinguished in most modern languages by the fact they live in different objects / source code files / even in C we know what the name of the current function is and can at least annotate that in logs.
- how to send the log messages somewhere useful, hopefully at least files and some primitive syslog type protocol, maybe a lot more
In a nice shiny modern logging framework I'd expect to be provided with default annotations so that e.g. when I error.log("Can't enrol students on a module that doesn't exist") the framework annotates the log entry with: The HTTP request that got us here, including e.g. query parameters and date-time stamp, and the Module we were trying to call a method on when it blew up.
But I haven't found it very useful to barf lots of data into a debug log and then hope to find something useful sifting through it when there's a problem. Maybe it's just one too many information-dense things to juggle for me and smarter people have more luck with this approach. I'm not totally against the idea, I just haven't found a way that works for me.
I find that having lots of trace data is useful because at a glance, you can compare problem situations against normal profiles and patterns.
In an ideal world, without restart, an application would let me selectively switch on logging levels from "errors only" up to "line-level original source code in a targeted function" - with a trigger system.
Functionally basically a debugger, but coded into the binary so we don't pay the debugger cost (and also so we can try it more broadly across a cluster). This is the direction that Linux kernel eBPF debugging is really leading us, and what it should be easier to get that sort of power into our applications at a code-size but not speed cost - even if it means having our compilers just include multiple copies of the function with different logging-level functionality enabled.
However, one of the horrible habits it gave me that I absolutely adore now is having an interactive REPL for everything. If I want to understand anything inside my code, I can run just that little tiny bit, spit out the answers I need, and move forward. Half the time the best part was just having an open powershell session connected to the same env as the script. For me, it was the first time experiencing scripting with almost no downtime between iterations (most of the time) and the ability to inspect any/every object in play mostly seemessely. I moved on from Powershell to Python/Ruby, but I've basically gone all in on Ruby (devops, no one cares what I write code in) because I am just so much more productive with Pry and the other Ruby REPL options.
It seems to come back to bite me fairly often, but despite trying every ~12 months or so to learn a better way to work with the code, I never seem to find anything that sticks consistently. CodeRunner/random extensions in VS Code, REPLit.com, all the IDE's I could think of....no joy. REPLit comes extremely close, but its online only and that becomes an issue almost immediately. Still, REPLit is amazing when you just get pissed at a random python script that's spitting out junk and you want to figure out how a specific function works.
Maybe I'm missing something? Maybe I need to learn to code the "right" way? Maybe I'm just burned out? Idk, I will literally try any IDE/Tool that hints at giving me a better REPL experience or teaches me a better way to do it.
The original author's point about debug logging remains, however.
These are all tools in your toolbox that fit different circumstances differently. Challenge yourself to learn new tools.
How does Replit in particular help with this (vs anything else that lets you run Python, including /usr/bin/python?)
But one should be aware of its limits. Debugging often involves performing something like a binary search over a collection of suspicious assumptions. The first few well-placed debug-logging messages can save several iterations in these binary searches. But the cost of saving one more binary search iteration in this manner increases exponentially. As with so many other things in engineering, moderation is a virtue; you want good debug-logging and an ability to perform quick manual tweaks to the code to cover the "last mile" of a debugging session, when this is at all possible in your execution environment.
They are different tools for different use cases. Neither is better or worse. You can read the comments from detractors and proponents of the two tools to understand when to use them.
Dynamic instrumentation is key to things that don't scale naively, and surprisingly reusable (if left intact).
Debug logging is a baseline. If it's too verbose for monitoring a certain task, I've become a fan of counters being incremented in e.g. loops or other code points and printing them out at the end: the counts (especially zeros or bigger than anything else), their ratios and which paths were even run often turn out to be fruitful for statistical analysis when oddities are seen and this is especially true with any kind of data munging.
For services which run continually, have configurable printing for statistics. Or have a console which can examine dynamic structures. Or both.
Last winter I couldn't wrap my head around something Zeek was doing, so I ran it under Frida.
Any time I find myself using a debugger in the normal fashion, I find that I ask myself "should you be writing a test?" and not everybody can write tests at that level. In fact, I think running an interactive debugger is a good way to waste resources when you don't wanna write a test. I don't have much sympathy.
<14>1 2022-05-06T21:01:24.54493Z web1.floren.lan webserver 20000066 webserver/search.go:188 [gw@1 searchid="13094420576" uid="2" user="john" query="tag=gravwell" start="2022-05-06T20:01:24Z" end="2022-05-06T21:01:24Z" background="false"] Search launched
I do find that I use Python's built-in debugger often during development, mostly to inspect objects as I'm writing code. I'll stick a breakpoint() in my script after an API call. It's often quicker than looking at Boto or Amazon docs, for example. But I never use it for actually debugging.
In critical systems, it's not unheard of to transaction log every input and every response to external requests, have strictly deterministic code, so that you specifically can replay or reconstruct a system state.
It's a pretty demanding way to build software, but you also get extremely resilient software as a result.
import logging
import threading
logger = logging.getLogger(__name__)
def log(fn):
def log_inner(*a, **kw):
logger.info('%s(%s, %s) @ %s', fn, a, kw, threading.get_native_id())
return fn(*a, **kw)
return log_inner
It's pretty easy to grab all the arguments and thread, but grabbing the return would depend on you not mutating anything permanent, so that probably needs some special casing.Good patterns for JS here? Maybe console.debug() or console.warn()?
I can't see how a thing that is basically a debugger except it remembers the program state after each statement, can possibly work without "attaching" it, or running the program under the debugger, like you would with any other debugger.
It's not like I can say, here, I've hit an error and now I want the history of the program's execution that was never recorded.
For example when the person trying to fix it is not a programmer. Human-readable logs are immensely useful for system administrators. Note the "human-readable" - don't dump some random binary garbage or flag values; log something that's actually useful for getting a high-level overview of what's happening.
For example, if you look at FreeBSD's "iscsid -d" output, you'll see this:
scsid: waiting for request from the kernel
iscsid: not forking due to -d flag; will exit after servicing a single request
iscsid: 127.0.0.1: global login_timeout at 60 sec
iscsid: 127.0.0.1: connecting to 127.0.0.1
iscsid: 127.0.0.1: Capsicum capability mode enabled
iscsid: 127.0.0.1: fetching limits from the kernel
iscsid: 127.0.0.1: setting session timeout to 60 seconds
iscsid: 127.0.0.1: beginning Login phase; sending Login PDU
iscsid: 127.0.0.1: key to send: "AuthMethod=None"
iscsid: 127.0.0.1: key to send: "InitiatorName=iqn.1994-09.org.freebsd:v3"
iscsid: 127.0.0.1: key to send: "SessionType=Discovery"
iscsid: 127.0.0.1: key received: "AuthMethod=None"
iscsid: 127.0.0.1: target requested transition to operational parameter negotiation
iscsid: 127.0.0.1: beginning operational parameter negotiation
iscsid: 127.0.0.1: Limits for offload "" are MaxRecvDataSegment=262144, max_send_dsl=262144, MaxBurstLength=1048576, FirstBurstLength=1048576
iscsid: 127.0.0.1: key to send: "HeaderDigest=None"
iscsid: 127.0.0.1: key to send: "DataDigest=None"
iscsid: 127.0.0.1: key to send: "MaxRecvDataSegmentLength=262144"
iscsid: 127.0.0.1: key to send: "DefaultTime2Wait=0"
iscsid: 127.0.0.1: key to send: "DefaultTime2Retain=0"
iscsid: 127.0.0.1: key to send: "ErrorRecoveryLevel=0"
iscsid: 127.0.0.1: key received: "HeaderDigest=None"
iscsid: 127.0.0.1: key received: "DataDigest=None"
iscsid: 127.0.0.1: key received: "DefaultTime2Wait=0"
iscsid: 127.0.0.1: key received: "DefaultTime2Retain=0"
iscsid: 127.0.0.1: key received: "ErrorRecoveryLevel=0"
iscsid: 127.0.0.1: key received: "MaxRecvDataSegmentLength=131072"
iscsid: 127.0.0.1: target prefers not to do header digest; we'll comply
iscsid: 127.0.0.1: target prefers not to do data digest; we'll comply
iscsid: 127.0.0.1: operational parameter negotiation done; transitioning to Full Feature phase
iscsid: 127.0.0.1: beginning discovery session
iscsid: 127.0.0.1: key to send: "SendTargets=All"
iscsid: 127.0.0.1: waiting for Text Response
iscsid: 127.0.0.1: key received: "TargetName=iqn.2012-06.com.example:target0"
iscsid: 127.0.0.1: key received: "TargetAddress=127.0.0.1:3260,257"
iscsid: 127.0.0.1: adding target iqn.2012-06.com.example:target0
iscsid: 127.0.0.1: removing temporary discovery session
iscsid: 127.0.0.1: discovery done; logging out
iscsid: 127.0.0.1: waiting for Logout Response
iscsid: 127.0.0.1: discovery session done
iscsid: 127.0.0.1: nothing more to do; exiting
If you (roughly) know how iSCSI works, this will tell you a lot, even if you're not a programmer at all.