- Automatic Tracking
- Manual Metrics Management
- Customizing the Dashboard View
- Dashboard Features
- Programmatic Usage
- Direct Entity Access
- HTTP Status Code Tracking
- Performance Notifications
- Best Practices
- Examples
The bundle automatically tracks route performance metrics for all routes (except ignored ones) in configured environments.
-
When a request comes in, the
PerformanceMetricsSubscriberstarts tracking:- Request start time
- Database query logger
-
When the request finishes, it:
- Calculates request execution time
- Collects query count and execution time
- Saves metrics to database
-
Metrics are only updated if they're worse (higher time or more queries)
Simply make requests to your routes, and metrics will be automatically tracked:
# Make a request
curl http://localhost:8000/app_home
# Metrics are automatically saved to databaseWhen enable_access_records: true is enabled in configuration, the bundle also stores
one access record per request in the routes_data_records table (RouteDataRecord entity).
These records contain:
accessedAt(timestamp)statusCoderesponseTimetotalQueries,queryTime,memoryUsage(when tracked)requestId(unique per request, for deduplication)referer(HTTP Referer header when present)userIdentifier,userId(logged-in user whentrack_useris true)
They power the Access Statistics and Access Records pages:
/performance/access-statistics– charts and heatmaps (by hour, day of week, month)/performance/access-records– paginated list of individual hits with filters (date range, route, status code, query time, memory, referer, user). Columns are sortable; the Path column shows a clickable link to the exact URL.
The main RouteData entity remains an aggregate view (per route + environment), while
RouteDataRecord is the normalized temporal log used for seasonality analysis.
Use the command to manually set or update route metrics:
# Set basic metrics
php bin/console nowo:performance:set-route app_home \
--env=dev \
--request-time=0.5 \
--queries=10
# Set all metrics
php bin/console nowo:performance:set-route app_user_show \
--env=prod \
--request-time=1.2 \
--queries=25 \
--query-time=0.3 \
--params='{"id":123}'route(required) - Route name--env, -e- Environment (default:dev)--request-time, -r- Request time in seconds (float)--queries- Total number of queries (integer)--query-time, -t- Total query execution time in seconds (float)--memory, -m- Peak memory usage in bytes (integer)--params, -p- Route parameters as JSON string
The performance dashboard is built using reusable Twig components, allowing you to customize specific parts without replacing the entire template.
The dashboard supports two CSS frameworks: Bootstrap (default) and Tailwind CSS. You can choose which one to use via configuration:
# config/packages/nowo_performance.yaml
nowo_performance:
dashboard:
template: 'bootstrap' # or 'tailwind'The dashboard consists of three main components, available in both Bootstrap and Tailwind versions:
Bootstrap components:
- Statistics Component (
_statistics_bootstrap.html.twig) - Displays performance statistics cards - Filters Component (
_filters_bootstrap.html.twig) - Contains the filtering form - Routes Table Component (
_routes_table_bootstrap.html.twig) - Shows the routes data table
Tailwind components:
- Statistics Component (
_statistics_tailwind.html.twig) - Displays performance statistics cards - Filters Component (
_filters_tailwind.html.twig) - Contains the filtering form - Routes Table Component (
_routes_table_tailwind.html.twig) - Shows the routes data table
You can override individual components by creating them in your project's template directory:
Path structure:
templates/
bundles/
NowoPerformanceBundle/
Performance/
components/
_statistics_bootstrap.html.twig # Override Bootstrap statistics
_filters_bootstrap.html.twig # Override Bootstrap filters
_routes_table_bootstrap.html.twig # Override Bootstrap table
_statistics_tailwind.html.twig # Override Tailwind statistics
_filters_tailwind.html.twig # Override Tailwind filters
_routes_table_tailwind.html.twig # Override Tailwind table
Example: Custom Bootstrap Statistics Component
{# templates/bundles/NowoPerformanceBundle/Performance/components/_statistics_bootstrap.html.twig #}
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-body">
<h5>Custom Statistics</h5>
<p>Total Routes: {{ stats.total_routes }}</p>
<p>Total Queries: {{ stats.total_queries }}</p>
</div>
</div>
</div>
</div>Example: Custom Tailwind Statistics Component
{# templates/bundles/NowoPerformanceBundle/Performance/components/_statistics_tailwind.html.twig #}
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4 mb-8">
<div class="bg-white overflow-hidden shadow rounded-lg p-5">
<h5 class="text-lg font-semibold">Custom Statistics</h5>
<p>Total Routes: {{ stats.total_routes }}</p>
<p>Total Queries: {{ stats.total_queries }}</p>
</div>
</div>Example: Custom Routes Table
{# templates/bundles/NowoPerformanceBundle/Performance/components/_routes_table.html.twig #}
<div class="row">
<div class="col-12">
<div class="custom-table">
{% for route in routes %}
<div class="route-item">
<strong>{{ route.name }}</strong>
<span>{{ (route.requestTime * 1000)|number_format(2) }} ms</span>
</div>
{% endfor %}
</div>
</div>
</div>If you prefer to replace the entire dashboard template, create:
templates/bundles/NowoPerformanceBundle/Performance/index.html.twig
This will completely override the default template. You can still use the components if needed:
{# Your custom template #}
{% extends 'base.html.twig' %}
{% block content %}
<h1>My Custom Dashboard</h1>
{# Use the default statistics component #}
{% include '@NowoPerformanceBundle/Performance/components/_statistics.html.twig' %}
{# Or use your custom component #}
{% include 'bundles/NowoPerformanceBundle/Performance/components/_statistics.html.twig' %}
{% endblock %}All components receive the same variables from the controller:
routes- Array of RouteData entitiesstats- Statistics array with:total_routes- Total number of routestotal_queries- Total number of queriesavg_request_time- Average request time (in seconds)avg_query_time- Average query time (in seconds)max_request_time- Maximum request time (in seconds)max_query_time- Maximum query time (in seconds)max_queries- Maximum query count
environment- Current environment filtercurrentRoute- Current route name filtersortBy- Current sort fieldorder- Current sort order (ASC/DESC)limit- Current result limitenvironments- Array of available environments
The bundle ships translations under the nowo_performance domain in src/Resources/translations/.
To override any key from your Symfony application:
- Create (or edit) your app translation file, for example:
translations/nowo_performance.en.yamltranslations/nowo_performance.es.yaml
- Copy only the keys you want to customize.
- Clear cache in dev if needed (
php bin/console cache:clear --env=dev --no-warmup).
Example:
# translations/nowo_performance.en.yaml
dashboard:
title: "My Custom Performance Dashboard"The dashboard includes export functionality to download performance data:
CSV Export:
- Click "Export CSV" button in dashboard header
- Downloads a CSV file with all current filtered data
- Includes: route name, environment, metrics, memory usage, access count, timestamps
- UTF-8 encoding with BOM for Excel compatibility
JSON Export:
- Click "Export JSON" button in dashboard header
- Downloads a JSON file with all current filtered data
- Includes metadata: environment, export date
- Structured format for programmatic use
Export respects filters:
- Current environment filter
- Route name filters
- Time/query range filters
- Date range filters
- Current sorting
Access records export (CSV / JSON):
- On the Access Records page (temporal records per route), use Export Records (CSV) or Export Records (JSON).
- Exports individual
RouteDataRecordrows (id, route name, path, params, environment, accessed at, status code, response time, total queries, query time, memory usage, referer, user_identifier, user_id). - Uses the same filters as the records page: environment, date range, route, status code, query time, memory, referer, user.
- Requires
enable_access_records: true. Subject to dashboard roles if configured.
When enable_record_management is enabled:
Delete Individual Records:
- Delete button appears in Actions column for each record
- Confirmation dialog before deletion
- CSRF protection
- Redirects to referer after deletion
- Cache is automatically invalidated
Clear All Records:
- "Clear All Records" button in dashboard header
- Optionally filters by environment
- Confirmation dialog before clearing
- CSRF protection
- Redirects to referer after clearing
Delete records by filter:
- Available on Access Records and Access Statistics pages when
enable_record_managementis enabled - Deletes all
RouteDataRecordrows matching the current filters (environment, date range, route, status code, query time, memory, referer, user) - Confirmation dialog before deletion
- CSRF protection (
delete_records_by_filtertoken) - Redirects back to the page you came from (Access Records or Access Statistics)
When enable_review_system is enabled:
Mark Records as Reviewed:
- Review button (check icon) appears for unreviewed records
- Modal form to mark as reviewed
- Options:
- Queries Improved: Yes / No / Not specified
- Time Improved: Yes / No / Not specified
- Reviewer username is automatically recorded
- Review date is automatically set
Edit Existing Review:
- Edit review button (pencil icon) appears for already reviewed records
- Same modal opens with the form pre-filled with current values
- Change "Queries improved" or "Time improved" and save; flash message "Review updated"
Review Status Display:
- "Reviewed" badge for reviewed records
- "Pending" badge for unreviewed records
- Improvement indicators:
- Green badge if improved
- Red badge if not improved
- Not shown if not specified
Filtering by Review Status:
- Can be added to filters (future enhancement)
use Nowo\PerformanceBundle\Service\PerformanceMetricsService;
class YourController
{
public function __construct(
private readonly PerformanceMetricsService $metricsService
) {
}
public function index(): Response
{
// Get route data
$routeData = $this->metricsService->getRouteData('app_home', 'dev');
if ($routeData) {
$requestTime = $routeData->getRequestTime();
$queryCount = $routeData->getTotalQueries();
}
// ...
}
}// Get specific route data
$routeData = $metricsService->getRouteData('app_home', 'dev');
if ($routeData) {
echo "Request Time: " . $routeData->getRequestTime() . "s\n";
echo "Queries: " . $routeData->getTotalQueries() . "\n";
echo "Query Time: " . $routeData->getQueryTime() . "s\n";
}// Get all routes for an environment
$routes = $metricsService->getRoutesByEnvironment('dev');
foreach ($routes as $route) {
echo $route->getName() . ": " . $route->getRequestTime() . "s\n";
}// Get top 10 worst performing routes
$worstRoutes = $metricsService->getWorstPerformingRoutes('dev', 10);
foreach ($worstRoutes as $route) {
echo sprintf(
"%s: %.4fs, %d queries\n",
$route->getName(),
$route->getRequestTime(),
$route->getTotalQueries()
);
}You can also access the entity directly via Doctrine:
use Nowo\PerformanceBundle\Entity\RouteData;
use Doctrine\ORM\EntityManagerInterface;
class YourService
{
public function __construct(
private readonly EntityManagerInterface $entityManager
) {
}
public function getMetrics(): array
{
$repository = $this->entityManager->getRepository(RouteData::class);
// Custom queries
$routes = $repository->findByEnvironment('dev');
return $routes;
}
}The bundle automatically tracks HTTP status codes for each route and calculates ratios.
Configure which status codes to track:
# config/packages/nowo_performance.yaml
nowo_performance:
track_status_codes: [200, 404, 500, 503]use Nowo\PerformanceBundle\Service\PerformanceMetricsService;
$routeData = $metricsService->getRouteData('app_home', 'dev');
if ($routeData) {
// Get status codes counts
$statusCodes = $routeData->getStatusCodes(); // ['200' => 100, '404' => 5, '500' => 2]
// Get count for specific status code
$count200 = $routeData->getStatusCodeCount(200); // 100
// Get ratio (percentage) for specific status code
$ratio200 = $routeData->getStatusCodeRatio(200); // 93.46 (percentage)
// Get total responses tracked
$total = $routeData->getTotalResponses(); // 107
}Status codes are displayed in the dashboard with:
- Color-coded badges (green for 200, red for errors)
- Percentage ratios (e.g., "200: 95.5%")
- Tooltips with absolute counts (e.g., "100 de 105 (95.5%)")
The bundle can send automatic notifications when performance thresholds are exceeded.
See NOTIFICATIONS.md for complete documentation.
Quick example:
# config/packages/nowo_performance.yaml
nowo_performance:
notifications:
enabled: true
email:
enabled: true
from: 'noreply@example.com'
to: ['admin@example.com']
slack:
enabled: true
webhook_url: 'https://hooks.slack.com/services/YOUR/WEBHOOK/URL'Notifications are automatically sent when:
- Request time exceeds warning/critical thresholds
- Query count exceeds warning/critical thresholds
- Memory usage exceeds warning/critical thresholds
- Enable only in dev/test - Don't track in production unless needed
- Use ignore_routes - Ignore profiler and debug routes
- Monitor worst routes - Regularly check worst performing routes
- Set baseline metrics - Use the command to set baseline metrics for important routes
- Review periodically - Review metrics to identify performance issues
- Configure status code tracking - Track relevant HTTP status codes for your application
- Set up notifications - Configure email/Slack/Teams alerts for production environments
- Sub-request tracking - Enable
track_sub_requestsonly if you need to monitor ESI, fragments, or includes separately (increases database storage)
# config/packages/nowo_performance.yaml
nowo_performance:
environments: ['dev', 'staging']
ignore_routes:
- '_wdt'
- '_profiler'# config/packages/prod/nowo_performance.yaml
nowo_performance:
enabled: true
environments: ['prod']
track_queries: true
track_request_time: true# Use dedicated connection for metrics
nowo_performance:
connection: 'metrics'