1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
5 * Copyright (C) 2001-2004 Novell, Inc.
6 * Copyright (C) 2008 Red Hat, Inc.
16 #include "soup-logger.h"
21 * @short_description: Debug logging support
23 * #SoupLogger watches a #SoupSession and logs the HTTP traffic that
24 * it generates, for debugging purposes. Many applications use an
25 * environment variable to determine whether or not to use
26 * #SoupLogger, and to determine the amount of debugging output.
28 * To use #SoupLogger, first create a logger with soup_logger_new(),
29 * optionally configure it with soup_logger_set_request_filter(),
30 * soup_logger_set_response_filter(), and soup_logger_set_printer(),
31 * and then attach it to a session (or multiple sessions) with
32 * soup_session_add_feature().
34 * By default, the debugging output is sent to
35 * <literal>stdout</literal>, and looks something like:
37 * <informalexample><screen>
38 * > POST /unauth HTTP/1.1
39 * > Soup-Debug-Timestamp: 1200171744
40 * > Soup-Debug: SoupSessionAsync 1 (0x612190), SoupMessage 1 (0x617000), SoupSocket 1 (0x612220)
42 * > Content-Type: text/plain
47 * < HTTP/1.1 201 Created
48 * < Soup-Debug-Timestamp: 1200171744
49 * < Soup-Debug: SoupMessage 1 (0x617000)
50 * < Date: Sun, 12 Jan 2008 21:02:24 GMT
51 * < Content-Length: 0
52 * </screen></informalexample>
54 * The <literal>Soup-Debug-Timestamp</literal> line gives the time (as
55 * a <type>time_t</type>) when the request was sent, or the response fully
58 * The <literal>Soup-Debug</literal> line gives further debugging
59 * information about the #SoupSession, #SoupMessage, and #SoupSocket
60 * involved; the hex numbers are the addresses of the objects in
61 * question (which may be useful if you are running in a debugger).
62 * The decimal IDs are simply counters that uniquely identify objects
63 * across the lifetime of the #SoupLogger. In particular, this can be
64 * used to identify when multiple messages are sent across the same
67 * Currently, the request half of the message is logged just before
68 * the first byte of the request gets written to the network (from the
69 * #SoupSession::request_started signal), which means that if you have
70 * not made the complete request body available at that point, it will
71 * not be logged. The response is logged just after the last byte of
72 * the response body is read from the network (from the
73 * #SoupMessage::got_body or #SoupMessage::got_informational signal),
74 * which means that the #SoupMessage::got_headers signal, and anything
75 * triggered off it (such as #SoupSession::authenticate) will be
76 * emitted <emphasis>before</emphasis> the response headers are
80 static void soup_logger_session_feature_init (SoupSessionFeatureInterface *feature_interface, gpointer interface_data);
82 G_DEFINE_TYPE_WITH_CODE (SoupLogger, soup_logger, G_TYPE_OBJECT,
83 G_IMPLEMENT_INTERFACE (SOUP_TYPE_SESSION_FEATURE,
84 soup_logger_session_feature_init))
87 /* We use a mutex so that if requests are being run in
88 * multiple threads, we don't mix up the output.
95 SoupLoggerLogLevel level;
98 SoupLoggerFilter request_filter;
99 gpointer request_filter_data;
100 GDestroyNotify request_filter_dnotify;
102 SoupLoggerFilter response_filter;
103 gpointer response_filter_data;
104 GDestroyNotify response_filter_dnotify;
106 SoupLoggerPrinter printer;
107 gpointer printer_data;
108 GDestroyNotify printer_dnotify;
110 #define SOUP_LOGGER_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), SOUP_TYPE_LOGGER, SoupLoggerPrivate))
113 soup_logger_init (SoupLogger *logger)
115 SoupLoggerPrivate *priv = SOUP_LOGGER_GET_PRIVATE (logger);
117 g_mutex_init (&priv->lock);
118 priv->tag = g_quark_from_static_string (g_strdup_printf ("SoupLogger-%p", logger));
119 priv->ids = g_hash_table_new (NULL, NULL);
123 soup_logger_finalize (GObject *object)
125 SoupLoggerPrivate *priv = SOUP_LOGGER_GET_PRIVATE (object);
127 g_hash_table_destroy (priv->ids);
129 if (priv->request_filter_dnotify)
130 priv->request_filter_dnotify (priv->request_filter_data);
131 if (priv->response_filter_dnotify)
132 priv->response_filter_dnotify (priv->response_filter_data);
133 if (priv->printer_dnotify)
134 priv->printer_dnotify (priv->printer_data);
136 g_mutex_clear (&priv->lock);
138 G_OBJECT_CLASS (soup_logger_parent_class)->finalize (object);
142 soup_logger_class_init (SoupLoggerClass *logger_class)
144 GObjectClass *object_class = G_OBJECT_CLASS (logger_class);
146 g_type_class_add_private (logger_class, sizeof (SoupLoggerPrivate));
148 object_class->finalize = soup_logger_finalize;
152 * SoupLoggerLogLevel:
153 * @SOUP_LOGGER_LOG_NONE: No logging
154 * @SOUP_LOGGER_LOG_MINIMAL: Log the Request-Line or Status-Line and
155 * the Soup-Debug pseudo-headers
156 * @SOUP_LOGGER_LOG_HEADERS: Log the full request/response headers
157 * @SOUP_LOGGER_LOG_BODY: Log the full headers and request/response
160 * Describes the level of logging output to provide.
165 * @level: the debug level
166 * @max_body_size: the maximum body size to output, or -1
168 * Creates a new #SoupLogger with the given debug level. If @level is
169 * %SOUP_LOGGER_LOG_BODY, @max_body_size gives the maximum number of
170 * bytes of the body that will be logged. (-1 means "no limit".)
172 * If you need finer control over what message parts are and aren't
173 * logged, use soup_logger_set_request_filter() and
174 * soup_logger_set_response_filter().
176 * Returns: a new #SoupLogger
179 soup_logger_new (SoupLoggerLogLevel level, int max_body_size)
182 SoupLoggerPrivate *priv;
184 logger = g_object_new (SOUP_TYPE_LOGGER, NULL);
186 priv = SOUP_LOGGER_GET_PRIVATE (logger);
188 priv->max_body_size = max_body_size;
195 * @logger: the #SoupLogger
196 * @msg: the message being logged
197 * @user_data: the data passed to soup_logger_set_request_filter()
198 * or soup_logger_set_response_filter()
200 * The prototype for a logging filter. The filter callback will be
201 * invoked for each request or response, and should analyze it and
202 * return a #SoupLoggerLogLevel value indicating how much of the
203 * message to log. Eg, it might choose between %SOUP_LOGGER_LOG_BODY
204 * and %SOUP_LOGGER_LOG_HEADERS depending on the Content-Type.
206 * Return value: a #SoupLoggerLogLevel value indicating how much of
211 * soup_logger_set_request_filter:
212 * @logger: a #SoupLogger
213 * @request_filter: the callback for request debugging
214 * @filter_data: data to pass to the callback
215 * @destroy: a #GDestroyNotify to free @filter_data
217 * Sets up a filter to determine the log level for a given request.
218 * For each HTTP request @logger will invoke @request_filter to
219 * determine how much (if any) of that request to log. (If you do not
220 * set a request filter, @logger will just always log requests at the
221 * level passed to soup_logger_new().)
224 soup_logger_set_request_filter (SoupLogger *logger,
225 SoupLoggerFilter request_filter,
226 gpointer filter_data,
227 GDestroyNotify destroy)
229 SoupLoggerPrivate *priv = SOUP_LOGGER_GET_PRIVATE (logger);
231 priv->request_filter = request_filter;
232 priv->request_filter_data = filter_data;
233 priv->request_filter_dnotify = destroy;
237 * soup_logger_set_response_filter:
238 * @logger: a #SoupLogger
239 * @response_filter: the callback for response debugging
240 * @filter_data: data to pass to the callback
241 * @destroy: a #GDestroyNotify to free @filter_data
243 * Sets up a filter to determine the log level for a given response.
244 * For each HTTP response @logger will invoke @response_filter to
245 * determine how much (if any) of that response to log. (If you do not
246 * set a response filter, @logger will just always log responses at
247 * the level passed to soup_logger_new().)
250 soup_logger_set_response_filter (SoupLogger *logger,
251 SoupLoggerFilter response_filter,
252 gpointer filter_data,
253 GDestroyNotify destroy)
255 SoupLoggerPrivate *priv = SOUP_LOGGER_GET_PRIVATE (logger);
257 priv->response_filter = response_filter;
258 priv->response_filter_data = filter_data;
259 priv->response_filter_dnotify = destroy;
264 * @logger: the #SoupLogger
265 * @level: the level of the information being printed.
266 * @direction: a single-character prefix to @data
267 * @data: data to print
268 * @user_data: the data passed to soup_logger_set_printer()
270 * The prototype for a custom printing callback.
272 * @level indicates what kind of information is being printed. Eg, it
273 * will be %SOUP_LOGGER_LOG_HEADERS if @data is header data.
275 * @direction is either '<', '>', or ' ', and @data is the single line
276 * to print; the printer is expected to add a terminating newline.
278 * To get the effect of the default printer, you would do:
280 * <informalexample><programlisting>
281 * printf ("%c %s\n", direction, data);
282 * </programlisting></informalexample>
286 * soup_logger_set_printer:
287 * @logger: a #SoupLogger
288 * @printer: the callback for printing logging output
289 * @printer_data: data to pass to the callback
290 * @destroy: a #GDestroyNotify to free @printer_data
292 * Sets up an alternate log printing routine, if you don't want
293 * the log to go to <literal>stdout</literal>.
296 soup_logger_set_printer (SoupLogger *logger,
297 SoupLoggerPrinter printer,
298 gpointer printer_data,
299 GDestroyNotify destroy)
301 SoupLoggerPrivate *priv = SOUP_LOGGER_GET_PRIVATE (logger);
303 priv->printer = printer;
304 priv->printer_data = printer_data;
305 priv->printer_dnotify = destroy;
309 soup_logger_get_id (SoupLogger *logger, gpointer object)
311 SoupLoggerPrivate *priv = SOUP_LOGGER_GET_PRIVATE (logger);
313 return GPOINTER_TO_UINT (g_object_get_qdata (object, priv->tag));
317 soup_logger_set_id (SoupLogger *logger, gpointer object)
319 SoupLoggerPrivate *priv = SOUP_LOGGER_GET_PRIVATE (logger);
320 gpointer klass = G_OBJECT_GET_CLASS (object);
323 id = g_hash_table_lookup (priv->ids, klass);
325 g_hash_table_insert (priv->ids, klass, id);
327 g_object_set_qdata (object, priv->tag, id);
328 return GPOINTER_TO_UINT (id);
332 * soup_logger_attach:
333 * @logger: a #SoupLogger
334 * @session: a #SoupSession
336 * Sets @logger to watch @session and print debug information for
339 * (The session will take a reference on @logger, which will be
340 * removed when you call soup_logger_detach(), or when the session is
343 * Deprecated: Use soup_session_add_feature() instead.
346 soup_logger_attach (SoupLogger *logger,
347 SoupSession *session)
349 soup_session_add_feature (session, SOUP_SESSION_FEATURE (logger));
353 * soup_logger_detach:
354 * @logger: a #SoupLogger
355 * @session: a #SoupSession
357 * Stops @logger from watching @session.
359 * Deprecated: Use soup_session_remove_feature() instead.
362 soup_logger_detach (SoupLogger *logger,
363 SoupSession *session)
365 soup_session_remove_feature (session, SOUP_SESSION_FEATURE (logger));
369 soup_logger_print (SoupLogger *logger, SoupLoggerLogLevel level,
370 char direction, const char *format, ...)
372 SoupLoggerPrivate *priv = SOUP_LOGGER_GET_PRIVATE (logger);
374 char *data, *line, *end;
376 va_start (args, format);
377 data = g_strdup_vprintf (format, args);
380 if (level == SOUP_LOGGER_LOG_BODY && priv->max_body_size > 0) {
381 if (strlen (data) > priv->max_body_size + 6)
382 strcpy (data + priv->max_body_size, "\n[...]");
387 end = strchr (line, '\n');
391 priv->printer (logger, level, direction,
392 line, priv->printer_data);
394 printf ("%c %s\n", direction, line);
397 } while (end && *line);
403 soup_logger_print_basic_auth (SoupLogger *logger, const char *value)
405 char *decoded, *decoded_utf8, *p;
408 decoded = (char *)g_base64_decode (value + 6, &len);
409 if (decoded && !g_utf8_validate (decoded, -1, NULL)) {
410 decoded_utf8 = g_convert_with_fallback (decoded, -1,
411 "UTF-8", "ISO-8859-1",
416 decoded = decoded_utf8;
421 decoded = g_strdup (value);
422 p = strchr (decoded, ':');
424 while (++p < decoded + len)
427 soup_logger_print (logger, SOUP_LOGGER_LOG_HEADERS, '>',
428 "Authorization: Basic [%.*s]", len, decoded);
433 print_request (SoupLogger *logger, SoupMessage *msg,
434 SoupSession *session, SoupSocket *socket,
437 SoupLoggerPrivate *priv = SOUP_LOGGER_GET_PRIVATE (logger);
438 SoupLoggerLogLevel log_level;
439 SoupMessageHeadersIter iter;
440 const char *name, *value;
443 if (priv->request_filter) {
444 log_level = priv->request_filter (logger, msg,
445 priv->request_filter_data);
447 log_level = priv->level;
449 if (log_level == SOUP_LOGGER_LOG_NONE)
452 uri = soup_message_get_uri (msg);
453 if (msg->method == SOUP_METHOD_CONNECT) {
454 soup_logger_print (logger, SOUP_LOGGER_LOG_MINIMAL, '>',
455 "CONNECT %s:%u HTTP/1.%d",
456 uri->host, uri->port,
457 soup_message_get_http_version (msg));
459 soup_logger_print (logger, SOUP_LOGGER_LOG_MINIMAL, '>',
460 "%s %s%s%s HTTP/1.%d",
461 msg->method, uri->path,
462 uri->query ? "?" : "",
463 uri->query ? uri->query : "",
464 soup_message_get_http_version (msg));
467 soup_logger_print (logger, SOUP_LOGGER_LOG_MINIMAL, '>',
468 "Soup-Debug-Timestamp: %lu",
469 (unsigned long)time (0));
470 soup_logger_print (logger, SOUP_LOGGER_LOG_MINIMAL, '>',
471 "Soup-Debug: %s %u (%p), %s %u (%p), %s %u (%p)%s",
472 g_type_name_from_instance ((GTypeInstance *)session),
473 soup_logger_get_id (logger, session), session,
474 g_type_name_from_instance ((GTypeInstance *)msg),
475 soup_logger_get_id (logger, msg), msg,
476 g_type_name_from_instance ((GTypeInstance *)socket),
477 soup_logger_get_id (logger, socket), socket,
478 restarted ? ", restarted" : "");
480 if (log_level == SOUP_LOGGER_LOG_MINIMAL)
483 if (!soup_message_headers_get_one (msg->request_headers, "Host")) {
486 if (strchr (uri->host, ':'))
487 uri_host = g_strdup_printf ("[%s]", uri->host);
488 else if (g_hostname_is_non_ascii (uri->host))
489 uri_host = g_hostname_to_ascii (uri->host);
491 uri_host = uri->host;
493 soup_logger_print (logger, SOUP_LOGGER_LOG_HEADERS, '>',
494 "Host: %s%c%u", uri_host,
495 soup_uri_uses_default_port (uri) ? '\0' : ':',
498 if (uri_host != uri->host)
501 soup_message_headers_iter_init (&iter, msg->request_headers);
502 while (soup_message_headers_iter_next (&iter, &name, &value)) {
503 if (!g_ascii_strcasecmp (name, "Authorization") &&
504 !g_ascii_strncasecmp (value, "Basic ", 6))
505 soup_logger_print_basic_auth (logger, value);
507 soup_logger_print (logger, SOUP_LOGGER_LOG_HEADERS, '>',
508 "%s: %s", name, value);
511 if (log_level == SOUP_LOGGER_LOG_HEADERS)
514 if (msg->request_body->length &&
515 soup_message_body_get_accumulate (msg->request_body)) {
518 request = soup_message_body_flatten (msg->request_body);
519 g_return_if_fail (request != NULL);
520 soup_buffer_free (request);
522 if (soup_message_headers_get_expectations (msg->request_headers) != SOUP_EXPECTATION_CONTINUE) {
523 soup_logger_print (logger, SOUP_LOGGER_LOG_BODY, '>',
524 "\n%s", msg->request_body->data);
530 print_response (SoupLogger *logger, SoupMessage *msg)
532 SoupLoggerPrivate *priv = SOUP_LOGGER_GET_PRIVATE (logger);
533 SoupLoggerLogLevel log_level;
534 SoupMessageHeadersIter iter;
535 const char *name, *value;
537 if (priv->response_filter) {
538 log_level = priv->response_filter (logger, msg,
539 priv->response_filter_data);
541 log_level = priv->level;
543 if (log_level == SOUP_LOGGER_LOG_NONE)
546 soup_logger_print (logger, SOUP_LOGGER_LOG_MINIMAL, '<',
548 soup_message_get_http_version (msg),
549 msg->status_code, msg->reason_phrase);
551 soup_logger_print (logger, SOUP_LOGGER_LOG_MINIMAL, '<',
552 "Soup-Debug-Timestamp: %lu",
553 (unsigned long)time (0));
554 soup_logger_print (logger, SOUP_LOGGER_LOG_MINIMAL, '<',
555 "Soup-Debug: %s %u (%p)",
556 g_type_name_from_instance ((GTypeInstance *)msg),
557 soup_logger_get_id (logger, msg), msg);
559 if (log_level == SOUP_LOGGER_LOG_MINIMAL)
562 soup_message_headers_iter_init (&iter, msg->response_headers);
563 while (soup_message_headers_iter_next (&iter, &name, &value)) {
564 soup_logger_print (logger, SOUP_LOGGER_LOG_HEADERS, '<',
565 "%s: %s", name, value);
567 if (log_level == SOUP_LOGGER_LOG_HEADERS)
570 if (msg->response_body->data) {
571 soup_logger_print (logger, SOUP_LOGGER_LOG_BODY, '<',
572 "\n%s", msg->response_body->data);
577 got_informational (SoupMessage *msg, gpointer user_data)
579 SoupLogger *logger = user_data;
580 SoupLoggerPrivate *priv = SOUP_LOGGER_GET_PRIVATE (logger);
582 g_mutex_lock (&priv->lock);
584 print_response (logger, msg);
585 soup_logger_print (logger, SOUP_LOGGER_LOG_MINIMAL, ' ', "");
587 if (msg->status_code == SOUP_STATUS_CONTINUE && msg->request_body->data) {
588 SoupLoggerLogLevel log_level;
590 soup_logger_print (logger, SOUP_LOGGER_LOG_MINIMAL, '>',
591 "[Now sending request body...]");
593 if (priv->request_filter) {
594 log_level = priv->request_filter (logger, msg,
595 priv->request_filter_data);
597 log_level = priv->level;
599 if (log_level == SOUP_LOGGER_LOG_BODY) {
600 soup_logger_print (logger, SOUP_LOGGER_LOG_BODY, '>',
601 "%s", msg->request_body->data);
604 soup_logger_print (logger, SOUP_LOGGER_LOG_MINIMAL, ' ', "");
607 g_mutex_unlock (&priv->lock);
611 got_body (SoupMessage *msg, gpointer user_data)
613 SoupLogger *logger = user_data;
614 SoupLoggerPrivate *priv = SOUP_LOGGER_GET_PRIVATE (logger);
616 g_mutex_lock (&priv->lock);
618 print_response (logger, msg);
619 soup_logger_print (logger, SOUP_LOGGER_LOG_MINIMAL, ' ', "");
621 g_mutex_unlock (&priv->lock);
625 soup_logger_request_queued (SoupSessionFeature *logger,
626 SoupSession *session,
629 g_return_if_fail (SOUP_IS_MESSAGE (msg));
631 g_signal_connect (msg, "got-informational",
632 G_CALLBACK (got_informational),
634 g_signal_connect (msg, "got-body",
635 G_CALLBACK (got_body),
640 soup_logger_request_started (SoupSessionFeature *feature,
641 SoupSession *session,
645 SoupLogger *logger = SOUP_LOGGER (feature);
649 g_return_if_fail (SOUP_IS_SESSION (session));
650 g_return_if_fail (SOUP_IS_MESSAGE (msg));
651 g_return_if_fail (SOUP_IS_SOCKET (socket));
653 msg_id = soup_logger_get_id (logger, msg);
657 soup_logger_set_id (logger, msg);
661 if (!soup_logger_get_id (logger, session))
662 soup_logger_set_id (logger, session);
664 if (!soup_logger_get_id (logger, socket))
665 soup_logger_set_id (logger, socket);
667 print_request (logger, msg, session, socket, restarted);
668 soup_logger_print (logger, SOUP_LOGGER_LOG_MINIMAL, ' ', "");
672 soup_logger_request_unqueued (SoupSessionFeature *logger,
673 SoupSession *session,
676 g_return_if_fail (SOUP_IS_MESSAGE (msg));
678 g_signal_handlers_disconnect_by_func (msg, got_informational, logger);
679 g_signal_handlers_disconnect_by_func (msg, got_body, logger);
683 soup_logger_session_feature_init (SoupSessionFeatureInterface *feature_interface,
684 gpointer interface_data)
686 feature_interface->request_queued = soup_logger_request_queued;
687 feature_interface->request_started = soup_logger_request_started;
688 feature_interface->request_unqueued = soup_logger_request_unqueued;