REFACTOR/BUG: Correção de bugs e alteção de run

- Alterado Código para que use o node cron e o gerenciamento seja feito pelo pm2
- Bug que não envia uma atualização para o SN quando um chamo é fechado fora do escopo corrigido
This commit is contained in:
Rafael Alves Lopes 2025-11-19 16:01:37 -03:00
parent fb3f4eee75
commit 25458d5e44
19 changed files with 1269 additions and 137 deletions

View File

@ -4,6 +4,9 @@
NODE_ENV=development
PORT=3000
# Frequência de execução do cron job (formato cron). Padrão: '* * * * *' (a cada 1 minutos)
CRON_SCHEDULE='* * * * *'
# ServiceNow API (Dev)
SERVICENOW_INSTANCE=https://caoadev.service-now.com
SERVICENOW_USERNAME=SOTHIS.CAOA
@ -26,22 +29,25 @@ SERVICENOW_DEFAULT_USER_SYSID=b1312fda977a1250501634a6f053af98
# GLPI (banco real)
GLPI_DB_TYPE=mysql
GLPI_DB_HOST=177.73.177.32
GLPI_DB_HOST=177.73.177.44
GLPI_DB_PORT=3306
GLPI_DB_USER=snglpi
GLPI_DB_PASSWORD=j2633669
GLPI_DB_USER=desenvolvimento
GLPI_DB_PASSWORD=Ut@2S@$M9Xs@@W
GLPI_DB_NAME=glpi_data
GLPI_DB_CHARSET=utf8mb4
# ID do usuário padrão do GLPI para criação de tickets e comentários
GLPI_DEFAULT_USER_ID=11111
# Banco intermediário (PostgreSQL)
SNGLPI_DB_HOST=localhost
SNGLPI_DB_HOST=10.0.120.75
SNGLPI_DB_PORT=5432
SNGLPI_DB_NAME=snglpi
SNGLPI_DB_NAME=snglpi-development
SNGLPI_DB_USER=desenvolvimento
SNGLPI_DB_PASSWORD=Ut@2S@$M9Xs@@W
# CSV de mapeamento
LOCATION_MAPPING_CSV_PATH=/mnt/csv_share/OCATION_MAPPING.csv
LOCATION_MAPPING_CSV_PATH=\\10.0.121.40\Tecnica\Controle CAOA\ENTIDADE SERVICE NOW\LOCATION_MAPPING.csv
# Logging
LOG_LEVEL=debug

57
.env.example Normal file
View File

@ -0,0 +1,57 @@
# ===================================================================
# EXEMPLO DE ARQUIVO DE CONFIGURAÇÃO DE AMBIENTE
#
# Copie este arquivo para .env.development e .env.production
# e preencha com os valores corretos para cada ambiente.
# Não adicione senhas ou chaves secretas diretamente neste arquivo.
# ===================================================================
# --- Configuração Geral da Aplicação ---
NODE_ENV=development
PORT=3000
# Frequência de execução do cron job (formato cron). Padrão: '* * * * *' (a cada 1 minutos)
CRON_SCHEDULE='* * * * *'
# --- Configuração da API do ServiceNow ---
# URL base da sua instância do ServiceNow
SERVICENOW_INSTANCE=https://your-instance.service-now.com
SERVICENOW_USERNAME=
SERVICENOW_PASSWORD=
# Sys_id do grupo de atribuição no ServiceNow que será monitorado para novos tickets
SERVICENOW_ASSIGNMENT_GROUP=
# Sys_id do usuário padrão no ServiceNow que será usado pela integração para criar/atualizar tickets
SERVICENOW_DEFAULT_USER_SYSID=
# --- Configuração do Banco de Dados do GLPI (MySQL) ---
GLPI_DB_TYPE=mysql
GLPI_DB_HOST=localhost
GLPI_DB_PORT=3306
GLPI_DB_USER=
GLPI_DB_PASSWORD=
GLPI_DB_NAME=glpi
GLPI_DB_CHARSET=utf8mb4
# ID do usuário padrão do GLPI para criação de tickets e comentários
GLPI_DEFAULT_USER_ID=1118
# --- Configuração do Banco de Dados Intermediário (PostgreSQL) ---
SNGLPI_DB_HOST=localhost
SNGLPI_DB_PORT=5432
SNGLPI_DB_NAME=snglpi_sync
SNGLPI_DB_USER=
SNGLPI_DB_PASSWORD=
# --- Configuração de Logs ---
LOG_LEVEL=debug
# --- Mapeamento de Localidades (Opcional) ---
# Caminho absoluto para o arquivo .csv que mapeia localidades do SNOW para entidades do GLPI
LOCATION_MAPPING_CSV_PATH=/path/to/your/location_mapping.csv
# Logging
LOG_LEVEL=debug
LOG_RETENTION_DAYS=10
LOG_DIR=logs

