Traces & Deployment

Today I feel like taking it easy a bit. I have a few small items that could be done to error log application. The main one being deployment. I’ll need to setup some secrets on github project and create a YAML action workflow to build the project and deploy it. I’ll also need to setup a separate FTP account for the transfer.

Deployment Prompt: Microsoft Image Creator

Streamlined deployment! Today I built a robust build script for the Angular application, enabling seamless deployment to the remote website via FTP. Additionally, I enhanced error and exception handling to provide valuable insights by including stack traces and requested URLs. To round things out, I made some final refinements to the application for optimal performance.

Hostinger FTP Issue Feedback

I went to setup an FTP account and was prompted about my experience with support staff recently. Oh boy – do I have a lot to say. I was entering feedback and opened up the support chat window to reference my ticket. Unfortunately, the feedback prompt went away. Ugh… really?

In the meantime, I reached out to Hostinger regarding the FTP issue that I had on the prior server to let them know that I haven’t experienced any recent problems. As always, I couldn’t respond to the ticket itself and had to work my way past the AI. I had plenty of time to prepare a statement…

Notifying support staff that FTP issue is resolved

Hello. I’m contacting you in regards to ticket #97118. I understand that Hosting doesn’t like people talking to support staff, viewing any information on a ticket, or commenting on tickets directly with screenshots, logs, or a status that the problem still persists. I just wanted to report that I haven’t had the daily issue with FTP happen after I my website (periplux.io) was moved to Arizona on June 17. This problem seems specific to Massachusetts. Since no one is working from 1:30 AM to 2:00 AM Eastern Time, I still recommend that support staff setup an automated script to log into an FTP account on that server (1502 I believe?) at 1:45 AM Eastern and try to get a directory list. This would at least help you confirm that a problem exists in that time period, even though it doesn’t show up in the logs. As an added measure, I recommend testing it from an external location in case there is a firewall that is blocking ports that you may be otherwise bypassing if testing internally.

I may come off as a bit rough, but my experience with this issue had left an overwhelming negative impression about the companies customer support in general. As a new customer still within the 30-day window, I was about to leave them for another host while the issue was still ongoing. Fortunately the move between servers resolved the problem for me – but I suspect it is still a problem waiting to be discovered by other clients.

Historical reference:

Deployment

I’ve setup my deployment script and build secrets. This deployment is a little different since it combines the content of a PHP folder with the Angular build output. It’s fairly simple to just copy the files from multiple folders into one. I hadn’t setup composer for the PHP files as I don’t use any third party libraries.

Skipped… why? When I copied the YAML script for another project, it was for a development branch. Although I had changed the branch to production, all of the build jobs had a reference check as well to confirm we were on develop branch – which we were not. All fixed, now onto the build…

Github workflow in action

And… we failed.

  npm run build
  shell: /usr/bin/bash -e {0}

> @codejamboree/errors.periplux.io@1.0.0 build
> ng build --configuration production

An unhandled exception occurred: The /home/runner/work/errors.periplux.io/errors.periplux.io/src/environments/environment.production.ts path in file replacements does not exist.
See "/tmp/ng-mXw5yS/angular-errors.log" for further details.
Error: Process completed with exit code 127.

Of course we failed. I forgot to create the environment file with production details. Let’s keep making fixes. The build kept failing with each change.

  • Need to create environment folder first
  • Need to create environment.ts file as well
  • Can’t create same folder a second time
  • Need to copy PHP folder recursively
  • Requesting build info json failed – I overwrite .htaccess rather than appended to it

Interesting… I didn’t overwrite the file. It actually didn’t copy over into the build folder. I tried cp -r php/* build/. The wildcard skipped over the file prefixed with a dot. I was able to tell the script to also include hidden files with the -a flag.

mkdir build
cp -r -a dist/errors.periplux.io/browser/* build/
cp dist/errors.periplux.io/3rdpartylicenses.txt build/
cp -r -a php/* build/

Nope – it’s still not copying. Let’s try a remote sync.

mkdir build
rsync -r dist/errors.periplux.io/browser/* build/
cp dist/errors.periplux.io/3rdpartylicenses.txt build/
rsync -r php/* build/

Locally it works on my machine. The build artifact shows that the .htaccess file wasn’t copied. Let’s try explicitly including it.

mkdir build
cp -r dist/errors.periplux.io/browser/* build/
cp dist/errors.periplux.io/3rdpartylicenses.txt build/
rsync -r --include="*/.htaccess" php/* build/

