Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3575803252 | ||
| d03900cefe | |||
|
|
6d49639845 | ||
| d4543cf4b9 |
@@ -57,6 +57,8 @@ services:
|
||||
- '8000:8000' # Bugsink error tracking HTTP (ADR-015)
|
||||
- '8443:8443' # Bugsink error tracking HTTPS (ADR-015)
|
||||
environment:
|
||||
# Timezone: PST (America/Los_Angeles) for consistent log timestamps
|
||||
- TZ=America/Los_Angeles
|
||||
# Core settings
|
||||
- NODE_ENV=development
|
||||
# Database - use service name for Docker networking
|
||||
@@ -122,6 +124,10 @@ services:
|
||||
ports:
|
||||
- '5432:5432'
|
||||
environment:
|
||||
# Timezone: PST (America/Los_Angeles) for consistent log timestamps
|
||||
TZ: America/Los_Angeles
|
||||
# PostgreSQL timezone setting (used by log_timezone and timezone parameters)
|
||||
PGTZ: America/Los_Angeles
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: flyer_crawler_dev
|
||||
@@ -142,6 +148,8 @@ services:
|
||||
postgres
|
||||
-c config_file=/var/lib/postgresql/data/postgresql.conf
|
||||
-c hba_file=/var/lib/postgresql/data/pg_hba.conf
|
||||
-c timezone=America/Los_Angeles
|
||||
-c log_timezone=America/Los_Angeles
|
||||
-c log_min_messages=notice
|
||||
-c client_min_messages=notice
|
||||
-c logging_collector=on
|
||||
@@ -175,6 +183,9 @@ services:
|
||||
user: root
|
||||
ports:
|
||||
- '6379:6379'
|
||||
environment:
|
||||
# Timezone: PST (America/Los_Angeles) for consistent log timestamps
|
||||
TZ: America/Los_Angeles
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
# Create log volume for Logstash access (ADR-050)
|
||||
|
||||
@@ -121,7 +121,8 @@ input {
|
||||
# ============================================================================
|
||||
# Captures PostgreSQL log output including fn_log() structured JSON messages.
|
||||
# PostgreSQL is configured to write logs to /var/log/postgresql/ (shared volume).
|
||||
# Log format: "2026-01-22 00:00:00 UTC [5724] postgres@flyer_crawler_dev LOG: message"
|
||||
# Log format: "2026-01-22 14:30:00 PST [5724] postgres@flyer_crawler_dev LOG: message"
|
||||
# Note: Timestamps are in PST (America/Los_Angeles) timezone as configured in compose.dev.yml
|
||||
file {
|
||||
path => "/var/log/postgresql/*.log"
|
||||
type => "postgres"
|
||||
@@ -226,10 +227,11 @@ filter {
|
||||
# PostgreSQL Log Processing (ADR-050)
|
||||
# ============================================================================
|
||||
# PostgreSQL log format in dev container:
|
||||
# "2026-01-22 00:00:00 UTC [5724] postgres@flyer_crawler_dev LOG: message"
|
||||
# "2026-01-22 07:06:03 UTC [19851] postgres@flyer_crawler_dev ERROR: column "id" does not exist"
|
||||
# "2026-01-22 14:30:00 PST [5724] postgres@flyer_crawler_dev LOG: message"
|
||||
# "2026-01-22 15:06:03 PST [19851] postgres@flyer_crawler_dev ERROR: column "id" does not exist"
|
||||
# Note: Timestamps are in PST (America/Los_Angeles) timezone
|
||||
if [type] == "postgres" {
|
||||
# Parse PostgreSQL log prefix with UTC timezone
|
||||
# Parse PostgreSQL log prefix with timezone (PST in dev, may vary in prod)
|
||||
grok {
|
||||
match => { "message" => "%{YEAR}-%{MONTHNUM}-%{MONTHDAY} %{TIME} %{WORD:pg_timezone} \[%{POSINT:pg_pid}\] %{DATA:pg_user}@%{DATA:pg_database} %{WORD:pg_level}: ?%{GREEDYDATA:pg_message}" }
|
||||
tag_on_failure => ["_postgres_grok_failure"]
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
# This file is mounted into the PostgreSQL container to enable structured logging
|
||||
# from database functions via fn_log()
|
||||
|
||||
# Timezone: PST (America/Los_Angeles) for consistent log timestamps
|
||||
timezone = 'America/Los_Angeles'
|
||||
log_timezone = 'America/Los_Angeles'
|
||||
|
||||
# Enable logging to files for Logstash pickup
|
||||
logging_collector = on
|
||||
log_destination = 'stderr'
|
||||
|
||||
@@ -272,14 +272,41 @@ podman-compose -f compose.dev.yml down
|
||||
|
||||
Key environment variables are set in `compose.dev.yml`:
|
||||
|
||||
| Variable | Value | Purpose |
|
||||
| ----------------- | ----------------------------- | -------------------- |
|
||||
| `NODE_ENV` | `development` | Environment mode |
|
||||
| `DB_HOST` | `postgres` | PostgreSQL hostname |
|
||||
| `REDIS_URL` | `redis://redis:6379` | Redis connection URL |
|
||||
| `FRONTEND_URL` | `https://localhost` | CORS origin |
|
||||
| `SENTRY_DSN` | `http://...@127.0.0.1:8000/1` | Backend Bugsink DSN |
|
||||
| `VITE_SENTRY_DSN` | `http://...@127.0.0.1:8000/2` | Frontend Bugsink DSN |
|
||||
| Variable | Value | Purpose |
|
||||
| ----------------- | ----------------------------- | --------------------------- |
|
||||
| `TZ` | `America/Los_Angeles` | Timezone (PST) for all logs |
|
||||
| `NODE_ENV` | `development` | Environment mode |
|
||||
| `DB_HOST` | `postgres` | PostgreSQL hostname |
|
||||
| `REDIS_URL` | `redis://redis:6379` | Redis connection URL |
|
||||
| `FRONTEND_URL` | `https://localhost` | CORS origin |
|
||||
| `SENTRY_DSN` | `http://...@127.0.0.1:8000/1` | Backend Bugsink DSN |
|
||||
| `VITE_SENTRY_DSN` | `http://...@127.0.0.1:8000/2` | Frontend Bugsink DSN |
|
||||
|
||||
### Timezone Configuration
|
||||
|
||||
All dev container services are configured to use PST (America/Los_Angeles) timezone for consistent log timestamps:
|
||||
|
||||
| Service | Configuration | Notes |
|
||||
| ---------- | ------------------------------------------------ | ------------------------------ |
|
||||
| App | `TZ=America/Los_Angeles` in compose.dev.yml | Also set via dev-entrypoint.sh |
|
||||
| PostgreSQL | `timezone` and `log_timezone` in postgres config | Logs timestamps in PST |
|
||||
| Redis | `TZ=America/Los_Angeles` in compose.dev.yml | Alpine uses TZ env var |
|
||||
| PM2 | `TZ` in ecosystem.dev.config.cjs | Pino timestamps use local time |
|
||||
|
||||
**Verifying Timezone**:
|
||||
|
||||
```bash
|
||||
# Check container timezone
|
||||
podman exec flyer-crawler-dev date
|
||||
|
||||
# Check PostgreSQL timezone
|
||||
podman exec flyer-crawler-postgres psql -U postgres -c "SHOW timezone;"
|
||||
|
||||
# Check Redis log timestamps
|
||||
MSYS_NO_PATHCONV=1 podman exec flyer-crawler-redis cat /var/log/redis/redis-server.log | head -5
|
||||
```
|
||||
|
||||
**Note**: If you need UTC timestamps for production compatibility, change `TZ=UTC` in compose.dev.yml and restart containers.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -101,6 +101,7 @@ MSYS_NO_PATHCONV=1 podman exec flyer-crawler-dev ls -la /var/log/redis/
|
||||
| NGINX logs missing | Output directory | `ls -lh /var/log/logstash/nginx-access-*.log` |
|
||||
| Redis logs missing | Shared volume | Dev: Check `redis_logs` volume mounted; Prod: Check `/var/log/redis/redis-server.log` exists |
|
||||
| High disk usage | Log rotation | Verify `/etc/logrotate.d/logstash` configured |
|
||||
| varchar(7) error | Level validation | Add Ruby filter to validate/normalize `sentry_level` before output |
|
||||
|
||||
## Related Documentation
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ This runbook provides step-by-step diagnostics and solutions for common Logstash
|
||||
| Wrong Bugsink project | Environment detection failed | Verify `pg_database` field extraction |
|
||||
| 403 authentication error | Missing/wrong DSN key | Check `X-Sentry-Auth` header |
|
||||
| 500 error from Bugsink | Invalid event format | Verify `event_id` and required fields |
|
||||
| varchar(7) constraint | Unresolved `%{sentry_level}` | Add Ruby filter for level validation |
|
||||
|
||||
---
|
||||
|
||||
@@ -385,7 +386,88 @@ systemctl status logstash
|
||||
|
||||
---
|
||||
|
||||
### Issue 7: Log File Rotation Issues
|
||||
### Issue 7: Level Field Constraint Violation (varchar(7))
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- Bugsink returns HTTP 500 errors
|
||||
- PostgreSQL errors: `value too long for type character varying(7)`
|
||||
- Events fail to insert with literal `%{sentry_level}` string (16 characters)
|
||||
|
||||
**Root Cause:**
|
||||
|
||||
When Logstash cannot determine the log level (no error patterns matched), the `sentry_level` field remains as the unresolved placeholder `%{sentry_level}`. Bugsink's PostgreSQL schema has a `varchar(7)` constraint on the level field.
|
||||
|
||||
Valid Sentry levels (all <= 7 characters): `fatal`, `error`, `warning`, `info`, `debug`
|
||||
|
||||
**Diagnosis:**
|
||||
|
||||
```bash
|
||||
# Check for HTTP 500 responses in Logstash logs
|
||||
podman exec flyer-crawler-dev cat /var/log/logstash/logstash.log | grep "500"
|
||||
|
||||
# Check Bugsink for constraint violation errors
|
||||
# Via MCP:
|
||||
mcp__localerrors__list_issues({ project_id: 1, status: 'unresolved' })
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
|
||||
Add a Ruby filter block in `docker/logstash/bugsink.conf` to validate and normalize the `sentry_level` field before sending to Bugsink:
|
||||
|
||||
```ruby
|
||||
# Add this AFTER all mutate filters that set sentry_level
|
||||
# and BEFORE the output section
|
||||
|
||||
ruby {
|
||||
code => '
|
||||
level = event.get("sentry_level")
|
||||
# Check if level is invalid (nil, empty, contains placeholder, or too long)
|
||||
if level.nil? || level.to_s.empty? || level.to_s.include?("%{") || level.to_s.length > 7
|
||||
# Default to "error" for error-tagged events, "info" otherwise
|
||||
if event.get("tags")&.include?("error")
|
||||
event.set("sentry_level", "error")
|
||||
else
|
||||
event.set("sentry_level", "info")
|
||||
end
|
||||
else
|
||||
# Normalize to lowercase and validate
|
||||
normalized = level.to_s.downcase
|
||||
valid_levels = ["fatal", "error", "warning", "info", "debug"]
|
||||
unless valid_levels.include?(normalized)
|
||||
normalized = "error"
|
||||
end
|
||||
event.set("sentry_level", normalized)
|
||||
end
|
||||
'
|
||||
}
|
||||
```
|
||||
|
||||
**Key validations performed:**
|
||||
|
||||
1. Checks for nil or empty values
|
||||
2. Detects unresolved placeholders (`%{...}`)
|
||||
3. Enforces 7-character maximum length
|
||||
4. Normalizes to lowercase
|
||||
5. Validates against allowed Sentry levels
|
||||
6. Defaults to "error" for error-tagged events, "info" otherwise
|
||||
|
||||
**Verification:**
|
||||
|
||||
```bash
|
||||
# Restart Logstash
|
||||
podman exec flyer-crawler-dev systemctl restart logstash
|
||||
|
||||
# Generate a test log that triggers the filter
|
||||
podman exec flyer-crawler-dev pm2 restart flyer-crawler-api-dev
|
||||
|
||||
# Check no new HTTP 500 errors
|
||||
podman exec flyer-crawler-dev cat /var/log/logstash/logstash.log | tail -50 | grep -E "(500|error)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Issue 8: Log File Rotation Issues
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
|
||||
@@ -801,6 +801,158 @@ podman exec flyer-crawler-dev psql -U postgres -h postgres -c "\l" | grep bugsin
|
||||
ssh root@projectium.com "cd /opt/bugsink && bugsink-manage check"
|
||||
```
|
||||
|
||||
### PostgreSQL Sequence Out of Sync (Duplicate Key Errors)
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- Bugsink throws `duplicate key value violates unique constraint "projects_project_pkey"`
|
||||
- Error detail shows: `Key (id)=(1) already exists`
|
||||
- New projects or other entities fail to create
|
||||
|
||||
**Root Cause:**
|
||||
|
||||
PostgreSQL sequences can become out of sync with actual data after:
|
||||
|
||||
- Manual data insertion or database seeding
|
||||
- Restoring from backup
|
||||
- Copying data between environments
|
||||
|
||||
The sequence generates IDs that already exist in the table.
|
||||
|
||||
**Diagnosis:**
|
||||
|
||||
```bash
|
||||
# Dev Container - Check sequence vs max ID
|
||||
podman exec flyer-crawler-dev psql -U bugsink -h postgres -d bugsink -c "
|
||||
SELECT
|
||||
(SELECT MAX(id) FROM projects_project) as max_id,
|
||||
(SELECT last_value FROM projects_project_id_seq) as seq_last_value,
|
||||
CASE
|
||||
WHEN (SELECT MAX(id) FROM projects_project) <= (SELECT last_value FROM projects_project_id_seq)
|
||||
THEN 'OK'
|
||||
ELSE 'OUT OF SYNC - Needs reset'
|
||||
END as status;
|
||||
"
|
||||
|
||||
# Production
|
||||
ssh root@projectium.com "cd /opt/bugsink && bugsink-manage dbshell" <<< "
|
||||
SELECT MAX(id) as max_id, (SELECT last_value FROM projects_project_id_seq) as seq_value FROM projects_project;
|
||||
"
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
|
||||
Reset the sequence to the maximum existing ID:
|
||||
|
||||
```bash
|
||||
# Dev Container
|
||||
podman exec flyer-crawler-dev psql -U bugsink -h postgres -d bugsink -c "
|
||||
SELECT setval('projects_project_id_seq', COALESCE((SELECT MAX(id) FROM projects_project), 1), true);
|
||||
"
|
||||
|
||||
# Production
|
||||
ssh root@projectium.com "cd /opt/bugsink && bugsink-manage dbshell" <<< "
|
||||
SELECT setval('projects_project_id_seq', COALESCE((SELECT MAX(id) FROM projects_project), 1), true);
|
||||
"
|
||||
```
|
||||
|
||||
**Verification:**
|
||||
|
||||
After running the fix, verify:
|
||||
|
||||
```bash
|
||||
# Next ID should be max_id + 1
|
||||
podman exec flyer-crawler-dev psql -U bugsink -h postgres -d bugsink -c "
|
||||
SELECT nextval('projects_project_id_seq') - 1 as current_seq_value;
|
||||
"
|
||||
```
|
||||
|
||||
**Prevention:**
|
||||
|
||||
When manually inserting data or restoring backups, always reset sequences:
|
||||
|
||||
```sql
|
||||
-- Generic pattern for any table/sequence
|
||||
SELECT setval('SEQUENCE_NAME', COALESCE((SELECT MAX(id) FROM TABLE_NAME), 1), true);
|
||||
|
||||
-- Common Bugsink sequences that may need reset:
|
||||
SELECT setval('projects_project_id_seq', COALESCE((SELECT MAX(id) FROM projects_project), 1), true);
|
||||
SELECT setval('teams_team_id_seq', COALESCE((SELECT MAX(id) FROM teams_team), 1), true);
|
||||
SELECT setval('releases_release_id_seq', COALESCE((SELECT MAX(id) FROM releases_release), 1), true);
|
||||
```
|
||||
|
||||
### Logstash Level Field Constraint Violation
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- Bugsink errors: `value too long for type character varying(7)`
|
||||
- Errors in Backend API project from Logstash
|
||||
- Log shows `%{sentry_level}` literal string being sent
|
||||
|
||||
**Root Cause:**
|
||||
|
||||
Logstash sends the literal placeholder `%{sentry_level}` (16 characters) to Bugsink when:
|
||||
|
||||
- No error pattern is detected in the log message
|
||||
- The `sentry_level` field is not properly initialized
|
||||
- Bugsink's `level` column has a `varchar(7)` constraint
|
||||
|
||||
Valid Sentry levels are: `fatal`, `error`, `warning`, `info`, `debug` (all <= 7 characters).
|
||||
|
||||
**Diagnosis:**
|
||||
|
||||
```bash
|
||||
# Check for recent level constraint errors in Bugsink
|
||||
# Via MCP:
|
||||
mcp__localerrors__list_issues({ project_id: 1, status: 'unresolved' })
|
||||
|
||||
# Or check Logstash logs for HTTP 500 responses
|
||||
podman exec flyer-crawler-dev cat /var/log/logstash/logstash.log | grep "500"
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
|
||||
The fix requires updating the Logstash configuration (`docker/logstash/bugsink.conf`) to:
|
||||
|
||||
1. Validate `sentry_level` is not nil, empty, or contains placeholder text
|
||||
2. Set a default value of "error" for any error-tagged event without a valid level
|
||||
3. Normalize levels to lowercase
|
||||
|
||||
**Key filter block (Ruby):**
|
||||
|
||||
```ruby
|
||||
ruby {
|
||||
code => '
|
||||
level = event.get("sentry_level")
|
||||
# Check if level is invalid (nil, empty, contains placeholder, or invalid value)
|
||||
if level.nil? || level.to_s.empty? || level.to_s.include?("%{") || level.to_s.length > 7
|
||||
# Default to "error" for error-tagged events, "info" otherwise
|
||||
if event.get("tags")&.include?("error")
|
||||
event.set("sentry_level", "error")
|
||||
else
|
||||
event.set("sentry_level", "info")
|
||||
end
|
||||
else
|
||||
# Normalize to lowercase and validate
|
||||
normalized = level.to_s.downcase
|
||||
valid_levels = ["fatal", "error", "warning", "info", "debug"]
|
||||
unless valid_levels.include?(normalized)
|
||||
normalized = "error"
|
||||
end
|
||||
event.set("sentry_level", normalized)
|
||||
end
|
||||
'
|
||||
}
|
||||
```
|
||||
|
||||
**Verification:**
|
||||
|
||||
After applying the fix:
|
||||
|
||||
1. Restart Logstash: `podman exec flyer-crawler-dev systemctl restart logstash`
|
||||
2. Generate a test error and verify it appears in Bugsink without level errors
|
||||
3. Check no new "value too long" errors appear in the project
|
||||
|
||||
### CSRF Verification Failed
|
||||
|
||||
**Symptoms:** "CSRF verification failed. Request aborted." error when performing actions in Bugsink UI (resolving issues, changing settings, etc.)
|
||||
|
||||
@@ -44,6 +44,8 @@ if (missingVars.length > 0) {
|
||||
// --- Shared Environment Variables ---
|
||||
// These come from compose.dev.yml environment section
|
||||
const sharedEnv = {
|
||||
// Timezone: PST (America/Los_Angeles) for consistent log timestamps
|
||||
TZ: process.env.TZ || 'America/Los_Angeles',
|
||||
NODE_ENV: 'development',
|
||||
DB_HOST: process.env.DB_HOST || 'postgres',
|
||||
DB_PORT: process.env.DB_PORT || '5432',
|
||||
@@ -160,6 +162,8 @@ module.exports = {
|
||||
min_uptime: '5s',
|
||||
// Environment
|
||||
env: {
|
||||
// Timezone: PST (America/Los_Angeles) for consistent log timestamps
|
||||
TZ: process.env.TZ || 'America/Los_Angeles',
|
||||
NODE_ENV: 'development',
|
||||
// Vite-specific env vars (VITE_ prefix)
|
||||
VITE_SENTRY_DSN: process.env.VITE_SENTRY_DSN,
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "flyer-crawler",
|
||||
"version": "0.12.9",
|
||||
"version": "0.12.11",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "flyer-crawler",
|
||||
"version": "0.12.9",
|
||||
"version": "0.12.11",
|
||||
"dependencies": {
|
||||
"@bull-board/api": "^6.14.2",
|
||||
"@bull-board/express": "^6.14.2",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "flyer-crawler",
|
||||
"private": true,
|
||||
"version": "0.12.9",
|
||||
"version": "0.12.11",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "concurrently \"npm:start:dev\" \"vite\"",
|
||||
|
||||
@@ -23,6 +23,26 @@ set -e
|
||||
|
||||
echo "Starting Flyer Crawler Dev Container..."
|
||||
|
||||
# ============================================================================
|
||||
# Timezone Configuration
|
||||
# ============================================================================
|
||||
# Ensure TZ is set for consistent log timestamps across all services.
|
||||
# TZ should be set via compose.dev.yml environment (default: America/Los_Angeles)
|
||||
# ============================================================================
|
||||
if [ -n "$TZ" ]; then
|
||||
echo "Timezone configured: $TZ"
|
||||
# Link timezone data if available (for date command and other tools)
|
||||
if [ -f "/usr/share/zoneinfo/$TZ" ]; then
|
||||
ln -sf "/usr/share/zoneinfo/$TZ" /etc/localtime
|
||||
echo "$TZ" > /etc/timezone
|
||||
echo "System timezone set to: $(date +%Z) ($(date))"
|
||||
else
|
||||
echo "Warning: Timezone data not found for $TZ, using TZ environment variable only"
|
||||
fi
|
||||
else
|
||||
echo "Warning: TZ environment variable not set, using container default timezone"
|
||||
fi
|
||||
|
||||
# Configure Bugsink HTTPS (ADR-015)
|
||||
echo "Configuring Bugsink HTTPS..."
|
||||
mkdir -p /etc/bugsink/ssl
|
||||
|
||||
Reference in New Issue
Block a user