View File

@ -8,6 +8,84 @@ A aplicação funciona como um job agendado (por exemplo, via cron) que executa
---
## Como Executar a Aplicação
Esta seção contém as instruções essenciais para configurar e rodar a aplicação em diferentes ambientes.
### 1. Pré-requisitos
- **Node.js**: Certifique-se de ter o Node.js (versão 18 ou superior) instalado.
- **Dependências**: Na raiz do projeto, execute o comando abaixo para instalar todas as dependências necessárias:
```bash
npm install
```
- **Arquivos de Ambiente**: Crie os arquivos `.env.development` e `.env.production` na raiz do projeto, baseando-se no exemplo `.env.example` (se houver) e preenchendo com as credenciais e URLs corretas para cada ambiente.
### 2. Executando com Scripts NPM
### 2. Configuração do Ambiente Python
A aplicação utiliza um script Python para sincronizar o mapeamento de localidades.
#### a) Dependências Python
Navegue até a pasta do script e instale as dependências listadas no `requirements.txt`:
```bash
cd src/scripts/python
pip install -r requirements.txt
```
#### b) Montagem do Compartilhamento de Arquivos (CIFS/Samba)
O script Python lê um arquivo `.csv` de um compartilhamento de rede. No servidor de produção, é necessário montar este compartilhamento para que o caminho definido em `LOCATION_MAPPING_CSV_PATH` seja acessível.
Adicione a seguinte linha ao seu arquivo `/etc/fstab` para montar o compartilhamento automaticamente durante o boot do sistema. **Ajuste o IP, credenciais e caminhos conforme necessário.**
```fstab
//10.0.121.40/Tecnica/Controle\040CAOA/ENTIDADE\040SERVICE\040NOW /mnt/csv_share cifs username=<seu_usuario>,password=<sua_senha>,uid=1000,gid=1000,iocharset=utf8 0 0
```
Após editar o `/etc/fstab`, execute `sudo mount -a` para montar o compartilhamento imediatamente.
Os comandos abaixo utilizam a variável `NODE_ENV` para carregar o arquivo de ambiente (`.env`) correto.
#### Ambiente de Desenvolvimento
Para rodar em modo de desenvolvimento com reinício automático a cada alteração de arquivo (usando `nodemon`):
```bash
npm run dev
```
#### Ambiente de Produção
Para rodar a aplicação em modo de produção (usando `node`):
```bash
npm start
```
ou o comando explícito:
```bash
npm run start:prod
```
### 3. Executando com PM2
O PM2 é o gerenciador de processos recomendado para ambientes de produção. Ele garante que a aplicação reinicie automaticamente em caso de falhas e facilita o gerenciamento.
- **Para iniciar em modo de produção:**
```bash
pm2 start ecosystem.config.js --env production
```
- **Para iniciar em modo de desenvolvimento:**
```bash
pm2 start ecosystem.config.js --env development
```
- **Comandos úteis do PM2:**
```bash
pm2 list # Lista todos os processos
pm2 logs sn-glpi-sync-cron # Exibe os logs em tempo real
pm2 restart sn-glpi-sync-cron # Reinicia a aplicação
pm2 stop sn-glpi-sync-cron # Para a aplicação
```
## Fluxo de Execução
A aplicação segue uma ordem de execução estrita para garantir a consistência dos dados. O ponto de entrada é o arquivo `src/app.js`, que orquestra a chamada dos seguintes controladores:

View File