Nothing… I’m beginning to wonder if the upload-artifact action is ignoring the file. Let’s create a tar archive of the files and store that as our upload artifact.

Well, it didn’t tar .htaccess. I’m going to setup to rsync to output what files it copied, as well as tell the tar to include .htaccess.

mkdir build
cp -r dist/errors.periplux.io/browser/* build/
cp dist/errors.periplux.io/3rdpartylicenses.txt build/
rsync -r -v --include="*/.htaccess" php/* build/
tar -cvf ./web_files.tar --include="*/.htaccess" ./build

Tar doesn’t respond to the include flag. However, the rsync was able to output the files – and .htaccess was not copied. So, tar isn’t going to help us if the file isn’t present.

Let’s just revert back and copy the .htaccess file explicitly

mkdir build
cp -r dist/errors.periplux.io/browser/* build/
cp dist/errors.periplux.io/3rdpartylicenses.txt build/
cp -r php/* build/
cp php/.htaccess build/

Nope. Still not included in the archive. The cp command doesn’t report what files were copied. Let’s change everything to rsync with verbose output and see if the file is reported in the copy process.

rsync -v -r dist/errors.periplux.io/browser/* build/
rsync -v dist/errors.periplux.io/3rdpartylicenses.txt build/
rsync -v -r php/* build/
rsync -v php/.htaccess build/
# .htaccess
# sent 1,085 bytes  received 35 bytes  2,240.00 bytes/sec
# total size is 997  speedup is 0.89

So… the file is copied. The problem now appears to be with the upload-artifact action. This may seem silly, but I’m going to rename it to use an underscore, and fix it during the deploy process.

Yes!! It worked. I had to change the request to build-info to include the json extension, but other than that, the build completed. However, the websites API endpoints are not working. Now why is that?

I’m not getting any response other than an HTTP 500 error. Creating a test.php file that simply echoed “Hello” gave me a response. So the .htaccess file isn’t causing problems with PHP. Let’s see if an error log appears on the file system.

[28-Jun-2024 00:08:09 UTC] PHP Warning:  require_once(/home/***/domains/periplux.io/public_html/errors/commonHTTP_STATUS.php): Failed to open stream: No such file or directory in /home/***/domains/periplux.io/public_html/errors/common/Database.php on line 2

It looks like a forward slash is missing. The files on my server seem to have been out of sync with the files in the source code repository. It’s a good thing I’ve setup the build process today. The database.php file is the only one that tried to prefix the current directory to the include files, but forgot to add a DIRECTORY_SEPARATOR. Let’s just drop the __DIR__ prefix for now.

Hey! We are in! And we have logs to transfer. Unfortunately, we have a problem reporting how many logs were transferred.

Oh – my bad. I had simply copied the same code that checks if logs exist. Let’s refresh and try that again.

Nice! And I see that our button to transfer logs went away afterwards.

Chasing Errors

Reviewing the errors, I found that one was missing plenty of details, but included an HTTP Status code in the message. Why wasn’t the stack-trace included when it was reported in the error log? I can trigger this error by trying to visit the /api/login endpoint without posting data. Without the stack-trace, I don’t see where this error originated.

I think I need to start including the requested URL. In addition, this specific error shouldn’t have been logged. I am now handling errors when instantiating PostedJson. I’m still concerned over the missing stack trace and url.

The URL is now logged as a detail including any query string parameters.

I’m still taken aback why the stack trace is not logged. I may have a clue as to why… wait, no. I thought I was attempting to save an array instead of a string. Let’s take a look at how I’m grabbing the stack trace.

$stack_trace = compile_stack(debug_backtrace());
function compile_stack($trace)
{
    array_shift($trace);
    $stack = "";
    foreach ($trace as $frame) {
        $file = $frame['file'];
        $line = $frame['line'];
        $func = $frame['function'];
        $stack .= "File: $file, Line: $line, function: $function\n";
    }
    return $stack;
}

Dumping $stack, I’m only getting an empty string. I’m not getting anything out of the frames. I shift the trace to remove the first frame indicating that I grabbed the backtrace while in the custom exception handler. That’s got to be my problem area.

Hmm… with an exception handler, it looks like there is no other trace when looking at the var dump. However, I see the trace on the exception itself. It makes sense to grab that one instead. We can format it the same way that it appears in the error log file for consistency.

