controlling the output of CProfileLogRoute

During development, I use CProfileLogRoute to help with profiling SQL queries, and to see Yii::trace() messages in the browser.

By default, CProfileLogRoute will output it’s results at the end of the request. Some layouts will break, if you simply append the HTML after the closing <html> tag.

Is there a way to control when/where the report is written to output?

I thought I would be able to do something like Yii::app()->log->processLogs() in the footer of my layout, but that does not work…

bump

Any help here? I have spent hours on this, and no luck so far…

Just letting it output after the </html> tag is wrong - it does not render correctly, nor will such markup validate :frowning:

It seems there is no way to do this, so I made some changes to CProfileLogRoute:




class CProfileLogRoute extends CWebLogRoute

{

	...


	/**

	 * @var bool whether the report has already been output

	 */

	private $_processed=false;

 

	...


	public function processLogs($logs)

	{

		if ($this->_processed) return; // return if report already displayed

    

		$app=Yii::app();

		if(!($app instanceof CWebApplication) || $app->getRequest()->getIsAjaxRequest())

			return;


		if($this->getReport()==='summary')

			$this->displaySummary($logs);

		else

			$this->displayCallstack($logs);

    

		$this->_processed = true; // mark report as displayed

	}



With these changes in place, you can now choose when to display the log output, by adding this to your layout:




<?php echo Yii::app()->log->collectLogs(null); ?>



I suspect this is probably bad and wrong, since other log routes would get processed twice as well, causing duplicate log entries…

How about this (not tested, might need some finetuning):


<?php

// configure this instead of CProfileLogRoute

class MyProfileLogRoute extends CProfileLogRoute {


  public function init() {

    parent::init();

    ob_start();

    ob_implicit_flush(false);

  }


  public function render() 

  {

    $output=ob_get_clean();


    ob_start();

    parent::render();

    $profileOutput=ob_get_glean();


    // now you have the rendered $output and the $profileOutput.

    // You could use preg_replace() now to insert $profileOutput

    // somewhere in $output. (See CClientScript::renderBodyBegin() for example)

  }

}

    

Interesting approach, thanks Mike!

Manually interfering with output buffering (around the framework) might not be the best approach though, maybe the component could install an OutputProcessor to do the actual replacement, and we could use a Clip to capture the content?

I don’t have time to experiment with this right now, but will look into it when I can…

Yeah, that’s what i was thinking about, too. But somehow i couldn’t wrap my head around how to use a widget in this case. So i just used the same ob_ methods like the COutputProcessor widget.

I think it should not be a problem as long as you make sure, that ob_ calls are nested correctly.

Here’s my take on a solution:




<?php


class ProfileLogRoute extends CProfileLogRoute {

  

  protected $content = '';

  protected $processed = false;

  

  const TOKEN = '<span id="ProfileLogRoute"></span>';

  

  public function init() {

    parent::init();

    Yii::app()->attachEventHandler(

      'onEndRequest',

      array($this, 'replaceToken')

    );

		ob_start();

		ob_implicit_flush(false);

  }

  

  public function processLogs($logs) {

    $this->content = ob_get_clean();

		ob_start();

		ob_implicit_flush(false);

    parent::processLogs($logs);

    $this->processed = true;

  }

  

  public function replaceToken() {

    if ($this->processed) {

      $log_output = ob_get_clean();

      echo str_replace(

        self::TOKEN,

        $log_output,

        $this->content

      );

    }

  }

  

}



In your main layout, where you want the output placed, put in the replacement token, e.g.:




...

<body>

...

<?php echo ProfileLogRoute::TOKEN; ?>

</body>



I still needed to hack CLogRouter a bit, because it attaches it’s event handler before the CLogRoutes get a chance to attach theirs, so the attachEventHandler call needs to move up:




	public function init()

	{

		parent::init();

		Yii::app()->attachEventHandler('onEndRequest',array($this,'collectLogs'));

		foreach($this->_routes as $name=>$route)

		{

			$route=Yii::createComponent($route);

			$route->init();

			$this->_routes[$name]=$route;

		}

	}



It works nicely.

I still would like to see this feature integrated in Yii in a cleaner way - e.g. a method call in the main layout, something like:


<?php Yii::app()->log->place(); ?>

This method would simply add a layer of output buffering for the rest of the page, and enable capture and replacement of the log output when the request ends. Much cleaner and simpler. But requires deeper changes to the framework components…

Also nice, but there might be a catch: The application hasn’t finished yet completely when layout is rendered. So you might lose some profiling data if something happens afterwards. Even though this might rarely be the case…

But the same is true for logging in general - you can’t guarantee that the logger will always run last, it depends on the order in which end-request event handlers were added.

I updated the code to handle requests with no entries - since processLogs is never called if there are no log entries, the output buffering control has to be skipped in this case, otherwise you get a blank page when there is no log entries!

Also note, I haven’t changed the order in which anything runs - a placeholder token is inserted where I want the log output, but it’s not replaced until the end of the request, so even if you put it at the top of the page, you will see log entries from queries invoked by the remainder of the template.

Okay, be careful - there’s a major problem with this … it will kill the YIIC command-line shell!

You can work around that by using the regular logger under the command line.