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
|
||||
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
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
|
||||
|
||||
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 { 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
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",
|
||||
"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"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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]);
|
||||
|
||||
@ -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 {
|
||||
|
||||
|
||||
@ -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 = [
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
pandas
|
||||
psycopg2-binary
|
||||
mysql-connector-python
|
||||
python-dotenv
|
||||
pandas
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user