$stack_trace = compile_stack($exception->getTrace());

function compile_stack($trace)
{
    $text = "";
    foreach ($trace as $index => $frame) {
        $file = $frame['file'];
        $line = $frame['line'];
        if (array_key_exists('function', $frame)) {
            $function = $frame['function'];
        } else {
            $function = '';
        }
        if (array_key_exists('class', $frame)) {
            $class = $frame['class'];
        } else {
            $class = '';
        }
        if (array_key_exists('type', $frame)) {
            $type = $frame['type'];

        } else {
            $type = '';
        }
        $text .= "#$index $file($line): $class$type$function\n";
    }
    return $text;
}

That looks just about normal. I see a few formatting issues. Main should have french brackets. Let’s find an original stack-trace.

It looks like the last frame needs to change it’s format.

Wait… my unfamiliarity with PHP is definitely showing. Apparently the exception has a method to getTraceAsString().

Now that’s looking great! Now that the exception handler is logging the stack trace, what about the error handler? Well – it looks like my original code to get the debug backtrace was needed after all. The in-line backtrace is available as errors are reported.

PHP Error Logging
<?php
require_once "Show.php";
require_once "DatabaseHelper.php";
require_once "Secrets.php";

function ignore_error($message)
{
    $messages = [
        // We don't have control over this extension
        'Creation of dynamic property Memcache::$connection is deprecated',
    ];
    foreach ($messages as $needle) {
        if (strpos($message, $needle) !== false) {
            return true;
        }
    }
    return false;
}
function full_url()
{
    $protocol = ((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != "off") || $_SERVER['SERVER_PORT'] == 443) ? "https://" : "http://";
    $host = $_SERVER['HTTP_HOST'];
    $uri = $_SERVER['REQUEST_URI'];
    return $protocol . $host . $uri;
}
function log_error($type, $message, $path, $line, ?string $stack_trace = null)
{
    if (ignore_error($message)) {
        return true;
    }
    try {
        $credentials = Secrets::revealAs("ERROR_DATABASE", 'array');
        if ($credentials === false) {
            return false;
        }
        $db = new DatabaseHelper($credentials);

        $log_id = $db->selectScalar(
            'CALL sp_log(?, ?, ?, ?, ?, ?)',
            'sisssi',
            $_SERVER['SERVER_NAME'],
            time(),
            $type,
            $message,
            $path,
            $line
        );

        if ($log_id === false) {
            return false;
        }

        $db->affectAny(
            "CALL sp_log_details(?, ?, ?)",
            'iss',
            $log_id,
            'Url',
            full_url()
        );

        if ($stack_trace === null) {
            return true;
        }

        $db->affectAny(
            "CALL sp_log_details(?, ?, ?)",
            'iss',
            $log_id,
            'Stack Trace',
            $stack_trace
        );

        return true;
    } catch (Exception $e) {
        return false;
    }

}
function error_number_as_type($errno)
{
    switch ($errno) {
        case E_DEPRECATED: return 'PHP Deprecated';
        case E_ERROR: return 'PHP Error';
        case E_WARNING: return 'PHP Warning';
        case E_PARSE: return 'PHP Parse';
        case E_NOTICE: return 'PHP Notice';
        case E_CORE_ERROR: return 'PHP Core Error';
        case E_CORE_WARNING: return 'PHP Core Warning';
        case E_USER_ERROR: return 'PHP User Error';
        case E_USER_WARNING: return 'PHP User Warning';
        case E_USER_NOTICE: return 'PHP User Notice';
        case E_STRICT: return 'PHP Strict';
        case E_RECOVERABLE_ERROR: return 'PHP Recoverable Error';
        case E_DEPRECATED: return 'PHP Deprecated';
        case E_USER_DEPRECATED: return 'PHP User Deprecated';
        case E_ALL: return 'PHP All Errors, Warnings, and Notices';
        default:return $errno;
    }
}
function continue_after_error_number($errno)
{
    switch ($errno) {
        case E_DEPRECATED:
        case E_WARNING:
        case E_PARSE:
        case E_NOTICE:
        case E_USER_WARNING:
        case E_USER_NOTICE:
        case E_CORE_WARNING:
        case E_DEPRECATED:
        case E_USER_DEPRECATED:
            return true;
        default:
            return false;
    }
}
function custom_error_handler($errno, $errstr, $errfile, $errline)
{
    $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
    array_shift($trace);
    $stack_trace = compile_stack($trace);

    $type = error_number_as_type($errno);
    if (log_error($type, $errstr, $errfile, $errline, $stack_trace)) {
        if (continue_after_error_number($errno)) {
            return true;
        }
        Show::error("An error ($type) was reported. $errstr");
    } else {
        Show::error($errstr);
    }
}