@ -1,4 +1,5 @@
const { Sequelize } = require('sequelize');
const { logInfo, logError } = require('../src/utils/logger');
// Database connection configuration
const dbConfig = {
@ -14,9 +15,9 @@ const sequelize = new Sequelize(process.env.DB_NAME, process.env.DB_USER, proces
const testConnection = async () => {
try {
await sequelize.authenticate();
console.log('Connection to the database has been established successfully.');
logInfo('Connection to the database has been established successfully.');
} catch (error) {
console.error('Unable to connect to the database:', error);
logError('Unable to connect to the database:', error);
}
};

44
cron.js Normal file
View File

@ -0,0 +1,44 @@
const path = require('path');
const dotenv = require('dotenv');
const nodeEnv = process.env.NODE_ENV || 'development';
const envFile = nodeEnv === 'production' ? '.env.production' : '.env.development';
dotenv.config({ path: path.resolve(__dirname, envFile) });
const cron = require('node-cron');
const { main: runSyncCycle } = require('./src/app.js');
const { logInfo, logError } = require('./src/utils/logger.js');
let isCronRunning = false;
const CRON_SCHEDULE = process.env.CRON_SCHEDULE || '*/5 * * * *'; // A cada 5 minutos por padrão
logInfo(`⏰ Agendando ciclo de sincronização para rodar com a frequência: ${CRON_SCHEDULE}`);
cron.schedule(CRON_SCHEDULE, async () => {
if (isCronRunning) {
logInfo('CRON: Ciclo anterior ainda em execução. Pulando esta rodada.');
return;
}
isCronRunning = true;
logInfo('CRON: Iniciando ciclo de sincronização...');
try {
await runSyncCycle();
// A função main já loga sua própria mensagem de sucesso, então não precisamos duplicar aqui.
} catch (error) {
logError('CRON: Ocorreu um erro inesperado durante o ciclo de sincronização.', error);
} finally {
logInfo('CRON: Finalizando ciclo de sincronização.');
isCronRunning = false;
}
});
/**
* @file cron.js
* @description Responsável por agendar e executar a rotina de sincronização periodicamente.
* Este arquivo:
* 1. Carrega as variáveis de ambiente apropriadas para o ambiente (desenvolvimento/produção).
* 2. Agenda um `cron job` (usando node-cron) para executar a função principal de sincronização.
* 3. Garante que apenas uma instância do ciclo de sincronização rode por vez.
*/

83
ecosystem.config.js Normal file
View File

@ -0,0 +1,83 @@
module.exports = {
apps: [
// 🕒 --- APLICAÇÃO 1: CRON JOB DE SINCRONIZAÇÃO (Node.js) ---
{
name: 'sn-glpi-sync-cron', // Nome que aparecerá no PM2
script: 'cron.js', // Caminho do arquivo principal que executa o cron
// 👇 Modo "fork" = apenas 1 instância, sem cluster (evita rodar crons duplicados)
exec_mode: 'fork',
instances: 1, // Força a rodar somente um processo
// ⚙️ Variáveis de ambiente padrão (modo development)
env: {
NODE_ENV: 'development',
},
// ⚙️ Variáveis de ambiente quando rodar com `--env production`
env_production: {
NODE_ENV: 'production',
// Você pode sobrescrever outras variáveis aqui para produção, se necessário
// Ex: CRON_SCHEDULE: '*/2 * * * *'
},
// Configurações de log
output: './logs/pm2-out.log',
error: './logs/pm2-error.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss',
},
// 🗺️ --- APLICAÇÃO 2: MAPEADOR DE LOCALIDADES (Python) ---
{
name: 'sn-glpi-location-mapper', // Nome do serviço do mapeador
script: 'src/scripts/python/update_location_mapping.py', // Caminho do script Python
interpreter: 'python3', // Especifica o interpretador a ser usado
exec_mode: 'fork',
instances: 1,
autorestart: true, // Reinicia automaticamente em caso de falha
watch: false, // O script já roda em loop, não precisa de watch
// Garante que o script Python também use as variáveis de ambiente corretas
env: {
NODE_ENV: 'development',
},
env_production: {
NODE_ENV: 'production',
},
// Deixamos o script Python gerenciar seus próprios arquivos de log
output: '/dev/null',
error: '/dev/null',
},
],
};
/**
* @file ecosystem.config.js
* @description Define a configuração do PM2 para gerenciar a aplicação de sincronização.
*
* - `sn-glpi-sync-cron`: Serviço Node.js que executa o ciclo principal de sincronização.
* - `sn-glpi-location-mapper`: Serviço Python que monitora e atualiza o mapeamento de localidades.
* Ambos rodam em modo `fork` para garantir que
* apenas uma instância das tarefas agendadas seja executada por vez, evitando duplicidade.
*
* 💡 Dicas de uso:
*
* 1. Iniciar a aplicação:
* // Para ambiente de desenvolvimento
* pm2 start ecosystem.config.js --env development
*
* // Para ambiente de produção
* pm2 start ecosystem.config.js --env production
*
* 2. Comandos úteis:
* pm2 list // Lista todos os processos gerenciados pelo PM2
* pm2 logs <name> // Exibe os logs de um processo (ex: pm2 logs sn-glpi-sync-cron)
* pm2 monit // Abre um painel de monitoramento
* pm2 restart sn-glpi-sync-cron // Reinicia a aplicação
* pm2 stop sn-glpi-sync-cron // Para a aplicação
* pm2 delete sn-glpi-sync-cron // Remove a aplicação da lista do PM2
*
* 3. Para iniciar com o sistema operacional (em produção):
* pm2 startup
* pm2 save
*/

795
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,10 +2,12 @@
"name": "servicenow-glpi-sync",
"version": "1.0.0",
"description": "A Node.js application for integrating ServiceNow and GLPI.",
"main": "app.js",
"main": "cron.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"start": "npm run start:prod",
"dev": "cross-env NODE_ENV=development nodemon cron.js",
"start:dev": "cross-env NODE_ENV=development node cron.js",
"start:prod": "cross-env NODE_ENV=production node cron.js",
"test": "echo \"No tests specified\" && exit 0"
},
"dependencies": {
@ -13,8 +15,14 @@
"dotenv": "^10.0.0",
"express": "^5.1.0",
"mysql2": "^3.14.5",
"node-cron": "^4.2.1",
"pg": "^8.7.1",
"winston": "^3.17.0"
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0"
},
"devDependencies": {
"cross-env": "^7.0.3",
"nodemon": "^3.1.4"
},
"author": "Your Name",
"license": "MIT"

View File

@ -1,7 +1,7 @@
const path = require('path');
const dotenv = require('dotenv');
const nodeEnv = process.env.NODE_ENV;
let envFile = '.env.devlopment';
let envFile = '.env.development';
if (nodeEnv === 'production') {envFile = '.env.production';} else {envFile = '.env.development';}
dotenv.config({ path: path.resolve(__dirname, '..', envFile) });
@ -30,7 +30,11 @@ async function main() {
}
}
main();
// A chamada main() foi removida daqui.
// Agora, o app.js apenas define e exporta a função main.
// A responsabilidade de EXECUTAR a função main é do cron.js,
// que a importará e a chamará dentro do agendamento.
module.exports = { main };
/**
* @file app.js

View File

@ -1,14 +1,20 @@
const {fetchCommentsFromServiceNow } = require('../services/servicenowService');
const { syncCommentsGlpitoSN, syncCommentsSNtoGlpi } = require('../services/glpiCommentService');
const TicketSyncModel = require('../models/ticketSyncModel');
const { logInfo } = require('../utils/logger');
const processCommentsController = async () => {
const ticketsToMonitor = await TicketSyncModel.getTicketsToMonitor();
logInfo(`📋Tickets em monitoramento: ${ticketsToMonitor.length}`);
for (const ticket of ticketsToMonitor){
// Sincroniza comentários do ServiceNow para o GLPI
const comments = await fetchCommentsFromServiceNow(ticket);
// Se houver comentários novos no ServiceNow, sincroniza para o GLPI
if(comments.length != 0){await syncCommentsSNtoGlpi(comments, ticket);}
// Sincroniza comentários do GLPI para o ServiceNow
await syncCommentsGlpitoSN(ticket);
}
}

View File

@ -90,7 +90,7 @@ const processStatusAndClosureController = async () => {
if (solution?.solutiontypes_id === 27) {
logInfo(`REGRA DETECTADA: Ticket ${ticket.glpi_ticket_id} está fora do escopo (SolutionType ID: 27).`);
const justification = solution?.content || 'Ticket marcado como "Fora do escopo" no GLPI.';
await addWorkNoteToServiceNow(ticket.sn_ticket_id, `Ticket fechado no GLPI com a justificativa: ${justification}`);
await addWorkNoteToServiceNow(ticket.sn_ticket_id, `Ticket não será mais exibido para equipe Sothis, com a justificativa: ${justification}`);
await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'closed', 'ignored');
await TicketGlpiModel.forceCloseTicket(ticket.glpi_ticket_id);
} else {

View File

@ -1,23 +1,31 @@
// src/data/database.js
// Configuração da conexão com o banco de dados PostgreSQL
const { Pool } = require('pg');
const { logInfo, logError } = require('../utils/logger');
const pool = new Pool({
host: '10.0.120.75',
port: 5432,
database: 'snglpi',
user: 'desenvolvimento',
password: 'Ut@2S@$M9Xs@@W'
});
const poolConfig = {
host: process.env.SNGLPI_DB_HOST ,
port: process.env.SNGLPI_DB_PORT || 5432,
database: process.env.SNGLPI_DB_NAME,
user: process.env.SNGLPI_DB_USER,
password: process.env.SNGLPI_DB_PASSWORD
};
const pool = new Pool(poolConfig);
// Log da configuração do pool para depuração (sem a senha)
const sanitizedConfig = { ...poolConfig, password: '*****' };
logInfo('--- Configuração do Pool PostgreSQL (Banco Intermediário) ---');
logInfo(sanitizedConfig);
logInfo('----------------------------------------------------------');
// testa a conexao
pool.on('connect', () => {
console.log('Conectado ao PostgreSQL');
logInfo('Conectado ao PostgreSQL');
});
pool.on('error', (err) => {
console.error('Erro de conexão PostgreSQL:', err);
logError('Erro de conexão PostgreSQL:', err);
});
//exporta o pool para ser usado em outros arquivos

View File

@ -2,6 +2,9 @@ const glpiPool = require('../data/glpiDataBase');
const { logInfo, logError } = require('../utils/logger');
const LocationMappingModel = require('./locationMappingModel');
// Carrega o ID do usuário padrão do GLPI a partir das variáveis de ambiente
const GLPI_DEFAULT_USER_ID = parseInt(process.env.GLPI_DEFAULT_USER_ID, 10) || 1118;
class TicketGlpiModel {
static mapStatus(snState) {
@ -186,8 +189,8 @@ class TicketGlpiModel {
name: formattedTitle,
date: ticketData.opened_at || new Date(),
date_mod: ticketData.updated_at || new Date(),
status: TicketGlpiModel.mapStatus(ticketData.state || '1'),
users_id_recipient: 1118,
status: TicketGlpiModel.mapStatus(ticketData.state || '1'), // O status é mapeado
users_id_recipient: GLPI_DEFAULT_USER_ID,
requesttypes_id: 1,
content: formattedDescription,
urgency: 3,
@ -211,8 +214,8 @@ class TicketGlpiModel {
await connection.execute(
`INSERT INTO glpi_tickets_users
(tickets_id, users_id, type, use_notification, alternative_email)
VALUES (?, ?, 1, 1, '')`,
[ticketId, 1118]
VALUES (?, ?, 1, 1, '')`, // 1 = Requerente
[ticketId, GLPI_DEFAULT_USER_ID]
);
await connection.execute(
@ -260,8 +263,9 @@ class TicketGlpiModel {
static async getFollowupsByItemId(itemId) {
const connection = await glpiPool.getConnection();
try {
const query = 'SELECT id, content, date, date_creation, date_mod FROM glpi_itilfollowups WHERE items_id = ? AND users_id != 1118';
const [rows] = await connection.execute(query, [itemId]);
const query = 'SELECT id, content, date, date_creation, date_mod FROM glpi_itilfollowups WHERE items_id = ? AND users_id != ?';
const [rows] = await connection.execute(query,[itemId,GLPI_DEFAULT_USER_ID]);
return rows;
} catch (error) {
logError(error, 'Erro ao buscar followups do ticket');
@ -273,7 +277,7 @@ class TicketGlpiModel {
const connection = await glpiPool.getConnection();
try {
const query = 'INSERT INTO glpi_itilfollowups(itemtype, items_id, date, users_id, content, date_creation, date_mod, timeline_position) VALUES(?, ?, NOW(), ?, ?, NOW(), NOW(), ?)';
const values = ['Ticket', items_id, 1118, comment.content , 1];
const values = ['Ticket', items_id, GLPI_DEFAULT_USER_ID, comment.content , 1];
const [result] = await connection.execute(query, values);
return [{ id: result.insertId }];
@ -313,7 +317,7 @@ class TicketGlpiModel {
await connection.beginTransaction();
const solutionQuery = `INSERT INTO glpi_itilsolutions (itemtype, items_id, content, users_id_editor, date_creation, date_mod) VALUES ('Ticket', ?, ?, ?, NOW(), NOW())`;
await connection.execute(solutionQuery, [ticketId, solutionContent, 1118]);
await connection.execute(solutionQuery, [ticketId, solutionContent, GLPI_DEFAULT_USER_ID]);
const updateTicketQuery = `UPDATE glpi_tickets SET status = 5, solvedate = NOW() WHERE id = ?`;
await connection.execute(updateTicketQuery, [ticketId]);

View File

@ -1,6 +1,6 @@
const { query } = require('winston');
const pool = require('../data/database');
const { logError } = require('../utils/logger');
const { logError, logInfo } = require('../utils/logger');
class TicketSyncModel {

View File

@ -10,14 +10,14 @@ class TicketUpdateModel {
INSERT INTO ticket_updates (ticket_sync_id, update_type, source_system, content, author, created_at, updated_at, source_id, destiny_id)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
ON CONFLICT (source_id) DO UPDATE SET
ticket_sync_id = $1,
update_type = $2,
source_system = $3,
content = $4,
author = $5,
created_at = $6,
updated_at = $7,
destiny_id = $9
ticket_sync_id = EXCLUDED.ticket_sync_id,
update_type = EXCLUDED.update_type,
source_system = EXCLUDED.source_system,
content = EXCLUDED.content,
author = EXCLUDED.author,
created_at = EXCLUDED.created_at,
updated_at = EXCLUDED.updated_at,
destiny_id = EXCLUDED.destiny_id
RETURNING *;
`;
const values = [

View File

@ -98,6 +98,9 @@ CREATE TABLE ticket_updates (
ON DELETE CASCADE
);
-- Adciona a restrição de unicidade na coluna source_id
ALTER TABLE ticket_updates ADD CONSTRAINT ticket_updates_source_id_unique UNIQUE (source_id);
-- Índices para melhor performance nas consultas frequentes
CREATE INDEX idx_ticket_updates_sync ON ticket_updates(ticket_sync_id);
CREATE INDEX idx_ticket_updates_source ON ticket_updates(source_system);
@ -244,9 +247,6 @@ COMMENT ON COLUMN ticket_updates.update_type IS 'Tipo de atualização: task, no
COMMENT ON COLUMN ticket_updates.source_system IS 'Sistema de origem: sn (ServiceNow) ou glpi';
COMMENT ON COLUMN ticket_updates.content IS 'Conteúdo da atualização/tarefa';
COMMENT ON COLUMN ticket_updates.author IS 'Autor da atualização';
COMMENT ON COLUMN ticket_updates.created_at_source IS 'Data e hora de criação no sistema de origem';
COMMENT ON COLUMN ticket_updates.sync_status IS 'Status da sincronização: pending, completed, error';
COMMENT ON COLUMN ticket_updates.sync_date IS 'Data e hora da sincronização';
COMMENT ON COLUMN ticket_updates.created_at IS 'Data e hora de criação do registro';
COMMENT ON COLUMN ticket_updates.updated_at IS 'Data e hora da última atualização do registro';
@ -284,3 +284,12 @@ COMMENT ON COLUMN sync_control.last_run_at IS 'Timestamp da última vez que o jo
-- Registro inicial para a marca d'água do GLPI.
INSERT INTO sync_control (system_name, last_watermark) VALUES ('glpi', '1970-01-01 00:00:00+00');
SELECT * FROM ticket_sync WHERE glpi_sync_status != 'ignored';
SELECT * FROM tickets_sn WHERE status != 'Encerrado' AND status != 'Encerrado - Omitido';
SELECT * FROM public.location_mapping
DELETE FROM tickets_sn WHERE status != 'Encerrado' AND status != 'Encerrado - Omitido';
UPDATE sync_control
SET last_watermark = '1970-01-01 00:00:00+00';

View File

@ -1,4 +1,4 @@
pandas
psycopg2-binary
mysql-connector-python
python-dotenv
pandas

View File

@ -1,13 +1,14 @@
#!/usr/bin/env python3
import os
from dotenv import load_dotenv
import time
import logging
import psycopg2
import mysql.connector
from mysql.connector import Error
import pandas as pd
import logging
from logging.handlers import TimedRotatingFileHandler
from datetime import datetime, timedelta
from dotenv import load_dotenv
from pathlib import Path
# ===========================
@ -16,7 +17,7 @@ from pathlib import Path
env = os.getenv('NODE_ENV', 'production')
env_file = f".env.{env}"
env_path = Path(__file__).resolve().parents[2] / env_file
env_path = Path(__file__).resolve().parents[3] / env_file
load_dotenv(dotenv_path=env_path)
print(f"🌎 Ambiente carregado: {env} ({env_path})")
@ -25,24 +26,41 @@ print(f"🌎 Ambiente carregado: {env} ({env_path})")
# 🪵 Configuração de Logs
# ===========================
LOG_DIR = Path(__file__).resolve().parents[3] / "logs" / "location"
ERROR_DIR = Path(__file__).resolve().parents[3] / "logs" / "error"
LOG_DIR = Path(__file__).resolve().parents[3] / "logs"
LOG_DIR.mkdir(parents=True, exist_ok=True)
ERROR_DIR.mkdir(parents=True, exist_ok=True)
log_file = LOG_DIR / f"location_{datetime.now().strftime('%Y-%m-%d')}.log"
error_file = ERROR_DIR / f"location_error_{datetime.now().strftime('%Y-%m-%d')}.log"
# Cria um logger
logger = logging.getLogger("LocationMappingLogger")
logger.setLevel(logging.INFO)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s",
handlers=[
logging.FileHandler(log_file, encoding="utf-8"),
logging.StreamHandler(),
logging.FileHandler(error_file, encoding="utf-8")
]
# Evita que os logs se propaguem para o logger raiz
logger.propagate = False
# Formato do log
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
# Handler para o console (útil em desenvolvimento)
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
# Handler para rotação diária de arquivos de log gerais
app_log_handler = TimedRotatingFileHandler(
LOG_DIR / "location-app.log", when="midnight", interval=1, backupCount=10, encoding="utf-8"
)
app_log_handler.setFormatter(formatter)
# Handler para rotação diária de arquivos de log de erro
error_log_handler = TimedRotatingFileHandler(
LOG_DIR / "location-error.log", when="midnight", interval=1, backupCount=10, encoding="utf-8"
)
error_log_handler.setLevel(logging.ERROR)
error_log_handler.setFormatter(formatter)
# Adiciona os handlers ao logger
logger.addHandler(console_handler)
logger.addHandler(app_log_handler)
logger.addHandler(error_log_handler)
# ===========================
# 💾 Configurações dos Bancos
@ -74,15 +92,15 @@ CSV_PATH = os.getenv('LOCATION_MAPPING_CSV_PATH')
def csv_recently_modified(minutes=2):
"""Verifica se o CSV foi alterado recentemente."""
if not os.path.exists(CSV_PATH):
logging.error(f"Arquivo CSV não encontrado: {CSV_PATH}")
logger.error(f"Arquivo CSV não encontrado: {CSV_PATH}")
return False
last_mod_time = datetime.fromtimestamp(os.path.getmtime(CSV_PATH))
if datetime.now() - last_mod_time <= timedelta(minutes=minutes):
logging.info(f"CSV modificado recentemente ({last_mod_time})")
logger.info(f"CSV modificado recentemente ({last_mod_time})")
return True
else:
logging.debug(f"CSV não foi modificado nos últimos {minutes} minutos.")
logger.debug(f"CSV não foi modificado nos últimos {minutes} minutos.")
return False
@ -90,7 +108,7 @@ def get_snglpi_connection():
try:
return psycopg2.connect(**SNGLPI_DB_CONFIG)
except Exception as e:
logging.error(f"Erro ao conectar ao banco SNGLPI: {e}")
logger.error(f"Erro ao conectar ao banco SNGLPI: {e}", exc_info=True)
return None
@ -98,7 +116,7 @@ def get_glpi_connection():
try:
return mysql.connector.connect(**GLPI_DB_CONFIG)
except Error as e:
logging.error(f"Erro ao conectar ao banco GLPI: {e}")
logger.error(f"Erro ao conectar ao banco GLPI: {e}", exc_info=True)
return None
@ -114,7 +132,7 @@ def find_sn_location_id(location_name, cursor):
result = cursor.fetchone()
return result[0] if result else None
except Exception as e:
logging.error(f"Erro ao buscar location_id ({location_name}): {e}")
logger.error(f"Erro ao buscar location_id ({location_name}): {e}", exc_info=True)
return None
@ -130,7 +148,7 @@ def find_glpi_entity_id(entity_name, cursor):
result = cursor.fetchone()
return result[0] if result else None
except Exception as e:
logging.error(f"Erro ao buscar entity_id ({entity_name}): {e}")
logger.error(f"Erro ao buscar entity_id ({entity_name}): {e}", exc_info=True)
return None
@ -138,59 +156,48 @@ def update_mapping_table():
"""Atualiza a tabela de mapeamento a partir do CSV."""
try:
if not os.path.exists(CSV_PATH):
logging.error(f"Arquivo CSV não encontrado: {CSV_PATH}")
logger.error(f"Arquivo CSV não encontrado: {CSV_PATH}")
return False
snglpi_conn = get_snglpi_connection()
glpi_conn = get_glpi_connection()
with get_snglpi_connection() as snglpi_conn, get_glpi_connection() as glpi_conn:
if not snglpi_conn or not glpi_conn:
return False
if not snglpi_conn or not glpi_conn:
return False
with snglpi_conn.cursor() as snglpi_cur, glpi_conn.cursor() as glpi_cur:
df = pd.read_csv(CSV_PATH, encoding='latin-1', sep=';')
df['sn_location_id'] = None
df['glpi_entity_id'] = None
snglpi_cur = snglpi_conn.cursor()
glpi_cur = glpi_conn.cursor()
mappings = []
for _, row in df.iterrows():
sn_name = row['sn_location_name']
glpi_name = row['glpi_entity_name']
df = pd.read_csv(CSV_PATH, encoding='latin-1', sep=';')
df['sn_location_id'] = None
df['glpi_entity_id'] = None
sn_id = find_sn_location_id(sn_name, snglpi_cur)
glpi_id = find_glpi_entity_id(glpi_name, glpi_cur)
mappings = []
for _, row in df.iterrows():
sn_name = row['sn_location_name']
glpi_name = row['glpi_entity_name']
if glpi_id:
mappings.append((sn_id, sn_name, glpi_id, glpi_name))
else:
logger.warning(f"Entity não encontrada: {glpi_name}")
sn_id = find_sn_location_id(sn_name, snglpi_cur)
glpi_id = find_glpi_entity_id(glpi_name, glpi_cur)
if glpi_id:
mappings.append((sn_id, sn_name, glpi_id, glpi_name))
else:
logging.warning(f"Entity não encontrada: {glpi_name}")
if mappings:
snglpi_cur.execute("DELETE FROM location_mapping")
insert_query = """
INSERT INTO location_mapping
(sn_location_id, sn_location_name, glpi_entity_id, glpi_entity_name)
VALUES (%s, %s, %s, %s)
"""
snglpi_cur.executemany(insert_query, mappings)
snglpi_conn.commit()
logging.info(f"Tabela atualizada com {len(mappings)} registros.")
else:
logging.warning("Nenhum mapeamento válido encontrado.")
snglpi_cur.close()
glpi_cur.close()
snglpi_conn.close()
glpi_conn.close()
if mappings:
snglpi_cur.execute("DELETE FROM location_mapping")
insert_query = """
INSERT INTO location_mapping
(sn_location_id, sn_location_name, glpi_entity_id, glpi_entity_name)
VALUES (%s, %s, %s, %s)
"""
snglpi_cur.executemany(insert_query, mappings)
snglpi_conn.commit()
logger.info(f"Tabela atualizada com {len(mappings)} registros.")
else:
logger.warning("Nenhum mapeamento válido encontrado.")
return True
except Exception as e:
logging.error(f"Erro no processo de atualização: {e}")
if 'snglpi_conn' in locals():
snglpi_conn.rollback()
logger.error(f"Erro no processo de atualização: {e}", exc_info=True)
return False
@ -199,16 +206,16 @@ def update_mapping_table():
# ===========================
if __name__ == "__main__":
logging.info("🚀 Iniciando monitoramento contínuo do Location Mapping")
logger.info("🚀 Iniciando monitoramento contínuo do Location Mapping")
while True:
try:
if csv_recently_modified(minutes=2):
success = update_mapping_table()
if success:
logging.info("✅ Atualização concluída com sucesso.")
logger.info("✅ Atualização concluída com sucesso.")
else:
logging.error("❌ Falha na atualização.")
logger.error("❌ Falha na atualização.")
time.sleep(60)
except Exception as e:
logging.error(f"Erro inesperado no loop principal: {e}")
logger.error(f"Erro inesperado no loop principal: {e}", exc_info=True)
time.sleep(60)

View File

@ -1,5 +1,6 @@
const winston = require('winston');
const path = require('path');
require('winston-daily-rotate-file');
const fs = require('fs');
// verifica se a pasta de logs existe, se não, cria
@ -11,27 +12,45 @@ if (!fs.existsSync(logsDir)) {
// Configuração do logger com winston
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss'
}),
winston.format.errors({ stack: true }), // ← Mostra stack trace de erros
winston.format.json()
),
format: winston.format.combine(winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), winston.format.errors({ stack: true })),
transports: [
// Log geral da aplicação
new winston.transports.File({
filename: path.join(logsDir, 'app.log'),
maxsize: 5242880, // 5MB
maxFiles: 5
new winston.transports.DailyRotateFile({
filename: path.join(logsDir, 'app-%DATE%.log'),
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '5m',
maxFiles: '10d',
format: winston.format.combine(
winston.format.printf((info) => {
const { timestamp, level, message, stack, ...meta } = info;
let logMessage = `${timestamp} [${level}]: ${stack || message}`;
if (Object.keys(meta).length) {
logMessage += ` ${JSON.stringify(meta, null, 2)}`;
}
return logMessage;
})
),
}),
// Log de erros
new winston.transports.File({
filename: path.join(logsDir, 'error.log'),
new winston.transports.DailyRotateFile({
filename: path.join(logsDir, 'error-%DATE%.log'),
level: 'error',
maxsize: 5242880,
maxFiles: 3
}),
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '5m',
maxFiles: '10d',
format: winston.format.combine(
winston.format.printf((info) => {
const { timestamp, level, message, stack, ...meta } = info;
let logMessage = `${timestamp} [${level}]: ${stack || message}`;
if (Object.keys(meta).length) {
logMessage += ` ${JSON.stringify(meta, null, 2)}`;
}
return logMessage;
})
),
})
],
});
@ -40,8 +59,13 @@ if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.printf(({ timestamp, level, message, stack }) => {
return `${timestamp} [${level}]: ${stack || message}`;
winston.format.printf((info) => {
const { timestamp, level, message, stack, ...meta } = info;
let logMessage = `${timestamp} [${level}]: ${stack || message}`;
if (Object.keys(meta).length) {
logMessage += ` ${JSON.stringify(meta, null, 2)}`;
}
return logMessage;
})
)
}));
@ -78,5 +102,5 @@ module.exports = {
logError,
logInfo,
logWarning,
logSync
logSync,
};