@kaikybrofc/omnizap-system 2.2.5 → 2.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prettierrc +16 -0
- package/README.md +11 -11
- package/app/modules/stickerPackModule/autoPackCollectorService.js +63 -8
- package/app/observability/metrics.js +15 -144
- package/index.js +13 -13
- package/kaikybrofc-omnizap-system-2.2.7.tgz +0 -0
- package/observability/sticker-catalog-slo.md +83 -0
- package/observability/sticker-scale-hardening-rollout.md +128 -0
- package/package.json +3 -35
- package/public/assets/images/brand-icon-192.png +0 -0
- package/public/assets/images/brand-logo-128.webp +0 -0
- package/public/assets/images/hero-banner-1280.avif +0 -0
- package/public/assets/images/hero-banner-1280.jpg +0 -0
- package/public/assets/images/hero-banner-1280.webp +0 -0
- package/public/assets/images/hero-banner-720.avif +0 -0
- package/public/assets/images/hero-banner-720.webp +0 -0
- package/public/index.html +91 -16
- package/public/js/apps/homeApp.js +469 -353
- package/public/robots.txt +9 -0
- package/public/sitemap.xml +28 -0
- package/server/controllers/metricsController.js +21 -0
- package/{app/modules/stickerPackModule/stickerPackCatalogHttp.js → server/controllers/stickerCatalogController.js} +66 -34
- package/server/http/httpServer.js +121 -0
- package/server/http/requestContext.js +20 -0
- package/server/index.js +1 -0
- package/server/routes/metricsRoute.js +7 -0
- package/server/routes/stickerCatalogRoute.js +20 -0
- package/observability/mysql-exporter.cnf +0 -5
- /package/{app/modules/stickerPackModule → server/routes/stickerCatalog}/catalogHandlers/catalogAdminHttp.js +0 -0
- /package/{app/modules/stickerPackModule → server/routes/stickerCatalog}/catalogHandlers/catalogAuthHttp.js +0 -0
- /package/{app/modules/stickerPackModule → server/routes/stickerCatalog}/catalogHandlers/catalogPublicHttp.js +0 -0
- /package/{app/modules/stickerPackModule → server/routes/stickerCatalog}/catalogHandlers/catalogUploadHttp.js +0 -0
- /package/{app/modules/stickerPackModule → server/routes/stickerCatalog}/catalogRouter.js +0 -0
package/.prettierrc
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"printWidth": 12000,
|
|
3
|
+
"tabWidth": 2,
|
|
4
|
+
"useTabs": false,
|
|
5
|
+
"semi": true,
|
|
6
|
+
"singleQuote": true,
|
|
7
|
+
"trailingComma": "all",
|
|
8
|
+
"bracketSpacing": true,
|
|
9
|
+
"arrowParens": "always",
|
|
10
|
+
"endOfLine": "lf",
|
|
11
|
+
"bracketSameLine": false,
|
|
12
|
+
"proseWrap": "preserve",
|
|
13
|
+
"htmlWhitespaceSensitivity": "css",
|
|
14
|
+
"quoteProps": "as-needed",
|
|
15
|
+
"embeddedLanguageFormatting": "auto"
|
|
16
|
+
}
|
package/README.md
CHANGED
|
@@ -53,26 +53,26 @@ Atualização em cache: **30 minutos** por padrão (`README_SUMMARY_CACHE_SECOND
|
|
|
53
53
|
<!-- README_SNAPSHOT:START -->
|
|
54
54
|
### Snapshot do Sistema
|
|
55
55
|
|
|
56
|
-
> Atualizado em `2026-02-
|
|
56
|
+
> Atualizado em `2026-02-28T09:29:12.922Z` | cache `1800s`
|
|
57
57
|
|
|
58
58
|
| Métrica | Valor |
|
|
59
59
|
| --- | ---: |
|
|
60
60
|
| Usuários (lid_map) | 5.504 |
|
|
61
61
|
| Grupos | 116 |
|
|
62
|
-
| Packs |
|
|
63
|
-
| Stickers | 6.
|
|
64
|
-
| Mensagens registradas |
|
|
62
|
+
| Packs | 295 |
|
|
63
|
+
| Stickers | 6.811 |
|
|
64
|
+
| Mensagens registradas | 441.124 |
|
|
65
65
|
|
|
66
66
|
#### Tipos de mensagem mais usados (amostra: 25.000)
|
|
67
67
|
| Tipo | Total |
|
|
68
68
|
| --- | ---: |
|
|
69
|
-
| `texto` | 16.
|
|
70
|
-
| `figurinha` | 4.
|
|
71
|
-
| `reacao` | 1.
|
|
72
|
-
| `imagem` | 1.
|
|
73
|
-
| `outros` |
|
|
74
|
-
| `video` |
|
|
75
|
-
| `audio` |
|
|
69
|
+
| `texto` | 16.180 |
|
|
70
|
+
| `figurinha` | 4.806 |
|
|
71
|
+
| `reacao` | 1.502 |
|
|
72
|
+
| `imagem` | 1.298 |
|
|
73
|
+
| `outros` | 769 |
|
|
74
|
+
| `video` | 227 |
|
|
75
|
+
| `audio` | 213 |
|
|
76
76
|
| `documento` | 5 |
|
|
77
77
|
|
|
78
78
|
<details><summary>Comandos disponíveis (62)</summary>
|
|
@@ -8,6 +8,8 @@ const DEFAULT_AUTO_PACK_NAME = process.env.STICKER_PACK_AUTO_PACK_NAME || 'pack'
|
|
|
8
8
|
const AUTO_PACK_TARGET_VISIBILITY = 'unlisted';
|
|
9
9
|
const AUTO_COLLECT_ENABLED = process.env.STICKER_PACK_AUTO_COLLECT_ENABLED !== 'false';
|
|
10
10
|
const AUTO_PACK_NAME_MAX_LENGTH = 120;
|
|
11
|
+
const AUTO_PACK_DESCRIPTION_MARKER = '[auto-pack:collector]';
|
|
12
|
+
const AUTO_PACK_DESCRIPTION_TEXT = 'Coleção automática de figurinhas criadas pelo usuário.';
|
|
11
13
|
const normalizeVisibility = (value) => String(value || '').trim().toLowerCase();
|
|
12
14
|
|
|
13
15
|
/**
|
|
@@ -43,6 +45,42 @@ const normalizeAutoPackName = (value, { fallback = 'pack', maxLength = AUTO_PACK
|
|
|
43
45
|
return normalized || fallback;
|
|
44
46
|
};
|
|
45
47
|
|
|
48
|
+
const buildAutoPackDescription = () => `${AUTO_PACK_DESCRIPTION_TEXT} ${AUTO_PACK_DESCRIPTION_MARKER}`.trim();
|
|
49
|
+
|
|
50
|
+
const isThemeCurationAutoPack = (pack) => {
|
|
51
|
+
if (!pack || typeof pack !== 'object') return false;
|
|
52
|
+
const description = String(pack.description || '').toLowerCase();
|
|
53
|
+
if (description.includes('[auto-theme:') || description.includes('[auto-tag:')) return true;
|
|
54
|
+
if (String(pack.name || '').trim().toLowerCase().startsWith('[auto]')) return true;
|
|
55
|
+
return Boolean(String(pack.pack_theme_key || '').trim());
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const isAutoCollectorPack = (pack) => {
|
|
59
|
+
if (!pack || typeof pack !== 'object') return false;
|
|
60
|
+
if (isThemeCurationAutoPack(pack)) return false;
|
|
61
|
+
|
|
62
|
+
const description = String(pack.description || '').toLowerCase();
|
|
63
|
+
if (description.includes(AUTO_PACK_DESCRIPTION_MARKER)) return true;
|
|
64
|
+
if (description.includes('coleção automática de figurinhas criadas pelo usuário.')) return true;
|
|
65
|
+
|
|
66
|
+
const normalizedName = normalizeAutoPackName(pack.name, { fallback: '', maxLength: AUTO_PACK_NAME_MAX_LENGTH });
|
|
67
|
+
if (!normalizedName) return false;
|
|
68
|
+
|
|
69
|
+
const base = normalizeAutoPackName(DEFAULT_AUTO_PACK_NAME, { fallback: 'pack', maxLength: AUTO_PACK_NAME_MAX_LENGTH });
|
|
70
|
+
const matcher = new RegExp(`^${escapeRegex(base.toLowerCase())}\\d+$`, 'i');
|
|
71
|
+
const looksLikeLegacyCollector = matcher.test(normalizedName);
|
|
72
|
+
if (looksLikeLegacyCollector) return true;
|
|
73
|
+
|
|
74
|
+
return pack.is_auto_pack === true || Number(pack.is_auto_pack || 0) === 1;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const isUserManagedPackCandidate = (pack) => {
|
|
78
|
+
if (!pack || typeof pack !== 'object') return false;
|
|
79
|
+
if (isThemeCurationAutoPack(pack)) return false;
|
|
80
|
+
if (isAutoCollectorPack(pack)) return false;
|
|
81
|
+
return Number(pack.is_auto_pack || 0) !== 1;
|
|
82
|
+
};
|
|
83
|
+
|
|
46
84
|
/**
|
|
47
85
|
* Monta candidato incremental para nome de pack automático.
|
|
48
86
|
*
|
|
@@ -177,21 +215,37 @@ export function createAutoPackCollector(options = {}) {
|
|
|
177
215
|
const ensureTargetPack = async ({ ownerJid, senderName }) => {
|
|
178
216
|
const packs = await deps.stickerPackService.listPacks({ ownerJid, limit: 30 });
|
|
179
217
|
if (packs.length > 0) {
|
|
218
|
+
// Prioriza pack gerenciado pelo usuário (pack atual/manual).
|
|
219
|
+
const userManagedPacks = packs.filter((entry) => isUserManagedPackCandidate(entry));
|
|
220
|
+
if (userManagedPacks.length > 0) {
|
|
221
|
+
return {
|
|
222
|
+
pack: userManagedPacks[0],
|
|
223
|
+
packs,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const managedAutoPacks = packs.filter((entry) => isAutoCollectorPack(entry));
|
|
180
228
|
const preferredPack =
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
229
|
+
managedAutoPacks.find((entry) => normalizeVisibility(entry?.visibility) === AUTO_PACK_TARGET_VISIBILITY)
|
|
230
|
+
|| managedAutoPacks[0]
|
|
231
|
+
|| null;
|
|
232
|
+
|
|
233
|
+
if (preferredPack) {
|
|
234
|
+
const ensuredPack = await ensureAutoPackVisibility({ ownerJid, pack: preferredPack });
|
|
235
|
+
return {
|
|
236
|
+
pack: ensuredPack,
|
|
237
|
+
packs,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
187
240
|
}
|
|
188
241
|
|
|
189
242
|
const created = await deps.stickerPackService.createPack({
|
|
190
243
|
ownerJid,
|
|
191
244
|
name: makeAutoPackName([]),
|
|
192
245
|
publisher: sanitizeText(senderName, 120, { allowEmpty: true }) || 'OmniZap',
|
|
193
|
-
description:
|
|
246
|
+
description: buildAutoPackDescription(),
|
|
194
247
|
visibility: AUTO_PACK_TARGET_VISIBILITY,
|
|
248
|
+
isAutoPack: true,
|
|
195
249
|
});
|
|
196
250
|
|
|
197
251
|
return {
|
|
@@ -250,8 +304,9 @@ export function createAutoPackCollector(options = {}) {
|
|
|
250
304
|
ownerJid,
|
|
251
305
|
name: makeAutoPackName(packs),
|
|
252
306
|
publisher: sanitizeText(senderName, 120, { allowEmpty: true }) || targetPack.publisher || 'OmniZap',
|
|
253
|
-
description:
|
|
307
|
+
description: buildAutoPackDescription(),
|
|
254
308
|
visibility: AUTO_PACK_TARGET_VISIBILITY,
|
|
309
|
+
isAutoPack: true,
|
|
255
310
|
});
|
|
256
311
|
|
|
257
312
|
const updated = await deps.stickerPackService.addStickerToPack({
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import 'dotenv/config';
|
|
2
2
|
|
|
3
|
-
import http from 'node:http';
|
|
4
|
-
import { randomUUID } from 'node:crypto';
|
|
5
|
-
import { URL } from 'node:url';
|
|
6
3
|
import client from 'prom-client';
|
|
7
|
-
import logger from '../utils/logger/loggerModule.js';
|
|
8
4
|
|
|
9
5
|
const parseEnvBool = (value, fallback) => {
|
|
10
6
|
if (value === undefined || value === null || value === '') return fallback;
|
|
@@ -39,23 +35,12 @@ const QUERY_THRESHOLDS_MS = parseThresholds(process.env.DB_QUERY_ALERT_THRESHOLD
|
|
|
39
35
|
|
|
40
36
|
const registry = new client.Registry();
|
|
41
37
|
let metrics = null;
|
|
42
|
-
let server = null;
|
|
43
|
-
let serverStarted = false;
|
|
44
|
-
let stickerCatalogModulePromise = null;
|
|
45
38
|
|
|
46
39
|
const normalizeLabel = (value, fallback = 'unknown') => {
|
|
47
40
|
if (value === undefined || value === null || value === '') return fallback;
|
|
48
41
|
return String(value);
|
|
49
42
|
};
|
|
50
43
|
|
|
51
|
-
const normalizeRequestId = (value) => {
|
|
52
|
-
const token = String(value || '')
|
|
53
|
-
.trim()
|
|
54
|
-
.replace(/[^a-zA-Z0-9._:-]/g, '')
|
|
55
|
-
.slice(0, 120);
|
|
56
|
-
return token || randomUUID();
|
|
57
|
-
};
|
|
58
|
-
|
|
59
44
|
const normalizeHttpMethod = (method) => {
|
|
60
45
|
const normalized = String(method || '').trim().toUpperCase();
|
|
61
46
|
if (!normalized) return 'UNKNOWN';
|
|
@@ -70,7 +55,7 @@ const toStatusClass = (statusCode) => {
|
|
|
70
55
|
return `${head}xx`;
|
|
71
56
|
};
|
|
72
57
|
|
|
73
|
-
const resolveRouteGroup = ({ pathname, metricsPath, catalogConfig = null } = {}) => {
|
|
58
|
+
export const resolveRouteGroup = ({ pathname, metricsPath, catalogConfig = null } = {}) => {
|
|
74
59
|
if (pathname?.startsWith(metricsPath)) return 'metrics';
|
|
75
60
|
if (pathname === '/sitemap.xml') return 'sitemap';
|
|
76
61
|
if (pathname === '/api/marketplace/stats') return 'marketplace_stats';
|
|
@@ -390,137 +375,23 @@ const ensureMetrics = () => {
|
|
|
390
375
|
return metrics;
|
|
391
376
|
};
|
|
392
377
|
|
|
393
|
-
const loadStickerCatalogModule = async () => {
|
|
394
|
-
if (!stickerCatalogModulePromise) {
|
|
395
|
-
stickerCatalogModulePromise = import('../modules/stickerPackModule/stickerPackCatalogHttp.js');
|
|
396
|
-
}
|
|
397
|
-
return stickerCatalogModulePromise;
|
|
398
|
-
};
|
|
399
|
-
|
|
400
378
|
export const isMetricsEnabled = () => METRICS_ENABLED;
|
|
401
379
|
|
|
402
|
-
export const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
res.setHeader('X-Request-Id', requestId);
|
|
409
|
-
|
|
410
|
-
const host = req.headers.host || `${METRICS_HOST}:${METRICS_PORT}`;
|
|
411
|
-
let parsedUrl;
|
|
412
|
-
try {
|
|
413
|
-
parsedUrl = new URL(req.url || '/', `http://${host}`);
|
|
414
|
-
} catch {
|
|
415
|
-
parsedUrl = new URL(req.url || '/', 'http://localhost');
|
|
416
|
-
}
|
|
417
|
-
const pathname = parsedUrl.pathname;
|
|
418
|
-
let routeGroup = resolveRouteGroup({
|
|
419
|
-
pathname,
|
|
420
|
-
metricsPath: METRICS_PATH,
|
|
421
|
-
catalogConfig: null,
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
res.once('finish', () => {
|
|
425
|
-
recordHttpRequest({
|
|
426
|
-
durationMs: Date.now() - requestStartedAt,
|
|
427
|
-
method: req.method,
|
|
428
|
-
statusCode: res.statusCode,
|
|
429
|
-
routeGroup,
|
|
430
|
-
});
|
|
431
|
-
});
|
|
432
|
-
|
|
433
|
-
try {
|
|
434
|
-
const stickerCatalogModule = await loadStickerCatalogModule();
|
|
435
|
-
const catalogConfig =
|
|
436
|
-
typeof stickerCatalogModule.getStickerCatalogConfig === 'function'
|
|
437
|
-
? stickerCatalogModule.getStickerCatalogConfig()
|
|
438
|
-
: null;
|
|
439
|
-
routeGroup = resolveRouteGroup({
|
|
440
|
-
pathname,
|
|
441
|
-
metricsPath: METRICS_PATH,
|
|
442
|
-
catalogConfig,
|
|
443
|
-
});
|
|
444
|
-
const handledByCatalog = await stickerCatalogModule.maybeHandleStickerCatalogRequest(req, res, {
|
|
445
|
-
pathname,
|
|
446
|
-
url: parsedUrl,
|
|
447
|
-
});
|
|
380
|
+
export const getMetricsServerConfig = () => ({
|
|
381
|
+
enabled: METRICS_ENABLED,
|
|
382
|
+
host: METRICS_HOST,
|
|
383
|
+
port: METRICS_PORT,
|
|
384
|
+
path: METRICS_PATH,
|
|
385
|
+
});
|
|
448
386
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
if (!pathname.startsWith(METRICS_PATH)) {
|
|
459
|
-
res.statusCode = 404;
|
|
460
|
-
res.end('Not Found');
|
|
461
|
-
return;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
try {
|
|
465
|
-
const body = await registry.metrics();
|
|
466
|
-
res.statusCode = 200;
|
|
467
|
-
res.setHeader('Content-Type', registry.contentType);
|
|
468
|
-
res.end(body);
|
|
469
|
-
} catch (error) {
|
|
470
|
-
res.statusCode = 500;
|
|
471
|
-
res.end('Metrics error');
|
|
472
|
-
logger.error('Erro ao gerar /metrics', { error: error.message });
|
|
473
|
-
}
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
server.listen(METRICS_PORT, METRICS_HOST, () => {
|
|
477
|
-
serverStarted = true;
|
|
478
|
-
logger.info('Servidor /metrics iniciado', {
|
|
479
|
-
host: METRICS_HOST,
|
|
480
|
-
port: METRICS_PORT,
|
|
481
|
-
path: METRICS_PATH,
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
loadStickerCatalogModule()
|
|
485
|
-
.then((module) => {
|
|
486
|
-
const config = typeof module.getStickerCatalogConfig === 'function' ? module.getStickerCatalogConfig() : null;
|
|
487
|
-
if (!config?.enabled) return;
|
|
488
|
-
logger.info('Catalogo web de sticker packs habilitado', {
|
|
489
|
-
web_path: config.webPath,
|
|
490
|
-
api_base_path: config.apiBasePath,
|
|
491
|
-
orphan_api_path: config.orphanApiPath,
|
|
492
|
-
data_public_path: config.dataPublicPath,
|
|
493
|
-
data_public_dir: config.dataPublicDir,
|
|
494
|
-
host: METRICS_HOST,
|
|
495
|
-
port: METRICS_PORT,
|
|
496
|
-
});
|
|
497
|
-
})
|
|
498
|
-
.catch((error) => {
|
|
499
|
-
logger.warn('Nao foi possivel carregar configuracao do catalogo de sticker packs.', {
|
|
500
|
-
error: error.message,
|
|
501
|
-
});
|
|
502
|
-
});
|
|
503
|
-
});
|
|
504
|
-
|
|
505
|
-
server.on('error', (error) => {
|
|
506
|
-
logger.error('Falha ao iniciar servidor /metrics', { error: error.message });
|
|
507
|
-
});
|
|
508
|
-
};
|
|
509
|
-
|
|
510
|
-
export const stopMetricsServer = async () => {
|
|
511
|
-
if (!serverStarted || !server) return;
|
|
512
|
-
const current = server;
|
|
513
|
-
server = null;
|
|
514
|
-
serverStarted = false;
|
|
515
|
-
await new Promise((resolve, reject) => {
|
|
516
|
-
current.close((error) => {
|
|
517
|
-
if (error) {
|
|
518
|
-
reject(error);
|
|
519
|
-
return;
|
|
520
|
-
}
|
|
521
|
-
resolve();
|
|
522
|
-
});
|
|
523
|
-
});
|
|
387
|
+
export const getMetricsPayload = async () => {
|
|
388
|
+
if (!METRICS_ENABLED) return null;
|
|
389
|
+
ensureMetrics();
|
|
390
|
+
const body = await registry.metrics();
|
|
391
|
+
return {
|
|
392
|
+
body,
|
|
393
|
+
contentType: registry.contentType,
|
|
394
|
+
};
|
|
524
395
|
};
|
|
525
396
|
|
|
526
397
|
export const recordError = (scope = 'app') => {
|
package/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Responsabilidades principais:
|
|
5
5
|
* - Inicializar o banco (garantindo DB e tabelas).
|
|
6
|
-
* - Subir servidor de métricas.
|
|
6
|
+
* - Subir servidor HTTP (catálogo web + endpoint de métricas).
|
|
7
7
|
* - Rodar backfill do lid_map (opcional, em background).
|
|
8
8
|
* - Conectar ao WhatsApp (Baileys).
|
|
9
9
|
* - Iniciar serviços auxiliares (ex: broadcast de notícias).
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
stopNewsBroadcastService,
|
|
26
26
|
} from './app/services/newsBroadcastService.js';
|
|
27
27
|
import initializeDatabase from './database/init.js';
|
|
28
|
-
import {
|
|
28
|
+
import { startHttpServer, stopHttpServer } from './server/index.js';
|
|
29
29
|
import {
|
|
30
30
|
startStickerClassificationBackground,
|
|
31
31
|
stopStickerClassificationBackground,
|
|
@@ -198,7 +198,7 @@ async function closeDatabasePool() {
|
|
|
198
198
|
*
|
|
199
199
|
* Fluxo de startup:
|
|
200
200
|
* 1) Inicializa DB (cria/verifica DB/tabelas)
|
|
201
|
-
* 2) Sobe servidor
|
|
201
|
+
* 2) Sobe servidor HTTP (catálogo + métricas)
|
|
202
202
|
* 3) Inicia backfill (opcional) em background
|
|
203
203
|
* 4) Conecta no WhatsApp
|
|
204
204
|
* 5) Inicia serviços auxiliares (news broadcast)
|
|
@@ -216,8 +216,8 @@ async function startApp() {
|
|
|
216
216
|
await withTimeout(initializeDatabase(), DB_INIT_TIMEOUT_MS, 'Inicializacao do banco');
|
|
217
217
|
logger.info('Banco de dados pronto.');
|
|
218
218
|
|
|
219
|
-
logger.info('Inicializando servidor
|
|
220
|
-
|
|
219
|
+
logger.info('Inicializando servidor HTTP...');
|
|
220
|
+
startHttpServer();
|
|
221
221
|
if (isStickerWorkerPipelineEnabled()) {
|
|
222
222
|
startStickerWorkerPipeline();
|
|
223
223
|
} else {
|
|
@@ -274,7 +274,7 @@ startApp();
|
|
|
274
274
|
* - serviço de notícias (se stop existir)
|
|
275
275
|
* - aguarda backfill (com timeout curto)
|
|
276
276
|
* - encerra socket do WhatsApp
|
|
277
|
-
* - encerra servidor
|
|
277
|
+
* - encerra servidor HTTP
|
|
278
278
|
* - encerra pool do MySQL
|
|
279
279
|
*
|
|
280
280
|
* Regras:
|
|
@@ -339,14 +339,14 @@ async function shutdown(signal, error) {
|
|
|
339
339
|
}
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
-
// 4) Encerrar servidor
|
|
343
|
-
if (typeof
|
|
342
|
+
// 4) Encerrar servidor HTTP
|
|
343
|
+
if (typeof stopHttpServer === 'function') {
|
|
344
344
|
try {
|
|
345
|
-
logger.info('Encerrando servidor
|
|
346
|
-
await withTimeout(
|
|
347
|
-
logger.info('Servidor
|
|
348
|
-
} catch (
|
|
349
|
-
logger.warn('Falha ao encerrar servidor
|
|
345
|
+
logger.info('Encerrando servidor HTTP...');
|
|
346
|
+
await withTimeout(stopHttpServer(), 8000, 'Encerramento HTTP');
|
|
347
|
+
logger.info('Servidor HTTP encerrado.');
|
|
348
|
+
} catch (httpError) {
|
|
349
|
+
logger.warn('Falha ao encerrar servidor HTTP.', { error: httpError.message });
|
|
350
350
|
}
|
|
351
351
|
}
|
|
352
352
|
|
|
Binary file
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Sticker Catalog 10x Baseline e SLOs
|
|
2
|
+
|
|
3
|
+
Este documento define a baseline operacional da camada HTTP + pipeline de classificação para o módulo de stickers.
|
|
4
|
+
|
|
5
|
+
## 1. Metas SLO (fase inicial)
|
|
6
|
+
|
|
7
|
+
### HTTP catálogo (`/api/sticker-packs*`, `/stickers*`, `/api/marketplace/stats`)
|
|
8
|
+
|
|
9
|
+
- **Latência p95**: `<= 750ms`
|
|
10
|
+
- **Latência p99**: `<= 1500ms`
|
|
11
|
+
- **Taxa de erro (5xx + timeout)**: `<= 2%` por janela de 5 minutos
|
|
12
|
+
- **Throughput alvo**: escalar linearmente com workers/processos sem aumento abrupto do p95
|
|
13
|
+
|
|
14
|
+
### Classificação de stickers
|
|
15
|
+
|
|
16
|
+
- **Duração média do ciclo**: `<= 10s`
|
|
17
|
+
- **Throughput mínimo (assets classificados/min)**: `>= 300` (ajustar por hardware)
|
|
18
|
+
- **Backlog de fila (`sticker_reprocess_pending`)**: tendência de queda após picos; alerta se cresce por mais de 15 min
|
|
19
|
+
|
|
20
|
+
## 2. Métricas instrumentadas
|
|
21
|
+
|
|
22
|
+
### HTTP
|
|
23
|
+
|
|
24
|
+
- `omnizap_http_requests_total{route_group,method,status_class}`
|
|
25
|
+
- `omnizap_http_request_duration_ms{route_group,method,status_class}`
|
|
26
|
+
- `omnizap_http_slo_violation_total{route_group,method}`
|
|
27
|
+
|
|
28
|
+
`route_group` segmenta tráfego em:
|
|
29
|
+
|
|
30
|
+
- `catalog_api_public`
|
|
31
|
+
- `catalog_api_auth`
|
|
32
|
+
- `catalog_api_admin`
|
|
33
|
+
- `catalog_api_upload`
|
|
34
|
+
- `catalog_web`
|
|
35
|
+
- `catalog_data_asset`
|
|
36
|
+
- `catalog_user_profile`
|
|
37
|
+
- `marketplace_stats`
|
|
38
|
+
- `metrics`
|
|
39
|
+
- `other`
|
|
40
|
+
|
|
41
|
+
### Classificação
|
|
42
|
+
|
|
43
|
+
- `omnizap_sticker_classification_cycle_duration_ms{status}`
|
|
44
|
+
- `omnizap_sticker_classification_cycle_total{status}`
|
|
45
|
+
- `omnizap_sticker_classification_assets_total{outcome}`
|
|
46
|
+
- `omnizap_queue_depth{queue}`
|
|
47
|
+
|
|
48
|
+
## 3. Tracing mínimo
|
|
49
|
+
|
|
50
|
+
- Cada request HTTP agora recebe/propaga `X-Request-Id`.
|
|
51
|
+
- Se o cliente enviar `X-Request-Id`, o valor é reaproveitado.
|
|
52
|
+
- Sem header, o servidor gera UUID.
|
|
53
|
+
|
|
54
|
+
## 4. Baseline de carga (script local)
|
|
55
|
+
|
|
56
|
+
Script: `scripts/sticker-catalog-loadtest.mjs`
|
|
57
|
+
|
|
58
|
+
Exemplo:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
node scripts/sticker-catalog-loadtest.mjs \
|
|
62
|
+
--base-url http://127.0.0.1:9102 \
|
|
63
|
+
--duration-seconds 60 \
|
|
64
|
+
--concurrency 40 \
|
|
65
|
+
--paths "/api/sticker-packs?limit=24&sort=popular,/api/sticker-packs/stats,/api/sticker-packs/creators?limit=25" \
|
|
66
|
+
--out /tmp/sticker-loadtest-report.json
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Interpretação rápida:
|
|
70
|
+
|
|
71
|
+
- `latency_ms.p95 <= 750` = SLO de latência cumprido
|
|
72
|
+
- `error_rate <= 0.02` = estabilidade aceitável
|
|
73
|
+
- `throughput_rps` = referência para comparar antes/depois de otimizações
|
|
74
|
+
|
|
75
|
+
## 5. Gate de rollout sugerido
|
|
76
|
+
|
|
77
|
+
1. Capturar baseline com carga atual.
|
|
78
|
+
2. Aplicar mudança de arquitetura/índice/cache.
|
|
79
|
+
3. Reexecutar carga com mesmos parâmetros.
|
|
80
|
+
4. Aprovar rollout apenas se:
|
|
81
|
+
- p95 não piorar mais de 10%
|
|
82
|
+
- erro não subir acima de 2%
|
|
83
|
+
- backlog voltar ao patamar normal em até 15 min
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Sticker 10x Hardening And Rollout
|
|
2
|
+
|
|
3
|
+
## Scope
|
|
4
|
+
|
|
5
|
+
This runbook covers phases 4-8 of the sticker-pack scale plan:
|
|
6
|
+
|
|
7
|
+
1. ranking snapshot read path
|
|
8
|
+
2. internal outbox/event consumer
|
|
9
|
+
3. dedicated workers (classification/curation/rebuild)
|
|
10
|
+
4. object storage delivery with secure URLs
|
|
11
|
+
5. canary rollout, rollback, and final tuning
|
|
12
|
+
|
|
13
|
+
## Feature Flags
|
|
14
|
+
|
|
15
|
+
Flags are stored in `feature_flag`:
|
|
16
|
+
|
|
17
|
+
- `enable_ranking_snapshot_read`
|
|
18
|
+
- `enable_domain_event_outbox`
|
|
19
|
+
- `enable_worker_dedicated_processes`
|
|
20
|
+
- `enable_object_storage_delivery`
|
|
21
|
+
|
|
22
|
+
### Query Current Status
|
|
23
|
+
|
|
24
|
+
```sql
|
|
25
|
+
SELECT flag_name, is_enabled, rollout_percent, updated_at
|
|
26
|
+
FROM feature_flag
|
|
27
|
+
WHERE flag_name IN (
|
|
28
|
+
'enable_ranking_snapshot_read',
|
|
29
|
+
'enable_domain_event_outbox',
|
|
30
|
+
'enable_worker_dedicated_processes',
|
|
31
|
+
'enable_object_storage_delivery'
|
|
32
|
+
)
|
|
33
|
+
ORDER BY flag_name;
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Update Rollout Percent
|
|
37
|
+
|
|
38
|
+
```sql
|
|
39
|
+
UPDATE feature_flag
|
|
40
|
+
SET is_enabled = 1, rollout_percent = 25, updated_by = 'ops'
|
|
41
|
+
WHERE flag_name = 'enable_worker_dedicated_processes';
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Emergency Disable
|
|
45
|
+
|
|
46
|
+
```sql
|
|
47
|
+
UPDATE feature_flag
|
|
48
|
+
SET is_enabled = 0, rollout_percent = 0, updated_by = 'ops'
|
|
49
|
+
WHERE flag_name IN (
|
|
50
|
+
'enable_worker_dedicated_processes',
|
|
51
|
+
'enable_object_storage_delivery',
|
|
52
|
+
'enable_domain_event_outbox'
|
|
53
|
+
);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Canary Sequence
|
|
57
|
+
|
|
58
|
+
1. `enable_ranking_snapshot_read`: 10% -> 50% -> 100%
|
|
59
|
+
2. `enable_domain_event_outbox`: 10% -> 50% -> 100%
|
|
60
|
+
3. start dedicated worker processes and set `enable_worker_dedicated_processes`: 10% -> 50% -> 100%
|
|
61
|
+
4. `enable_object_storage_delivery`: 5% -> 25% -> 100%
|
|
62
|
+
|
|
63
|
+
Promotion gate for each step:
|
|
64
|
+
|
|
65
|
+
- HTTP p95 within target
|
|
66
|
+
- queue backlog stable (`pending`, `failed`)
|
|
67
|
+
- outbox DLQ not growing unexpectedly
|
|
68
|
+
- no sustained error-rate increase
|
|
69
|
+
|
|
70
|
+
## Dedicated Workers
|
|
71
|
+
|
|
72
|
+
Run workers as isolated processes:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npm run worker:sticker:classification
|
|
76
|
+
npm run worker:sticker:curation
|
|
77
|
+
npm run worker:sticker:rebuild
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
PM2 production profile includes these workers in `ecosystem.prod.config.cjs`.
|
|
81
|
+
|
|
82
|
+
## 10x Validation
|
|
83
|
+
|
|
84
|
+
### HTTP Stress
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
npm run loadtest:stickers -- --base-url http://127.0.0.1:9102 --duration-seconds 120 --concurrency 200 --slo-ms 750
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Queue/Worker Validation
|
|
91
|
+
|
|
92
|
+
Monitor:
|
|
93
|
+
|
|
94
|
+
- `sticker_worker_tasks_pending`
|
|
95
|
+
- `sticker_worker_tasks_processing`
|
|
96
|
+
- `sticker_worker_tasks_failed`
|
|
97
|
+
- `domain_event_outbox_pending`
|
|
98
|
+
- `domain_event_outbox_failed`
|
|
99
|
+
|
|
100
|
+
Acceptance:
|
|
101
|
+
|
|
102
|
+
- failed queues remain near zero (transient spikes allowed)
|
|
103
|
+
- pending queues recover after load burst
|
|
104
|
+
- no monotonic growth in DLQ tables
|
|
105
|
+
|
|
106
|
+
## Rollback Plan
|
|
107
|
+
|
|
108
|
+
1. Disable `enable_object_storage_delivery`.
|
|
109
|
+
2. Disable `enable_worker_dedicated_processes` (inline poller resumes).
|
|
110
|
+
3. Disable `enable_domain_event_outbox` if event flow is unstable.
|
|
111
|
+
4. Keep `enable_ranking_snapshot_read` enabled only if snapshot freshness is healthy.
|
|
112
|
+
|
|
113
|
+
Data safety notes:
|
|
114
|
+
|
|
115
|
+
- tasks/events are persisted in SQL queues
|
|
116
|
+
- failed terminal tasks/events are preserved in DLQ tables
|
|
117
|
+
- local disk read path remains fallback for sticker asset serving
|
|
118
|
+
|
|
119
|
+
## Post-Rollout Tuning
|
|
120
|
+
|
|
121
|
+
Tune these env vars after baseline:
|
|
122
|
+
|
|
123
|
+
- `STICKER_WORKER_CLASSIFICATION_CADENCE_MS`
|
|
124
|
+
- `STICKER_WORKER_CURATION_CADENCE_MS`
|
|
125
|
+
- `STICKER_WORKER_REBUILD_CADENCE_MS`
|
|
126
|
+
- `STICKER_DEDICATED_WORKER_POLL_INTERVAL_MS`
|
|
127
|
+
- `STICKER_SCORE_SNAPSHOT_REFRESH_INTERVAL_MS`
|
|
128
|
+
- `STICKER_OBJECT_STORAGE_SIGNED_URL_TTL_SECONDS`
|