function custom_exception_handler($exception)
{
    $class = get_class($exception);
    $code = $exception->getCode();
    $message = $exception->getMessage();
    $file = $exception->getFile();
    $line = $exception->getLine();

    if ($code !== null && $code !== 0) {
        $message = "[$code] " . $message;
    }

    $stack_trace = $exception->getTraceAsString();

    if (log_error($class, $message, $file, $line, $stack_trace)) {
        Show::error("An unexpected exception was reported. $message");
    } else {
        Show::error($message);
    }
}
function get_key_value(string $key, array $array, string $default_value)
{
    if (array_key_exists($key, $array)) {
        return $array[$key];
    }
    return $default_value;
}
function compile_stack($trace)
{
    if (is_string($trace)) {
        return $trace;
    }

    $stack = "";
    $count = count($trace);
    foreach ($trace as $index => $frame) {
        if (is_string($frame)) {
            $stack .= $frame . "\n";
            continue;
        }
        $file = get_key_value('file', $frame, '');
        $line = get_key_value('line', $frame, '');
        $func = get_key_value('function', $frame, '');
        $class = get_key_value('class', $frame, '');
        $type = get_key_value('type', $frame, '');
        $stack .= "#$index $file($line): $class$type$func\n";
    }
    return $stack;
}

set_error_handler('custom_error_handler', E_ALL);

set_exception_handler('custom_exception_handler');

One final touchup. When navigating between errors using the arrow keys or prior/next buttons, the error details and dates are not loading the next error. I’m already passing the id to the date and details components. It looks like I had to implement OnChanges.

ngOnChanges(changes: SimpleChanges): void {
    if (changes.hasOwnProperty('id')) {
      this.loadData(0, this.pageSize);
    }
  }

Small potatoes

I figured out that I lost the ellipses on the messages once I assigned the message class to the table cell. I got the ellipses back by assigning them to the div tag within the cell.

Hash Images seemed a bit excessive in resource usage with canvas and generating data urls. I decided to try to do it with CSS. I first started with trying to read attributes as a number attr(data-hue-1), but the hsl class complained that it was not a number. I then created a function to parse the string value as a number, but it was reading the code “attr(data-hue-1)” rather than the evaluated value. Rather than creating a color based on the hue, I decided to pass a whole color as an attribute, but I still ran into trouble. I threw in the towel and decided to calculate the entire linear gradient in JavaScript/TypeScript instead.

<div
  class="image-hash"
  [style.background-image]="linearGradient(item.message_hash)"
></div>

I was able to create four vertical bars. I tried to create a similar grid of 2×2 cells, but my browser didn’t seem to work with multiple linear gradients. I think what I’ve got now works fine.

I think I’m good to go now.

Angular

After spending the past week learning Angular, it’s find that it’s very noisy. It feels like it makes things much more difficult then they have to be. What I mean is that I’m constantly importing many resources just to do simple things like initialization, reading properties, and detecting property changes. I’m still learning the ins and outs, so perhaps I’m overlooking something.

Wrap Up

Today was mostly filled with small tweaks, cleanup, and automating the build process.

  • Created FTP account for deployment
  • Notified Hostinger support staff that previous issue has been resolved
  • Setup secrets and variables in build server for deployment configuration
  • Created YAML file for github action workflow to build Angular web application and deploy both it and PHP files
  • Explored various ways to copy and archive files prefixed with dots
    • cp, rsync, tar, and mv commands
  • Fixed a bug where the wrong file was called to transfer log files
  • Log the requested URL with errors
  • Log stack-trace in custom error handler
  • Fix logging stack-trace in custom exception handler
  • Load dates/details when navigating between error logs
  • Refresh log list after logs imported or search button is clicked without any changes
  • Restore missing ellipses on truncated messages
  • Change hash images to linear gradients

Tomorrow I get to start working on the UI for the secrets manager.

Discover more from Lewis Moten

Subscribe now to keep reading and get access to the full archive.

Continue reading