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:
parent
fb3f4eee75
commit
25458d5e44
@ -4,6 +4,9 @@
|
|||||||
NODE_ENV=development
|
NODE_ENV=development
|
||||||
PORT=3000
|
PORT=3000
|
||||||
|
|
||||||
|
# Frequência de execução do cron job (formato cron). Padrão: '* * * * *' (a cada 1 minutos)
|
||||||
|
CRON_SCHEDULE='* * * * *'
|
||||||
|
|
||||||
# ServiceNow API (Dev)
|
# ServiceNow API (Dev)
|
||||||
SERVICENOW_INSTANCE=https://caoadev.service-now.com
|
SERVICENOW_INSTANCE=https://caoadev.service-now.com
|
||||||
SERVICENOW_USERNAME=SOTHIS.CAOA
|
SERVICENOW_USERNAME=SOTHIS.CAOA
|
||||||
@ -26,22 +29,25 @@ SERVICENOW_DEFAULT_USER_SYSID=b1312fda977a1250501634a6f053af98
|
|||||||
|
|
||||||
# GLPI (banco real)
|
# GLPI (banco real)
|
||||||
GLPI_DB_TYPE=mysql
|
GLPI_DB_TYPE=mysql
|
||||||
GLPI_DB_HOST=177.73.177.32
|
GLPI_DB_HOST=177.73.177.44
|
||||||
GLPI_DB_PORT=3306
|
GLPI_DB_PORT=3306
|
||||||
GLPI_DB_USER=snglpi
|
GLPI_DB_USER=desenvolvimento
|
||||||
GLPI_DB_PASSWORD=j2633669
|
GLPI_DB_PASSWORD=Ut@2S@$M9Xs@@W
|
||||||
GLPI_DB_NAME=glpi_data
|
GLPI_DB_NAME=glpi_data
|
||||||
GLPI_DB_CHARSET=utf8mb4
|
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)
|
# Banco intermediário (PostgreSQL)
|
||||||
SNGLPI_DB_HOST=localhost
|
SNGLPI_DB_HOST=10.0.120.75
|
||||||
SNGLPI_DB_PORT=5432
|
SNGLPI_DB_PORT=5432
|
||||||
SNGLPI_DB_NAME=snglpi
|
SNGLPI_DB_NAME=snglpi-development
|
||||||
SNGLPI_DB_USER=desenvolvimento
|
SNGLPI_DB_USER=desenvolvimento
|
||||||
SNGLPI_DB_PASSWORD=Ut@2S@$M9Xs@@W
|
SNGLPI_DB_PASSWORD=Ut@2S@$M9Xs@@W
|
||||||
|
|
||||||
# CSV de mapeamento
|
# 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
|
# Logging
|
||||||
LOG_LEVEL=debug
|
LOG_LEVEL=debug
|
||||||
|
|||||||
57
.env.example
Normal file
57
.env.example
Normal 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
|
||||||
78
README.md
78
README.md
@ -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
|
## 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:
|
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:
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
const { Sequelize } = require('sequelize');
|
const { Sequelize } = require('sequelize');
|
||||||
|
const { logInfo, logError } = require('../src/utils/logger');
|
||||||
|
|
||||||
// Database connection configuration
|
// Database connection configuration
|
||||||
const dbConfig = {
|
const dbConfig = {
|
||||||
@ -14,9 +15,9 @@ const sequelize = new Sequelize(process.env.DB_NAME, process.env.DB_USER, proces
|
|||||||
const testConnection = async () => {
|
const testConnection = async () => {
|
||||||
try {
|
try {
|
||||||
await sequelize.authenticate();
|
await sequelize.authenticate();
|
||||||
console.log('Connection to the database has been established successfully.');
|
logInfo('Connection to the database has been established successfully.');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Unable to connect to the database:', error);
|
logError('Unable to connect to the database:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
44
cron.js
Normal file
44
cron.js
Normal 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
83
ecosystem.config.js
Normal 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
795
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
@ -2,10 +2,12 @@
|
|||||||
"name": "servicenow-glpi-sync",
|
"name": "servicenow-glpi-sync",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "A Node.js application for integrating ServiceNow and GLPI.",
|
"description": "A Node.js application for integrating ServiceNow and GLPI.",
|
||||||
"main": "app.js",
|
"main": "cron.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node server.js",
|
"start": "npm run start:prod",
|
||||||
"dev": "nodemon server.js",
|
"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"
|
"test": "echo \"No tests specified\" && exit 0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -13,8 +15,14 @@
|
|||||||
"dotenv": "^10.0.0",
|
"dotenv": "^10.0.0",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"mysql2": "^3.14.5",
|
"mysql2": "^3.14.5",
|
||||||
|
"node-cron": "^4.2.1",
|
||||||
"pg": "^8.7.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",
|
"author": "Your Name",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const dotenv = require('dotenv');
|
const dotenv = require('dotenv');
|
||||||
const nodeEnv = process.env.NODE_ENV;
|
const nodeEnv = process.env.NODE_ENV;
|
||||||
let envFile = '.env.devlopment';
|
let envFile = '.env.development';
|
||||||
if (nodeEnv === 'production') {envFile = '.env.production';} else {envFile = '.env.development';}
|
if (nodeEnv === 'production') {envFile = '.env.production';} else {envFile = '.env.development';}
|
||||||
dotenv.config({ path: path.resolve(__dirname, '..', envFile) });
|
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
|
* @file app.js
|
||||||
|
|||||||
@ -1,14 +1,20 @@
|
|||||||
const {fetchCommentsFromServiceNow } = require('../services/servicenowService');
|
const {fetchCommentsFromServiceNow } = require('../services/servicenowService');
|
||||||
const { syncCommentsGlpitoSN, syncCommentsSNtoGlpi } = require('../services/glpiCommentService');
|
const { syncCommentsGlpitoSN, syncCommentsSNtoGlpi } = require('../services/glpiCommentService');
|
||||||
const TicketSyncModel = require('../models/ticketSyncModel');
|
const TicketSyncModel = require('../models/ticketSyncModel');
|
||||||
|
const { logInfo } = require('../utils/logger');
|
||||||
|
|
||||||
|
|
||||||
const processCommentsController = async () => {
|
const processCommentsController = async () => {
|
||||||
const ticketsToMonitor = await TicketSyncModel.getTicketsToMonitor();
|
const ticketsToMonitor = await TicketSyncModel.getTicketsToMonitor();
|
||||||
|
logInfo(`📋Tickets em monitoramento: ${ticketsToMonitor.length}`);
|
||||||
|
|
||||||
|
|
||||||
for (const ticket of ticketsToMonitor){
|
for (const ticket of ticketsToMonitor){
|
||||||
|
// Sincroniza comentários do ServiceNow para o GLPI
|
||||||
const comments = await fetchCommentsFromServiceNow(ticket);
|
const comments = await fetchCommentsFromServiceNow(ticket);
|
||||||
|
// Se houver comentários novos no ServiceNow, sincroniza para o GLPI
|
||||||
if(comments.length != 0){await syncCommentsSNtoGlpi(comments, ticket);}
|
if(comments.length != 0){await syncCommentsSNtoGlpi(comments, ticket);}
|
||||||
|
// Sincroniza comentários do GLPI para o ServiceNow
|
||||||
await syncCommentsGlpitoSN(ticket);
|
await syncCommentsGlpitoSN(ticket);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -90,7 +90,7 @@ const processStatusAndClosureController = async () => {
|
|||||||
if (solution?.solutiontypes_id === 27) {
|
if (solution?.solutiontypes_id === 27) {
|
||||||
logInfo(`REGRA DETECTADA: Ticket ${ticket.glpi_ticket_id} está fora do escopo (SolutionType 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.';
|
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 TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'closed', 'ignored');
|
||||||
await TicketGlpiModel.forceCloseTicket(ticket.glpi_ticket_id);
|
await TicketGlpiModel.forceCloseTicket(ticket.glpi_ticket_id);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -1,23 +1,31 @@
|
|||||||
// src/data/database.js
|
// src/data/database.js
|
||||||
// Configuração da conexão com o banco de dados PostgreSQL
|
// Configuração da conexão com o banco de dados PostgreSQL
|
||||||
const { Pool } = require('pg');
|
const { Pool } = require('pg');
|
||||||
|
const { logInfo, logError } = require('../utils/logger');
|
||||||
|
|
||||||
const pool = new Pool({
|
const poolConfig = {
|
||||||
host: '10.0.120.75',
|
host: process.env.SNGLPI_DB_HOST ,
|
||||||
port: 5432,
|
port: process.env.SNGLPI_DB_PORT || 5432,
|
||||||
database: 'snglpi',
|
database: process.env.SNGLPI_DB_NAME,
|
||||||
user: 'desenvolvimento',
|
user: process.env.SNGLPI_DB_USER,
|
||||||
password: 'Ut@2S@$M9Xs@@W'
|
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
|
// testa a conexao
|
||||||
pool.on('connect', () => {
|
pool.on('connect', () => {
|
||||||
console.log('Conectado ao PostgreSQL');
|
logInfo('Conectado ao PostgreSQL');
|
||||||
});
|
});
|
||||||
|
|
||||||
pool.on('error', (err) => {
|
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
|
//exporta o pool para ser usado em outros arquivos
|
||||||
|
|||||||
@ -2,6 +2,9 @@ const glpiPool = require('../data/glpiDataBase');
|
|||||||
const { logInfo, logError } = require('../utils/logger');
|
const { logInfo, logError } = require('../utils/logger');
|
||||||
const LocationMappingModel = require('./locationMappingModel');
|
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 {
|
class TicketGlpiModel {
|
||||||
|
|
||||||
static mapStatus(snState) {
|
static mapStatus(snState) {
|
||||||
@ -186,8 +189,8 @@ class TicketGlpiModel {
|
|||||||
name: formattedTitle,
|
name: formattedTitle,
|
||||||
date: ticketData.opened_at || new Date(),
|
date: ticketData.opened_at || new Date(),
|
||||||
date_mod: ticketData.updated_at || new Date(),
|
date_mod: ticketData.updated_at || new Date(),
|
||||||
status: TicketGlpiModel.mapStatus(ticketData.state || '1'),
|
status: TicketGlpiModel.mapStatus(ticketData.state || '1'), // O status é mapeado
|
||||||
users_id_recipient: 1118,
|
users_id_recipient: GLPI_DEFAULT_USER_ID,
|
||||||
requesttypes_id: 1,
|
requesttypes_id: 1,
|
||||||
content: formattedDescription,
|
content: formattedDescription,
|
||||||
urgency: 3,
|
urgency: 3,
|
||||||
@ -211,8 +214,8 @@ class TicketGlpiModel {
|
|||||||
await connection.execute(
|
await connection.execute(
|
||||||
`INSERT INTO glpi_tickets_users
|
`INSERT INTO glpi_tickets_users
|
||||||
(tickets_id, users_id, type, use_notification, alternative_email)
|
(tickets_id, users_id, type, use_notification, alternative_email)
|
||||||
VALUES (?, ?, 1, 1, '')`,
|
VALUES (?, ?, 1, 1, '')`, // 1 = Requerente
|
||||||
[ticketId, 1118]
|
[ticketId, GLPI_DEFAULT_USER_ID]
|
||||||
);
|
);
|
||||||
|
|
||||||
await connection.execute(
|
await connection.execute(
|
||||||
@ -260,8 +263,9 @@ class TicketGlpiModel {
|
|||||||
static async getFollowupsByItemId(itemId) {
|
static async getFollowupsByItemId(itemId) {
|
||||||
const connection = await glpiPool.getConnection();
|
const connection = await glpiPool.getConnection();
|
||||||
try {
|
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;
|
return rows;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, 'Erro ao buscar followups do ticket');
|
logError(error, 'Erro ao buscar followups do ticket');
|
||||||
@ -273,7 +277,7 @@ class TicketGlpiModel {
|
|||||||
const connection = await glpiPool.getConnection();
|
const connection = await glpiPool.getConnection();
|
||||||
try {
|
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 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);
|
const [result] = await connection.execute(query, values);
|
||||||
return [{ id: result.insertId }];
|
return [{ id: result.insertId }];
|
||||||
@ -313,7 +317,7 @@ class TicketGlpiModel {
|
|||||||
await connection.beginTransaction();
|
await connection.beginTransaction();
|
||||||
|
|
||||||
const solutionQuery = `INSERT INTO glpi_itilsolutions (itemtype, items_id, content, users_id_editor, date_creation, date_mod) VALUES ('Ticket', ?, ?, ?, NOW(), NOW())`;
|
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 = ?`;
|
const updateTicketQuery = `UPDATE glpi_tickets SET status = 5, solvedate = NOW() WHERE id = ?`;
|
||||||
await connection.execute(updateTicketQuery, [ticketId]);
|
await connection.execute(updateTicketQuery, [ticketId]);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
const { query } = require('winston');
|
const { query } = require('winston');
|
||||||
const pool = require('../data/database');
|
const pool = require('../data/database');
|
||||||
const { logError } = require('../utils/logger');
|
const { logError, logInfo } = require('../utils/logger');
|
||||||
|
|
||||||
class TicketSyncModel {
|
class TicketSyncModel {
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
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)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||||
ON CONFLICT (source_id) DO UPDATE SET
|
ON CONFLICT (source_id) DO UPDATE SET
|
||||||
ticket_sync_id = $1,
|
ticket_sync_id = EXCLUDED.ticket_sync_id,
|
||||||
update_type = $2,
|
update_type = EXCLUDED.update_type,
|
||||||
source_system = $3,
|
source_system = EXCLUDED.source_system,
|
||||||
content = $4,
|
content = EXCLUDED.content,
|
||||||
author = $5,
|
author = EXCLUDED.author,
|
||||||
created_at = $6,
|
created_at = EXCLUDED.created_at,
|
||||||
updated_at = $7,
|
updated_at = EXCLUDED.updated_at,
|
||||||
destiny_id = $9
|
destiny_id = EXCLUDED.destiny_id
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
`;
|
`;
|
||||||
const values = [
|
const values = [
|
||||||
|
|||||||
@ -98,6 +98,9 @@ CREATE TABLE ticket_updates (
|
|||||||
ON DELETE CASCADE
|
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
|
-- Índices para melhor performance nas consultas frequentes
|
||||||
CREATE INDEX idx_ticket_updates_sync ON ticket_updates(ticket_sync_id);
|
CREATE INDEX idx_ticket_updates_sync ON ticket_updates(ticket_sync_id);
|
||||||
CREATE INDEX idx_ticket_updates_source ON ticket_updates(source_system);
|
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.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.content IS 'Conteúdo da atualização/tarefa';
|
||||||
COMMENT ON COLUMN ticket_updates.author IS 'Autor da atualização';
|
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.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';
|
COMMENT ON COLUMN ticket_updates.updated_at IS 'Data e hora da última atualização do registro';
|
||||||
|
|
||||||
@ -283,4 +283,13 @@ COMMENT ON COLUMN sync_control.last_run_status IS 'Status da última execução
|
|||||||
COMMENT ON COLUMN sync_control.last_run_at IS 'Timestamp da última vez que o job rodou.';
|
COMMENT ON COLUMN sync_control.last_run_at IS 'Timestamp da última vez que o job rodou.';
|
||||||
|
|
||||||
-- Registro inicial para a marca d'água do GLPI.
|
-- 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');
|
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';
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
|
pandas
|
||||||
psycopg2-binary
|
psycopg2-binary
|
||||||
mysql-connector-python
|
mysql-connector-python
|
||||||
python-dotenv
|
python-dotenv
|
||||||
pandas
|
|
||||||
@ -1,13 +1,14 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import os
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
import time
|
import time
|
||||||
import logging
|
|
||||||
import psycopg2
|
import psycopg2
|
||||||
import mysql.connector
|
import mysql.connector
|
||||||
from mysql.connector import Error
|
from mysql.connector import Error
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
import logging
|
||||||
|
from logging.handlers import TimedRotatingFileHandler
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from dotenv import load_dotenv
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# ===========================
|
# ===========================
|
||||||
@ -16,7 +17,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
env = os.getenv('NODE_ENV', 'production')
|
env = os.getenv('NODE_ENV', 'production')
|
||||||
env_file = f".env.{env}"
|
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)
|
load_dotenv(dotenv_path=env_path)
|
||||||
|
|
||||||
print(f"🌎 Ambiente carregado: {env} ({env_path})")
|
print(f"🌎 Ambiente carregado: {env} ({env_path})")
|
||||||
@ -25,24 +26,41 @@ print(f"🌎 Ambiente carregado: {env} ({env_path})")
|
|||||||
# 🪵 Configuração de Logs
|
# 🪵 Configuração de Logs
|
||||||
# ===========================
|
# ===========================
|
||||||
|
|
||||||
LOG_DIR = Path(__file__).resolve().parents[3] / "logs" / "location"
|
LOG_DIR = Path(__file__).resolve().parents[3] / "logs"
|
||||||
ERROR_DIR = Path(__file__).resolve().parents[3] / "logs" / "error"
|
|
||||||
|
|
||||||
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
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"
|
# Cria um logger
|
||||||
error_file = ERROR_DIR / f"location_error_{datetime.now().strftime('%Y-%m-%d')}.log"
|
logger = logging.getLogger("LocationMappingLogger")
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
logging.basicConfig(
|
# Evita que os logs se propaguem para o logger raiz
|
||||||
level=logging.INFO,
|
logger.propagate = False
|
||||||
format="%(asctime)s - %(levelname)s - %(message)s",
|
|
||||||
handlers=[
|
# Formato do log
|
||||||
logging.FileHandler(log_file, encoding="utf-8"),
|
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
logging.StreamHandler(),
|
|
||||||
logging.FileHandler(error_file, encoding="utf-8")
|
# 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
|
# 💾 Configurações dos Bancos
|
||||||
@ -74,15 +92,15 @@ CSV_PATH = os.getenv('LOCATION_MAPPING_CSV_PATH')
|
|||||||
def csv_recently_modified(minutes=2):
|
def csv_recently_modified(minutes=2):
|
||||||
"""Verifica se o CSV foi alterado recentemente."""
|
"""Verifica se o CSV foi alterado recentemente."""
|
||||||
if not os.path.exists(CSV_PATH):
|
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
|
return False
|
||||||
|
|
||||||
last_mod_time = datetime.fromtimestamp(os.path.getmtime(CSV_PATH))
|
last_mod_time = datetime.fromtimestamp(os.path.getmtime(CSV_PATH))
|
||||||
if datetime.now() - last_mod_time <= timedelta(minutes=minutes):
|
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
|
return True
|
||||||
else:
|
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
|
return False
|
||||||
|
|
||||||
|
|
||||||
@ -90,7 +108,7 @@ def get_snglpi_connection():
|
|||||||
try:
|
try:
|
||||||
return psycopg2.connect(**SNGLPI_DB_CONFIG)
|
return psycopg2.connect(**SNGLPI_DB_CONFIG)
|
||||||
except Exception as e:
|
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
|
return None
|
||||||
|
|
||||||
|
|
||||||
@ -98,7 +116,7 @@ def get_glpi_connection():
|
|||||||
try:
|
try:
|
||||||
return mysql.connector.connect(**GLPI_DB_CONFIG)
|
return mysql.connector.connect(**GLPI_DB_CONFIG)
|
||||||
except Error as e:
|
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
|
return None
|
||||||
|
|
||||||
|
|
||||||
@ -114,7 +132,7 @@ def find_sn_location_id(location_name, cursor):
|
|||||||
result = cursor.fetchone()
|
result = cursor.fetchone()
|
||||||
return result[0] if result else None
|
return result[0] if result else None
|
||||||
except Exception as e:
|
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
|
return None
|
||||||
|
|
||||||
|
|
||||||
@ -130,7 +148,7 @@ def find_glpi_entity_id(entity_name, cursor):
|
|||||||
result = cursor.fetchone()
|
result = cursor.fetchone()
|
||||||
return result[0] if result else None
|
return result[0] if result else None
|
||||||
except Exception as e:
|
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
|
return None
|
||||||
|
|
||||||
|
|
||||||
@ -138,59 +156,48 @@ def update_mapping_table():
|
|||||||
"""Atualiza a tabela de mapeamento a partir do CSV."""
|
"""Atualiza a tabela de mapeamento a partir do CSV."""
|
||||||
try:
|
try:
|
||||||
if not os.path.exists(CSV_PATH):
|
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
|
return False
|
||||||
|
|
||||||
snglpi_conn = get_snglpi_connection()
|
with get_snglpi_connection() as snglpi_conn, get_glpi_connection() as glpi_conn:
|
||||||
glpi_conn = get_glpi_connection()
|
if not snglpi_conn or not glpi_conn:
|
||||||
|
return False
|
||||||
|
|
||||||
if not snglpi_conn or not glpi_conn:
|
with snglpi_conn.cursor() as snglpi_cur, glpi_conn.cursor() as glpi_cur:
|
||||||
return False
|
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()
|
mappings = []
|
||||||
glpi_cur = glpi_conn.cursor()
|
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=';')
|
sn_id = find_sn_location_id(sn_name, snglpi_cur)
|
||||||
df['sn_location_id'] = None
|
glpi_id = find_glpi_entity_id(glpi_name, glpi_cur)
|
||||||
df['glpi_entity_id'] = None
|
|
||||||
|
|
||||||
mappings = []
|
if glpi_id:
|
||||||
for _, row in df.iterrows():
|
mappings.append((sn_id, sn_name, glpi_id, glpi_name))
|
||||||
sn_name = row['sn_location_name']
|
else:
|
||||||
glpi_name = row['glpi_entity_name']
|
logger.warning(f"Entity não encontrada: {glpi_name}")
|
||||||
|
|
||||||
sn_id = find_sn_location_id(sn_name, snglpi_cur)
|
if mappings:
|
||||||
glpi_id = find_glpi_entity_id(glpi_name, glpi_cur)
|
snglpi_cur.execute("DELETE FROM location_mapping")
|
||||||
|
insert_query = """
|
||||||
if glpi_id:
|
INSERT INTO location_mapping
|
||||||
mappings.append((sn_id, sn_name, glpi_id, glpi_name))
|
(sn_location_id, sn_location_name, glpi_entity_id, glpi_entity_name)
|
||||||
else:
|
VALUES (%s, %s, %s, %s)
|
||||||
logging.warning(f"Entity não encontrada: {glpi_name}")
|
"""
|
||||||
|
snglpi_cur.executemany(insert_query, mappings)
|
||||||
if mappings:
|
snglpi_conn.commit()
|
||||||
snglpi_cur.execute("DELETE FROM location_mapping")
|
logger.info(f"Tabela atualizada com {len(mappings)} registros.")
|
||||||
insert_query = """
|
else:
|
||||||
INSERT INTO location_mapping
|
logger.warning("Nenhum mapeamento válido encontrado.")
|
||||||
(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()
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Erro no processo de atualização: {e}")
|
logger.error(f"Erro no processo de atualização: {e}", exc_info=True)
|
||||||
if 'snglpi_conn' in locals():
|
|
||||||
snglpi_conn.rollback()
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@ -199,16 +206,16 @@ def update_mapping_table():
|
|||||||
# ===========================
|
# ===========================
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
logging.info("🚀 Iniciando monitoramento contínuo do Location Mapping")
|
logger.info("🚀 Iniciando monitoramento contínuo do Location Mapping")
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
if csv_recently_modified(minutes=2):
|
if csv_recently_modified(minutes=2):
|
||||||
success = update_mapping_table()
|
success = update_mapping_table()
|
||||||
if success:
|
if success:
|
||||||
logging.info("✅ Atualização concluída com sucesso.")
|
logger.info("✅ Atualização concluída com sucesso.")
|
||||||
else:
|
else:
|
||||||
logging.error("❌ Falha na atualização.")
|
logger.error("❌ Falha na atualização.")
|
||||||
time.sleep(60)
|
time.sleep(60)
|
||||||
except Exception as e:
|
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)
|
time.sleep(60)
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
const winston = require('winston');
|
const winston = require('winston');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
require('winston-daily-rotate-file');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
|
||||||
// verifica se a pasta de logs existe, se não, cria
|
// 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
|
// Configuração do logger com winston
|
||||||
const logger = winston.createLogger({
|
const logger = winston.createLogger({
|
||||||
level: 'info',
|
level: 'info',
|
||||||
format: winston.format.combine(
|
format: winston.format.combine(winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), winston.format.errors({ stack: true })),
|
||||||
winston.format.timestamp({
|
|
||||||
format: 'YYYY-MM-DD HH:mm:ss'
|
|
||||||
}),
|
|
||||||
winston.format.errors({ stack: true }), // ← Mostra stack trace de erros
|
|
||||||
winston.format.json()
|
|
||||||
),
|
|
||||||
transports: [
|
transports: [
|
||||||
// Log geral da aplicação
|
// Log geral da aplicação
|
||||||
new winston.transports.File({
|
new winston.transports.DailyRotateFile({
|
||||||
filename: path.join(logsDir, 'app.log'),
|
filename: path.join(logsDir, 'app-%DATE%.log'),
|
||||||
maxsize: 5242880, // 5MB
|
datePattern: 'YYYY-MM-DD',
|
||||||
maxFiles: 5
|
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
|
// Log de erros
|
||||||
new winston.transports.File({
|
new winston.transports.DailyRotateFile({
|
||||||
filename: path.join(logsDir, 'error.log'),
|
filename: path.join(logsDir, 'error-%DATE%.log'),
|
||||||
level: 'error',
|
level: 'error',
|
||||||
maxsize: 5242880,
|
datePattern: 'YYYY-MM-DD',
|
||||||
maxFiles: 3
|
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({
|
logger.add(new winston.transports.Console({
|
||||||
format: winston.format.combine(
|
format: winston.format.combine(
|
||||||
winston.format.colorize(),
|
winston.format.colorize(),
|
||||||
winston.format.printf(({ timestamp, level, message, stack }) => {
|
winston.format.printf((info) => {
|
||||||
return `${timestamp} [${level}]: ${stack || message}`;
|
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,
|
logError,
|
||||||
logInfo,
|
logInfo,
|
||||||
logWarning,
|
logWarning,
|
||||||
logSync
|
logSync,
|
||||||
};
|
};
|
||||||
Loading…
Reference in New Issue
Block a user