Skip to content

Commit

Permalink
Initial Code.
Browse files Browse the repository at this point in the history
  • Loading branch information
muhdfaiz committed Nov 25, 2020
0 parents commit 64664ef
Show file tree
Hide file tree
Showing 14 changed files with 1,131 additions and 0 deletions.
74 changes: 74 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# This is a basic workflow to help you get started with Actions

name: testing

# Controls when the action will run.
on:
# Triggers the workflow on push or pull request events but only for the master branch
push:
branches: [ master ]
pull_request:
branches: [ master ]

workflow_dispatch:

jobs:
test:
runs-on: ${{ matrix.os }}

services:
mysql:
image: mysql:5.7
env:
MYSQL_ALLOW_EMPTY_PASSWORD: yes
MYSQL_USER: root
MYSQL_DATABASE: test_db
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3

strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest]
php: [ 7.4 ]
laravel: [ 6.*, 7.*, 8.* ]
include:
- laravel: 6.*
testbench: 4.*
- laravel: 7.*
testbench: 5.*
- laravel: 8.*
testbench: 6.*

name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} - ${{ matrix.os }}

steps:
- uses: actions/checkout@v2

- name: Cache dependencies
uses: actions/cache@v2
with:
path: ~/.composer/cache/files
key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick
coverage: none

- name: Install dependencies
run: |
composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update
composer update --prefer-dist --no-interaction --no-suggest
- name: Execute tests
env:
DB_CONNECTION: mysql
DB_DATABASE: test_db
DB_PORT: ${{ job.services.mysql.ports[3306] }}
DB_USER: root
DB_HOST: 127.0.0.1
run: vendor/bin/phpunit
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/vendor
composer.phar
composer.lock
.phpunit.result.cache
.php_cs.cache
.idea
48 changes: 48 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"name": "muhdfaiz/laravel-tail-db",
"description": "Provide artisan command to tail database query. Able to automatically run explain command for each of query received.",
"keywords": [
"laravel",
"tail",
"log",
"explain",
"query",
"database",
"tail-db"
],
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Muhammad Faiz",
"email": "[email protected]"
}
],
"require": {
"php": "^7.0",
"symfony/process": "^4.0|^5.0"
},
"require-dev": {
"phpunit/phpunit": "^8.0|^9.0",
"laravel/framework": "^6.0|^7.0|^8.0",
"orchestra/testbench": "^4.0|^5.0|^6.0"
},
"autoload": {
"psr-4": {
"Muhdfaiz\\LaravelTailDb\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Muhdfaiz\\LaravelTailDb\\Tests\\": "tests"
}
},
"extra": {
"laravel": {
"providers": [
"Muhdfaiz\\LaravelTailDb\\TailDatabaseServiceProvider"
]
}
},
"prefer-stable": true
}
84 changes: 84 additions & 0 deletions config/tail-db.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

return [
/*
|--------------------------------------------------------------------------
| Laravel Tail Database Switch
|--------------------------------------------------------------------------
|
| This option used to enable or disable the Laravel Tail DB watcher.
| If enabled, every sql query executed from the application will
| be captured and store in the directory you based on the config.
|
*/
'enabled' => env('TAIL_DB_ENABLED', true),

/*
|--------------------------------------------------------------------------
| Duration of time considered slow query.
|--------------------------------------------------------------------------
|
| This option used to tell Laravel Tail DB if the query slow or not.
| For example, if you specify 2000ms and the last query executed take
| more than 2000ms, Laravel Tail DB will highlight the time with red color.
| If the query below than 2000ms Laravel Tail DB will highlight with green color.
| The value must be in milliseconds.
*/
'slow_duration' => env('TAIL_DB_SLOW_DURATION', 3000),

/*
|--------------------------------------------------------------------------
| Ignore queries
|--------------------------------------------------------------------------
|
| You can specify the keyword to skip the tailing.
| Laravel Tail DB will check if the query contain those keyword or not.
| If exist, Laravel Tail DB will skip recording to log file.
| The key keyword must be separated by comma.
| Example: alter table}drop table. (Separated by |)
|
*/
'ignore_query_keyword' => env('TAIL_DB_IGNORE_QUERY_KEYWORD', ''),

/*
|--------------------------------------------------------------------------
| Filename to store mysql queries log
|--------------------------------------------------------------------------
|
| Default filename is database.log
|
*/
'filename' => env ('TAIL_DB_FILENAME', 'database.log'),

/*
|--------------------------------------------------------------------------
| Path to store sql queries log.
|--------------------------------------------------------------------------
|
| Default path is inside storage/logs.
|
*/
'path' => env ('TAIL_DB_PATH', storage_path('logs')),

/*
|--------------------------------------------------------------------------
| Show explain sql during the tail.
|--------------------------------------------------------------------------
|
| By default every sql query executed, laravel tail db will run explain
| command. Useful if you want to troubleshooting performance issue.
| If turn off, Laravel Tail DB only show the query executed, the time and
| the location where the query executed.
*/
'show_explain' => env ('TAIL_DB_SHOW_EXPLAIN', true),

/*
|--------------------------------------------------------------------------
| Clear log
|--------------------------------------------------------------------------
|
| When you end the tail:database command or every time Laravel Tail DB
| received new data, the data in the log will be cleared.
*/
'clear_log' => env ('TAIL_DB_CLEAR_LOG', true),
];
25 changes: 25 additions & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php"
backupGlobals="false"
backupStaticAttributes="false"
colors="true"
verbose="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false">
<testsuites>
<testsuite name="Laravel Tail Database Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory suffix=".php">src/</directory>
</whitelist>
</filter>
<php>
<env name="APP_KEY" value="base64:m+pDa0MKS1KpMlxzzdVEaqFHysv3IPhrx/3TFSWBqJA=" />
</php>
</phpunit>
145 changes: 145 additions & 0 deletions src/DatabaseWatcher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<?php

namespace Muhdfaiz\LaravelTailDb;

use Illuminate\Contracts\Foundation\Application;
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;

class DatabaseWatcher
{
/**
* Register the watcher.
*
* @param Application $app
* @return void
*/
public function register($app)
{
$app['events']->listen(QueryExecuted::class, [$this, 'recordQuery']);
}

/**
* Record a query was executed.
*
* @param QueryExecuted $event
* @return void
*/
public function recordQuery(QueryExecuted $event)
{
// Check if the query contain keywords user want to ignore based on the config.
if (config('tail-db.ignore_query_keyword') && preg_match('(' . config('tail-db.ignore_query_keyword') . ')', strtolower($event->sql)) === 1) {
return;
}

// Need to skip recording if the query received related to migration.
// For example, create table, drop table and alter table.
if ($this->checkIfQueryRelatedToMigration(strtolower($event->sql))) {
return;
};

// Get stack trace.
$caller = $this->getCallerFromStackTrace();

// Set data before storing in the database log file.
$data = [
'connection' => $event->connectionName,
'bindings' => $event->bindings,
'sql' => strtolower($this->replaceBindings($event)),
'time' => number_format($event->time, 2, '.', ''),
'file' => $caller['file'],
'line' => $caller['line'],
];

// Store the data in the log file.
Log::channel('taildb')->info(json_encode($data));
}

/**
* Find the first frame in the stack trace outside of Laravel vendor.
*
* @return array
*/
protected function getCallerFromStackTrace()
{
$trace = collect(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS))->forget(0);
return $trace->first(function ($frame) {
if (! isset($frame['file'])) {
return false;
}

if (Str::contains($frame['file'],'vendor'.DIRECTORY_SEPARATOR.'laravel')) {
return false;
}

return $frame['file'];
});
}

/**
* Format the given bindings to strings.
*
* @param QueryExecuted $event
*
* @return array
*/
protected function formatBindings($event)
{
return $event->connection->prepareBindings($event->bindings);
}

/**
* Replace the placeholders with the actual bindings.
*
* @param QueryExecuted $event
*
* @return string
*/
public function replaceBindings($event)
{
$sql = $event->sql;

foreach ($this->formatBindings($event) as $key => $binding) {
$regex = is_numeric($key)
? "/\?(?=(?:[^'\\\']*'[^'\\\']*')*[^'\\\']*$)/"
: "/:{$key}(?=(?:[^'\\\']*'[^'\\\']*')*[^'\\\']*$)/";

if ($binding === null) {
$binding = 'null';
} elseif (! is_int($binding) && ! is_float($binding)) {
$binding = $event->connection->getPdo()->quote($binding);
}

$sql = preg_replace($regex, $binding, $sql, 1);
}

return $sql;
}

/**
* Check if query related to migration.
*
* @param $query
*
* @return bool
*/
private function checkIfQueryRelatedToMigration($query)
{
$query = strtolower($query);

// Get ignore query from the config in case user want to ignore other query.


// Default query that will skip by Laravel Tail DB.
if (strpos($query, 'explain') !== false || strpos($query, 'alter table') !== false
|| strpos($query, 'create table') !== false || strpos($query, 'drop table') !== false
|| strpos($query, 'create index') !== false || strpos($query, 'create unique index') !== false
|| strpos($query, 'information_schema') !== false
) {
return true;
}

return false;
}
}
Loading

0 comments on commit 64664ef

Please sign in to comment.