mirror of
https://github.com/ptarmiganlabs/butler-sos.git
synced 2025-12-19 17:58:18 -05:00
refactor: Split InfluxDB v1/v2/v3 code into smaller, more manageable pieces
This commit is contained in:
@@ -510,6 +510,9 @@ Butler-SOS:
|
||||
# Influx db config parameters
|
||||
influxdbConfig:
|
||||
enable: true
|
||||
# Feature flag to enable refactored InfluxDB code (recommended for better maintainability)
|
||||
# Set to true to use the new modular implementation, false for legacy code
|
||||
useRefactoredCode: false
|
||||
# Items below are mandatory if influxdbConfig.enable=true
|
||||
host: influxdb.mycompany.com # InfluxDB host, hostname, FQDN or IP address
|
||||
port: 8086 # Port where InfluxDBdb is listening, usually 8086
|
||||
|
||||
@@ -88,7 +88,7 @@ jest.unstable_mockModule('../../globals.js', () => ({
|
||||
|
||||
// Mock dependent modules
|
||||
const mockPostProxySessionsToInfluxdb = jest.fn().mockResolvedValue();
|
||||
jest.unstable_mockModule('../post-to-influxdb.js', () => ({
|
||||
jest.unstable_mockModule('../influxdb/index.js', () => ({
|
||||
postProxySessionsToInfluxdb: mockPostProxySessionsToInfluxdb,
|
||||
}));
|
||||
|
||||
|
||||
@@ -310,6 +310,7 @@ export const destinationsSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
enable: { type: 'boolean' },
|
||||
useRefactoredCode: { type: 'boolean' },
|
||||
host: {
|
||||
type: 'string',
|
||||
format: 'hostname',
|
||||
|
||||
88
src/lib/influxdb/README.md
Normal file
88
src/lib/influxdb/README.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# InfluxDB Module Refactoring
|
||||
|
||||
This directory contains the refactored InfluxDB integration code, organized by version for better maintainability and testability.
|
||||
|
||||
## Structure
|
||||
|
||||
```text
|
||||
influxdb/
|
||||
├── shared/ # Shared utilities and helpers
|
||||
│ └── utils.js # Common functions used across all versions
|
||||
├── v1/ # InfluxDB 1.x implementations
|
||||
├── v2/ # InfluxDB 2.x implementations
|
||||
├── v3/ # InfluxDB 3.x implementations
|
||||
│ └── health-metrics.js # Health metrics for v3
|
||||
├── factory.js # Version router that delegates to appropriate implementation
|
||||
└── index.js # Main facade providing backward compatibility
|
||||
```
|
||||
|
||||
## Feature Flag
|
||||
|
||||
The refactored code is controlled by the `Butler-SOS.influxdbConfig.useRefactoredCode` configuration flag:
|
||||
|
||||
```yaml
|
||||
Butler-SOS:
|
||||
influxdbConfig:
|
||||
enable: true
|
||||
useRefactoredCode: false # Set to true to use refactored code
|
||||
version: 3
|
||||
# ... other config
|
||||
```
|
||||
|
||||
**Default:** `false` (uses original code for backward compatibility)
|
||||
|
||||
## Migration Status
|
||||
|
||||
### Completed
|
||||
|
||||
- ✅ Directory structure
|
||||
- ✅ Shared utilities (`getFormattedTime`, `processAppDocuments`, etc.)
|
||||
- ✅ V3 health metrics implementation
|
||||
- ✅ Factory router with feature flag
|
||||
- ✅ Backward-compatible facade
|
||||
- ✅ Configuration schema updated
|
||||
|
||||
### In Progress
|
||||
|
||||
- 🚧 V3 remaining modules (sessions, log events, user events, queue metrics)
|
||||
- 🚧 V2 implementations
|
||||
- 🚧 V1 implementations
|
||||
|
||||
### Pending
|
||||
|
||||
- ⏳ Complete test coverage for all modules
|
||||
- ⏳ Integration tests
|
||||
- ⏳ Performance benchmarking
|
||||
|
||||
## Usage
|
||||
|
||||
### For Developers
|
||||
|
||||
When the feature flag is enabled, the facade in `index.js` will route calls to the refactored implementations. If a version-specific implementation is not yet complete, it automatically falls back to the original code.
|
||||
|
||||
```javascript
|
||||
// Imports work the same way
|
||||
import { postHealthMetricsToInfluxdb } from './lib/influxdb/index.js';
|
||||
|
||||
// Function automatically routes based on feature flag
|
||||
await postHealthMetricsToInfluxdb(serverName, host, body, serverTags);
|
||||
```
|
||||
|
||||
### Adding New Implementations
|
||||
|
||||
1. Create the version-specific module (e.g., `v3/sessions.js`)
|
||||
2. Import and export it in `factory.js`
|
||||
3. Update the facade in `index.js` to use the factory
|
||||
4. Add tests in the appropriate `__tests__` directory
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Maintainability**: Smaller, focused files instead of one 3000+ line file
|
||||
2. **Testability**: Each module can be tested in isolation
|
||||
3. **Code Reuse**: Shared utilities reduce duplication
|
||||
4. **Version Management**: Easy to deprecate old versions when needed
|
||||
5. **Safe Migration**: Feature flag allows gradual rollout
|
||||
|
||||
## Original Implementation
|
||||
|
||||
The original implementation remains in `/src/lib/post-to-influxdb.js` and continues to work as before. This ensures no breaking changes during migration.
|
||||
239
src/lib/influxdb/factory.js
Normal file
239
src/lib/influxdb/factory.js
Normal file
@@ -0,0 +1,239 @@
|
||||
import globals from '../../globals.js';
|
||||
import { getInfluxDbVersion, useRefactoredInfluxDb } from './shared/utils.js';
|
||||
|
||||
// Import version-specific implementations
|
||||
import { storeHealthMetricsV1 } from './v1/health-metrics.js';
|
||||
import { storeSessionsV1 } from './v1/sessions.js';
|
||||
import { storeButlerMemoryV1 } from './v1/butler-memory.js';
|
||||
import { storeUserEventV1 } from './v1/user-events.js';
|
||||
import { storeEventCountV1, storeRejectedEventCountV1 } from './v1/event-counts.js';
|
||||
import { storeUserEventQueueMetricsV1, storeLogEventQueueMetricsV1 } from './v1/queue-metrics.js';
|
||||
import { storeLogEventV1 } from './v1/log-events.js';
|
||||
|
||||
import { storeHealthMetricsV2 } from './v2/health-metrics.js';
|
||||
import { storeSessionsV2 } from './v2/sessions.js';
|
||||
import { storeButlerMemoryV2 } from './v2/butler-memory.js';
|
||||
import { storeUserEventV2 } from './v2/user-events.js';
|
||||
import { storeEventCountV2, storeRejectedEventCountV2 } from './v2/event-counts.js';
|
||||
import { storeUserEventQueueMetricsV2, storeLogEventQueueMetricsV2 } from './v2/queue-metrics.js';
|
||||
import { storeLogEventV2 } from './v2/log-events.js';
|
||||
|
||||
import { postHealthMetricsToInfluxdbV3 } from './v3/health-metrics.js';
|
||||
import { postProxySessionsToInfluxdbV3 } from './v3/sessions.js';
|
||||
import { postButlerSOSMemoryUsageToInfluxdbV3 } from './v3/butler-memory.js';
|
||||
import { postUserEventToInfluxdbV3 } from './v3/user-events.js';
|
||||
import { storeEventCountInfluxDBV3, storeRejectedEventCountInfluxDBV3 } from './v3/event-counts.js';
|
||||
import {
|
||||
postUserEventQueueMetricsToInfluxdbV3,
|
||||
postLogEventQueueMetricsToInfluxdbV3,
|
||||
} from './v3/queue-metrics.js';
|
||||
import { postLogEventToInfluxdbV3 } from './v3/log-events.js';
|
||||
|
||||
/**
|
||||
* Factory function that routes health metrics to the appropriate InfluxDB version implementation.
|
||||
*
|
||||
* @param {string} serverName - The name of the Qlik Sense server
|
||||
* @param {string} host - The hostname or IP of the Qlik Sense server
|
||||
* @param {object} body - The health metrics data from Sense engine healthcheck API
|
||||
* @param {object} serverTags - Tags to associate with the metrics
|
||||
* @returns {Promise<void>} Promise that resolves when data has been posted to InfluxDB
|
||||
*/
|
||||
export async function postHealthMetricsToInfluxdb(serverName, host, body, serverTags) {
|
||||
const version = getInfluxDbVersion();
|
||||
|
||||
if (version === 1) {
|
||||
return storeHealthMetricsV1(serverTags, body);
|
||||
}
|
||||
if (version === 2) {
|
||||
return storeHealthMetricsV2(serverName, host, body);
|
||||
}
|
||||
if (version === 3) {
|
||||
return postHealthMetricsToInfluxdbV3(serverName, host, body, serverTags);
|
||||
}
|
||||
|
||||
globals.logger.debug(`INFLUXDB FACTORY: Unknown InfluxDB version: v${version}`);
|
||||
throw new Error(`InfluxDB v${version} not supported`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory function that routes proxy sessions to the appropriate InfluxDB version implementation.
|
||||
*
|
||||
* @param {object} userSessions - User session data
|
||||
* @returns {Promise<void>} Promise that resolves when data has been posted to InfluxDB
|
||||
*/
|
||||
export async function postProxySessionsToInfluxdb(userSessions) {
|
||||
const version = getInfluxDbVersion();
|
||||
|
||||
if (version === 1) {
|
||||
return storeSessionsV1(userSessions);
|
||||
}
|
||||
if (version === 2) {
|
||||
return storeSessionsV2(userSessions);
|
||||
}
|
||||
if (version === 3) {
|
||||
return postProxySessionsToInfluxdbV3(userSessions);
|
||||
}
|
||||
|
||||
globals.logger.debug(`INFLUXDB FACTORY: Unknown InfluxDB version: v${version}`);
|
||||
throw new Error(`InfluxDB v${version} not supported`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory function that routes Butler SOS memory usage to the appropriate InfluxDB version implementation.
|
||||
*
|
||||
* @param {object} memory - Memory usage data object
|
||||
* @returns {Promise<void>} Promise that resolves when data has been posted to InfluxDB
|
||||
*/
|
||||
export async function postButlerSOSMemoryUsageToInfluxdb(memory) {
|
||||
const version = getInfluxDbVersion();
|
||||
|
||||
if (version === 1) {
|
||||
return storeButlerMemoryV1(memory);
|
||||
}
|
||||
if (version === 2) {
|
||||
return storeButlerMemoryV2(memory);
|
||||
}
|
||||
if (version === 3) {
|
||||
return postButlerSOSMemoryUsageToInfluxdbV3(memory);
|
||||
}
|
||||
|
||||
globals.logger.debug(`INFLUXDB FACTORY: Unknown InfluxDB version: v${version}`);
|
||||
throw new Error(`InfluxDB v${version} not supported`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory function that routes user events to the appropriate InfluxDB version implementation.
|
||||
*
|
||||
* @param {object} msg - The user event message
|
||||
* @returns {Promise<void>} Promise that resolves when data has been posted to InfluxDB
|
||||
*/
|
||||
export async function postUserEventToInfluxdb(msg) {
|
||||
const version = getInfluxDbVersion();
|
||||
|
||||
if (version === 1) {
|
||||
return storeUserEventV1(msg);
|
||||
}
|
||||
if (version === 2) {
|
||||
return storeUserEventV2(msg);
|
||||
}
|
||||
if (version === 3) {
|
||||
return postUserEventToInfluxdbV3(msg);
|
||||
}
|
||||
|
||||
globals.logger.debug(`INFLUXDB FACTORY: Unknown InfluxDB version: v${version}`);
|
||||
throw new Error(`InfluxDB v${version} not supported`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory function that routes event count storage to the appropriate InfluxDB version implementation.
|
||||
*
|
||||
* @returns {Promise<void>} Promise that resolves when data has been posted to InfluxDB
|
||||
*/
|
||||
export async function storeEventCountInfluxDB() {
|
||||
const version = getInfluxDbVersion();
|
||||
|
||||
if (version === 1) {
|
||||
return storeEventCountV1();
|
||||
}
|
||||
if (version === 2) {
|
||||
return storeEventCountV2();
|
||||
}
|
||||
if (version === 3) {
|
||||
return storeEventCountInfluxDBV3();
|
||||
}
|
||||
|
||||
globals.logger.debug(`INFLUXDB FACTORY: Unknown InfluxDB version: v${version}`);
|
||||
throw new Error(`InfluxDB v${version} not supported`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory function that routes rejected event count storage to the appropriate InfluxDB version implementation.
|
||||
*
|
||||
* @returns {Promise<void>} Promise that resolves when data has been posted to InfluxDB
|
||||
*/
|
||||
export async function storeRejectedEventCountInfluxDB() {
|
||||
const version = getInfluxDbVersion();
|
||||
|
||||
if (version === 1) {
|
||||
return storeRejectedEventCountV1();
|
||||
}
|
||||
if (version === 2) {
|
||||
return storeRejectedEventCountV2();
|
||||
}
|
||||
if (version === 3) {
|
||||
return storeRejectedEventCountInfluxDBV3();
|
||||
}
|
||||
|
||||
globals.logger.debug(`INFLUXDB FACTORY: Unknown InfluxDB version: v${version}`);
|
||||
throw new Error(`InfluxDB v${version} not supported`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory function that routes user event queue metrics to the appropriate InfluxDB version implementation.
|
||||
*
|
||||
* @returns {Promise<void>} Promise that resolves when data has been posted to InfluxDB
|
||||
*/
|
||||
export async function postUserEventQueueMetricsToInfluxdb() {
|
||||
const version = getInfluxDbVersion();
|
||||
|
||||
if (version === 1) {
|
||||
return storeUserEventQueueMetricsV1();
|
||||
}
|
||||
if (version === 2) {
|
||||
return storeUserEventQueueMetricsV2();
|
||||
}
|
||||
if (version === 3) {
|
||||
return postUserEventQueueMetricsToInfluxdbV3();
|
||||
}
|
||||
|
||||
globals.logger.debug(`INFLUXDB FACTORY: Unknown InfluxDB version: v${version}`);
|
||||
throw new Error(`InfluxDB v${version} not supported`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory function that routes log event queue metrics to the appropriate InfluxDB version implementation.
|
||||
*
|
||||
* @returns {Promise<void>} Promise that resolves when data has been posted to InfluxDB
|
||||
*/
|
||||
export async function postLogEventQueueMetricsToInfluxdb() {
|
||||
const version = getInfluxDbVersion();
|
||||
|
||||
if (version === 1) {
|
||||
return storeLogEventQueueMetricsV1();
|
||||
}
|
||||
if (version === 2) {
|
||||
return storeLogEventQueueMetricsV2();
|
||||
}
|
||||
if (version === 3) {
|
||||
return postLogEventQueueMetricsToInfluxdbV3();
|
||||
}
|
||||
|
||||
globals.logger.debug(`INFLUXDB FACTORY: Unknown InfluxDB version: v${version}`);
|
||||
throw new Error(`InfluxDB v${version} not supported`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory function that routes log events to the appropriate InfluxDB version implementation.
|
||||
*
|
||||
* @param {object} msg - The log event message
|
||||
* @returns {Promise<void>} Promise that resolves when data has been posted to InfluxDB
|
||||
*/
|
||||
export async function postLogEventToInfluxdb(msg) {
|
||||
const version = getInfluxDbVersion();
|
||||
|
||||
if (version === 1) {
|
||||
return storeLogEventV1(msg);
|
||||
}
|
||||
if (version === 2) {
|
||||
return storeLogEventV2(msg);
|
||||
}
|
||||
if (version === 3) {
|
||||
return postLogEventToInfluxdbV3(msg);
|
||||
}
|
||||
|
||||
globals.logger.debug(`INFLUXDB FACTORY: Unknown InfluxDB version: v${version}`);
|
||||
throw new Error(`InfluxDB v${version} not supported`);
|
||||
}
|
||||
|
||||
// TODO: Add other factory functions as they're implemented
|
||||
// etc...
|
||||
218
src/lib/influxdb/index.js
Normal file
218
src/lib/influxdb/index.js
Normal file
@@ -0,0 +1,218 @@
|
||||
import { useRefactoredInfluxDb, getFormattedTime } from './shared/utils.js';
|
||||
import * as factory from './factory.js';
|
||||
|
||||
// Import original implementation for fallback
|
||||
import * as original from '../post-to-influxdb.js';
|
||||
|
||||
/**
|
||||
* Main facade that routes to either refactored or original implementation based on feature flag.
|
||||
*
|
||||
* This allows for safe migration by testing refactored code alongside original implementation.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Calculates and formats the uptime of a Qlik Sense engine.
|
||||
* This function is version-agnostic and always uses the shared implementation.
|
||||
*
|
||||
* @param {string} serverStarted - The server start time in format "YYYYMMDDThhmmss"
|
||||
* @returns {string} A formatted string representing uptime (e.g. "5 days, 3h 45m 12s")
|
||||
*/
|
||||
export { getFormattedTime };
|
||||
|
||||
/**
|
||||
* Posts health metrics data from Qlik Sense to InfluxDB.
|
||||
*
|
||||
* Routes to refactored or original implementation based on feature flag.
|
||||
*
|
||||
* @param {string} serverName - The name of the Qlik Sense server
|
||||
* @param {string} host - The hostname or IP of the Qlik Sense server
|
||||
* @param {object} body - The health metrics data from Sense engine healthcheck API
|
||||
* @param {object} serverTags - Tags to associate with the metrics
|
||||
* @returns {Promise<void>} Promise that resolves when data has been posted to InfluxDB
|
||||
*/
|
||||
export async function postHealthMetricsToInfluxdb(serverName, host, body, serverTags) {
|
||||
if (useRefactoredInfluxDb()) {
|
||||
try {
|
||||
return await factory.postHealthMetricsToInfluxdb(serverName, host, body, serverTags);
|
||||
} catch (err) {
|
||||
// If refactored code not yet implemented for this version, fall back to original
|
||||
return await original.postHealthMetricsToInfluxdb(serverName, host, body, serverTags);
|
||||
}
|
||||
}
|
||||
return await original.postHealthMetricsToInfluxdb(serverName, host, body, serverTags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Posts proxy sessions data to InfluxDB.
|
||||
*
|
||||
* Routes to refactored or original implementation based on feature flag.
|
||||
*
|
||||
* @param {object} userSessions - User session data
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function postProxySessionsToInfluxdb(userSessions) {
|
||||
if (useRefactoredInfluxDb()) {
|
||||
try {
|
||||
return await factory.postProxySessionsToInfluxdb(userSessions);
|
||||
} catch (err) {
|
||||
// If refactored code not yet implemented for this version, fall back to original
|
||||
return await original.postProxySessionsToInfluxdb(userSessions);
|
||||
}
|
||||
}
|
||||
return await original.postProxySessionsToInfluxdb(userSessions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Posts Butler SOS's own memory usage to InfluxDB.
|
||||
*
|
||||
* Routes to refactored or original implementation based on feature flag.
|
||||
*
|
||||
* @param {object} memory - Memory usage data object
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function postButlerSOSMemoryUsageToInfluxdb(memory) {
|
||||
if (useRefactoredInfluxDb()) {
|
||||
try {
|
||||
return await factory.postButlerSOSMemoryUsageToInfluxdb(memory);
|
||||
} catch (err) {
|
||||
// If refactored code not yet implemented for this version, fall back to original
|
||||
return await original.postButlerSOSMemoryUsageToInfluxdb(memory);
|
||||
}
|
||||
}
|
||||
return await original.postButlerSOSMemoryUsageToInfluxdb(memory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Posts user events to InfluxDB.
|
||||
*
|
||||
* Routes to refactored or original implementation based on feature flag.
|
||||
*
|
||||
* @param {object} msg - The user event message
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function postUserEventToInfluxdb(msg) {
|
||||
if (useRefactoredInfluxDb()) {
|
||||
try {
|
||||
return await factory.postUserEventToInfluxdb(msg);
|
||||
} catch (err) {
|
||||
// If refactored code not yet implemented for this version, fall back to original
|
||||
return await original.postUserEventToInfluxdb(msg);
|
||||
}
|
||||
}
|
||||
return await original.postUserEventToInfluxdb(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Posts log events to InfluxDB.
|
||||
*
|
||||
* Routes to refactored or original implementation based on feature flag.
|
||||
*
|
||||
* @param {object} msg - The log event message
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function postLogEventToInfluxdb(msg) {
|
||||
if (useRefactoredInfluxDb()) {
|
||||
try {
|
||||
return await factory.postLogEventToInfluxdb(msg);
|
||||
} catch (err) {
|
||||
// If refactored code not yet implemented for this version, fall back to original
|
||||
return await original.postLogEventToInfluxdb(msg);
|
||||
}
|
||||
}
|
||||
return await original.postLogEventToInfluxdb(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores event counts to InfluxDB.
|
||||
*
|
||||
* Routes to refactored or original implementation based on feature flag.
|
||||
*
|
||||
* @param {string} eventsSinceMidnight - Events since midnight data
|
||||
* @param {string} eventsLastHour - Events last hour data
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function storeEventCountInfluxDB(eventsSinceMidnight, eventsLastHour) {
|
||||
if (useRefactoredInfluxDb()) {
|
||||
try {
|
||||
return await factory.storeEventCountInfluxDB();
|
||||
} catch (err) {
|
||||
// If refactored code not yet implemented for this version, fall back to original
|
||||
return await original.storeEventCountInfluxDB(eventsSinceMidnight, eventsLastHour);
|
||||
}
|
||||
}
|
||||
return await original.storeEventCountInfluxDB(eventsSinceMidnight, eventsLastHour);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores rejected event counts to InfluxDB.
|
||||
*
|
||||
* Routes to refactored or original implementation based on feature flag.
|
||||
*
|
||||
* @param {object} rejectedSinceMidnight - Rejected events since midnight
|
||||
* @param {object} rejectedLastHour - Rejected events last hour
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function storeRejectedEventCountInfluxDB(rejectedSinceMidnight, rejectedLastHour) {
|
||||
if (useRefactoredInfluxDb()) {
|
||||
try {
|
||||
return await factory.storeRejectedEventCountInfluxDB();
|
||||
} catch (err) {
|
||||
// If refactored code not yet implemented for this version, fall back to original
|
||||
return await original.storeRejectedEventCountInfluxDB(
|
||||
rejectedSinceMidnight,
|
||||
rejectedLastHour
|
||||
);
|
||||
}
|
||||
}
|
||||
return await original.storeRejectedEventCountInfluxDB(rejectedSinceMidnight, rejectedLastHour);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores user event queue metrics to InfluxDB.
|
||||
*
|
||||
* Routes to refactored or original implementation based on feature flag.
|
||||
*
|
||||
* @param {object} queueMetrics - Queue metrics data
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function postUserEventQueueMetricsToInfluxdb(queueMetrics) {
|
||||
if (useRefactoredInfluxDb()) {
|
||||
try {
|
||||
return await factory.postUserEventQueueMetricsToInfluxdb();
|
||||
} catch (err) {
|
||||
// If refactored code not yet implemented for this version, fall back to original
|
||||
return await original.postUserEventQueueMetricsToInfluxdb(queueMetrics);
|
||||
}
|
||||
}
|
||||
return await original.postUserEventQueueMetricsToInfluxdb(queueMetrics);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores log event queue metrics to InfluxDB.
|
||||
*
|
||||
* Routes to refactored or original implementation based on feature flag.
|
||||
*
|
||||
* @param {object} queueMetrics - Queue metrics data
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function postLogEventQueueMetricsToInfluxdb(queueMetrics) {
|
||||
if (useRefactoredInfluxDb()) {
|
||||
try {
|
||||
return await factory.postLogEventQueueMetricsToInfluxdb();
|
||||
} catch (err) {
|
||||
// If refactored code not yet implemented for this version, fall back to original
|
||||
return await original.postLogEventQueueMetricsToInfluxdb(queueMetrics);
|
||||
}
|
||||
}
|
||||
return await original.postLogEventQueueMetricsToInfluxdb(queueMetrics);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up timers for queue metrics storage.
|
||||
*
|
||||
* @returns {object} Object containing interval IDs for cleanup
|
||||
*/
|
||||
export function setupUdpQueueMetricsStorage() {
|
||||
// This is version-agnostic, always use original
|
||||
return original.setupUdpQueueMetricsStorage();
|
||||
}
|
||||
191
src/lib/influxdb/shared/utils.js
Normal file
191
src/lib/influxdb/shared/utils.js
Normal file
@@ -0,0 +1,191 @@
|
||||
import globals from '../../../globals.js';
|
||||
|
||||
const sessionAppPrefix = 'SessionApp';
|
||||
const MIN_TIMESTAMP_LENGTH = 15;
|
||||
|
||||
/**
|
||||
* Calculates and formats the uptime of a Qlik Sense engine.
|
||||
*
|
||||
* This function takes the server start time from the engine healthcheck API
|
||||
* and calculates how long the server has been running, returning a formatted string.
|
||||
*
|
||||
* @param {string} serverStarted - The server start time in format "YYYYMMDDThhmmss"
|
||||
* @returns {string} A formatted string representing uptime (e.g. "5 days, 3h 45m 12s")
|
||||
*/
|
||||
export function getFormattedTime(serverStarted) {
|
||||
// Handle invalid or empty input
|
||||
if (
|
||||
!serverStarted ||
|
||||
typeof serverStarted !== 'string' ||
|
||||
serverStarted.length < MIN_TIMESTAMP_LENGTH
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const dateTime = Date.now();
|
||||
const timestamp = Math.floor(dateTime);
|
||||
|
||||
const str = serverStarted;
|
||||
const year = str.substring(0, 4);
|
||||
const month = str.substring(4, 6);
|
||||
const day = str.substring(6, 8);
|
||||
const hour = str.substring(9, 11);
|
||||
const minute = str.substring(11, 13);
|
||||
const second = str.substring(13, 15);
|
||||
|
||||
// Validate date components
|
||||
if (
|
||||
isNaN(year) ||
|
||||
isNaN(month) ||
|
||||
isNaN(day) ||
|
||||
isNaN(hour) ||
|
||||
isNaN(minute) ||
|
||||
isNaN(second)
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const dateTimeStarted = new Date(year, month - 1, day, hour, minute, second);
|
||||
|
||||
// Check if the date is valid
|
||||
if (isNaN(dateTimeStarted.getTime())) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const timestampStarted = Math.floor(dateTimeStarted);
|
||||
|
||||
const diff = timestamp - timestampStarted;
|
||||
|
||||
// Create a new JavaScript Date object based on the timestamp
|
||||
// multiplied by 1000 so that the argument is in milliseconds, not seconds.
|
||||
const date = new Date(diff);
|
||||
|
||||
const days = Math.trunc(diff / (1000 * 60 * 60 * 24));
|
||||
|
||||
// Hours part from the timestamp
|
||||
const hours = date.getHours();
|
||||
|
||||
// Minutes part from the timestamp
|
||||
const minutes = `0${date.getMinutes()}`;
|
||||
|
||||
// Seconds part from the timestamp
|
||||
const seconds = `0${date.getSeconds()}`;
|
||||
|
||||
// Will display time in 10:30:23 format
|
||||
return `${days} days, ${hours}h ${minutes.substr(-2)}m ${seconds.substr(-2)}s`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes app documents and categorizes them as session apps or regular apps.
|
||||
* Returns arrays of app names for both categories.
|
||||
*
|
||||
* @param {string[]} docIDs - Array of document IDs to process
|
||||
* @param {string} logPrefix - Prefix for log messages
|
||||
* @param {string} appState - Description of app state (e.g., 'active', 'loaded', 'in memory')
|
||||
* @returns {Promise<{appNames: string[], sessionAppNames: string[]}>} Object containing sorted arrays of app names
|
||||
*/
|
||||
export async function processAppDocuments(docIDs, logPrefix, appState) {
|
||||
const appNames = [];
|
||||
const sessionAppNames = [];
|
||||
|
||||
/**
|
||||
* Stores a document ID in the appropriate array based on its type.
|
||||
*
|
||||
* @param {string} docID - The document ID to store
|
||||
* @returns {Promise<void>} Promise that resolves when the document ID has been processed
|
||||
*/
|
||||
const storeDoc = (docID) => {
|
||||
return new Promise((resolve, _reject) => {
|
||||
if (docID.substring(0, sessionAppPrefix.length) === sessionAppPrefix) {
|
||||
// Session app
|
||||
globals.logger.debug(`${logPrefix}: Session app is ${appState}: ${docID}`);
|
||||
sessionAppNames.push(docID);
|
||||
} else {
|
||||
// Not session app
|
||||
const app = globals.appNames.find((element) => element.id === docID);
|
||||
|
||||
if (app) {
|
||||
globals.logger.debug(`${logPrefix}: App is ${appState}: ${app.name}`);
|
||||
appNames.push(app.name);
|
||||
} else {
|
||||
appNames.push(docID);
|
||||
}
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
};
|
||||
|
||||
const promises = docIDs.map(
|
||||
(docID) =>
|
||||
new Promise(async (resolve, _reject) => {
|
||||
await storeDoc(docID);
|
||||
resolve();
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
appNames.sort();
|
||||
sessionAppNames.sort();
|
||||
|
||||
return { appNames, sessionAppNames };
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if InfluxDB is enabled and initialized.
|
||||
*
|
||||
* @returns {boolean} True if InfluxDB is enabled and initialized
|
||||
*/
|
||||
export function isInfluxDbEnabled() {
|
||||
if (!globals.influx) {
|
||||
globals.logger.warn(
|
||||
'INFLUXDB: Influxdb object not initialized. Data will not be sent to InfluxDB'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the InfluxDB version from configuration.
|
||||
*
|
||||
* @returns {number} The InfluxDB version (1, 2, or 3)
|
||||
*/
|
||||
export function getInfluxDbVersion() {
|
||||
return globals.config.get('Butler-SOS.influxdbConfig.version');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the refactored InfluxDB code path should be used.
|
||||
*
|
||||
* @returns {boolean} True if refactored code should be used
|
||||
*/
|
||||
export function useRefactoredInfluxDb() {
|
||||
// Feature flag to enable/disable refactored code path
|
||||
// Default to false for backward compatibility
|
||||
return globals.config.get('Butler-SOS.influxdbConfig.useRefactoredCode') === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies tags from a tags object to an InfluxDB Point3 object.
|
||||
* This is needed for v3 as it doesn't have automatic default tags like v2.
|
||||
*
|
||||
* @param {object} point - The Point3 object to apply tags to
|
||||
* @param {object} tags - Object containing tag key-value pairs
|
||||
* @returns {object} The Point3 object with tags applied (for chaining)
|
||||
*/
|
||||
export function applyTagsToPoint3(point, tags) {
|
||||
if (!tags || typeof tags !== 'object') {
|
||||
return point;
|
||||
}
|
||||
|
||||
// Apply each tag to the point
|
||||
Object.entries(tags).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null) {
|
||||
point.setTag(key, String(value));
|
||||
}
|
||||
});
|
||||
|
||||
return point;
|
||||
}
|
||||
46
src/lib/influxdb/v1/butler-memory.js
Normal file
46
src/lib/influxdb/v1/butler-memory.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import globals from '../../../globals.js';
|
||||
|
||||
/**
|
||||
* Store Butler SOS memory usage to InfluxDB v1
|
||||
*
|
||||
* @param {object} memory - Memory usage data
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function storeButlerMemoryV1(memory) {
|
||||
try {
|
||||
const butlerVersion = globals.appVersion;
|
||||
|
||||
const datapoint = [
|
||||
{
|
||||
measurement: 'butlersos_memory_usage',
|
||||
tags: {
|
||||
butler_sos_instance: memory.instanceTag,
|
||||
version: butlerVersion,
|
||||
},
|
||||
fields: {
|
||||
heap_used: memory.heapUsedMByte,
|
||||
heap_total: memory.heapTotalMByte,
|
||||
external: memory.externalMemoryMByte,
|
||||
process_memory: memory.processMemoryMByte,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
globals.logger.silly(
|
||||
`MEMORY USAGE V1: Influxdb datapoint for Butler SOS memory usage: ${JSON.stringify(
|
||||
datapoint,
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
);
|
||||
|
||||
await globals.influx.writePoints(datapoint);
|
||||
|
||||
globals.logger.verbose('MEMORY USAGE V1: Sent Butler SOS memory usage data to InfluxDB');
|
||||
} catch (err) {
|
||||
globals.logger.error(
|
||||
`MEMORY USAGE V1: Error saving Butler SOS memory data: ${globals.getErrorMessage(err)}`
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
215
src/lib/influxdb/v1/event-counts.js
Normal file
215
src/lib/influxdb/v1/event-counts.js
Normal file
@@ -0,0 +1,215 @@
|
||||
import globals from '../../../globals.js';
|
||||
|
||||
/**
|
||||
* Store event counts to InfluxDB v1
|
||||
* Aggregates and stores counts for log and user events
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function storeEventCountV1() {
|
||||
try {
|
||||
// Get array of log events
|
||||
const logEvents = await globals.udpEvents.getLogEvents();
|
||||
const userEvents = await globals.udpEvents.getUserEvents();
|
||||
|
||||
globals.logger.debug(`EVENT COUNT V1: Log events: ${JSON.stringify(logEvents, null, 2)}`);
|
||||
globals.logger.debug(`EVENT COUNT V1: User events: ${JSON.stringify(userEvents, null, 2)}`);
|
||||
|
||||
// Are there any events to store?
|
||||
if (logEvents.length === 0 && userEvents.length === 0) {
|
||||
globals.logger.verbose('EVENT COUNT V1: No events to store in InfluxDB');
|
||||
return;
|
||||
}
|
||||
|
||||
const points = [];
|
||||
|
||||
// Get measurement name to use for event counts
|
||||
const measurementName = globals.config.get(
|
||||
'Butler-SOS.qlikSenseEvents.eventCount.influxdb.measurementName'
|
||||
);
|
||||
|
||||
// Loop through data in log events and create datapoints
|
||||
for (const event of logEvents) {
|
||||
const point = {
|
||||
measurement: measurementName,
|
||||
tags: {
|
||||
event_type: 'log',
|
||||
source: event.source,
|
||||
host: event.host,
|
||||
subsystem: event.subsystem,
|
||||
},
|
||||
fields: {
|
||||
counter: event.counter,
|
||||
},
|
||||
};
|
||||
|
||||
// Add static tags from config file
|
||||
if (
|
||||
globals.config.has('Butler-SOS.qlikSenseEvents.eventCount.influxdb.tags') &&
|
||||
globals.config.get('Butler-SOS.qlikSenseEvents.eventCount.influxdb.tags') !==
|
||||
null &&
|
||||
globals.config.get('Butler-SOS.qlikSenseEvents.eventCount.influxdb.tags').length > 0
|
||||
) {
|
||||
const configTags = globals.config.get(
|
||||
'Butler-SOS.qlikSenseEvents.eventCount.influxdb.tags'
|
||||
);
|
||||
for (const item of configTags) {
|
||||
point.tags[item.name] = item.value;
|
||||
}
|
||||
}
|
||||
|
||||
points.push(point);
|
||||
}
|
||||
|
||||
// Loop through data in user events and create datapoints
|
||||
for (const event of userEvents) {
|
||||
const point = {
|
||||
measurement: measurementName,
|
||||
tags: {
|
||||
event_type: 'user',
|
||||
source: event.source,
|
||||
host: event.host,
|
||||
subsystem: event.subsystem,
|
||||
},
|
||||
fields: {
|
||||
counter: event.counter,
|
||||
},
|
||||
};
|
||||
|
||||
// Add static tags from config file
|
||||
if (
|
||||
globals.config.has('Butler-SOS.qlikSenseEvents.eventCount.influxdb.tags') &&
|
||||
globals.config.get('Butler-SOS.qlikSenseEvents.eventCount.influxdb.tags') !==
|
||||
null &&
|
||||
globals.config.get('Butler-SOS.qlikSenseEvents.eventCount.influxdb.tags').length > 0
|
||||
) {
|
||||
const configTags = globals.config.get(
|
||||
'Butler-SOS.qlikSenseEvents.eventCount.influxdb.tags'
|
||||
);
|
||||
for (const item of configTags) {
|
||||
point.tags[item.name] = item.value;
|
||||
}
|
||||
}
|
||||
|
||||
points.push(point);
|
||||
}
|
||||
|
||||
await globals.influx.writePoints(points);
|
||||
|
||||
globals.logger.verbose('EVENT COUNT V1: Sent event count data to InfluxDB');
|
||||
} catch (err) {
|
||||
globals.logger.error(`EVENT COUNT V1: Error saving data: ${err}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store rejected event counts to InfluxDB v1
|
||||
* Tracks events that were rejected due to validation failures or rate limiting
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function storeRejectedEventCountV1() {
|
||||
try {
|
||||
// Get array of rejected log events
|
||||
const rejectedLogEvents = await globals.rejectedEvents.getRejectedLogEvents();
|
||||
|
||||
globals.logger.debug(
|
||||
`REJECTED EVENT COUNT V1: Rejected log events: ${JSON.stringify(
|
||||
rejectedLogEvents,
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
);
|
||||
|
||||
// Are there any events to store?
|
||||
if (rejectedLogEvents.length === 0) {
|
||||
globals.logger.verbose('REJECTED EVENT COUNT V1: No events to store in InfluxDB');
|
||||
return;
|
||||
}
|
||||
|
||||
const points = [];
|
||||
|
||||
// Get measurement name to use for rejected events
|
||||
const measurementName = globals.config.get(
|
||||
'Butler-SOS.qlikSenseEvents.rejectedEventCount.influxdb.measurementName'
|
||||
);
|
||||
|
||||
// Loop through data in rejected log events and create datapoints
|
||||
// Use counter and process_time as fields
|
||||
for (const event of rejectedLogEvents) {
|
||||
if (event.source === 'qseow-qix-perf') {
|
||||
// For each unique combination of source, appId, appName, method and objectType,
|
||||
// write the counter and processTime properties to InfluxDB
|
||||
const tags = {
|
||||
source: event.source,
|
||||
app_id: event.appId,
|
||||
method: event.method,
|
||||
object_type: event.objectType,
|
||||
};
|
||||
|
||||
// Tags that are empty in some cases. Only add if they are non-empty
|
||||
if (event?.appName?.length > 0) {
|
||||
tags.app_name = event.appName;
|
||||
tags.app_name_set = 'true';
|
||||
} else {
|
||||
tags.app_name_set = 'false';
|
||||
}
|
||||
|
||||
// Add static tags from config file
|
||||
if (
|
||||
globals.config.has(
|
||||
'Butler-SOS.logEvents.enginePerformanceMonitor.trackRejectedEvents.tags'
|
||||
) &&
|
||||
globals.config.get(
|
||||
'Butler-SOS.logEvents.enginePerformanceMonitor.trackRejectedEvents.tags'
|
||||
) !== null &&
|
||||
globals.config.get(
|
||||
'Butler-SOS.logEvents.enginePerformanceMonitor.trackRejectedEvents.tags'
|
||||
).length > 0
|
||||
) {
|
||||
const configTags = globals.config.get(
|
||||
'Butler-SOS.logEvents.enginePerformanceMonitor.trackRejectedEvents.tags'
|
||||
);
|
||||
for (const item of configTags) {
|
||||
tags[item.name] = item.value;
|
||||
}
|
||||
}
|
||||
|
||||
const fields = {
|
||||
counter: event.counter,
|
||||
process_time: event.processTime,
|
||||
};
|
||||
|
||||
const point = {
|
||||
measurement: measurementName,
|
||||
tags,
|
||||
fields,
|
||||
};
|
||||
|
||||
points.push(point);
|
||||
} else {
|
||||
const point = {
|
||||
measurement: measurementName,
|
||||
tags: {
|
||||
source: event.source,
|
||||
},
|
||||
fields: {
|
||||
counter: event.counter,
|
||||
},
|
||||
};
|
||||
|
||||
points.push(point);
|
||||
}
|
||||
}
|
||||
|
||||
await globals.influx.writePoints(points);
|
||||
|
||||
globals.logger.verbose(
|
||||
'REJECTED EVENT COUNT V1: Sent rejected event count data to InfluxDB'
|
||||
);
|
||||
} catch (err) {
|
||||
globals.logger.error(`REJECTED EVENT COUNT V1: Error saving data: ${err}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
156
src/lib/influxdb/v1/health-metrics.js
Normal file
156
src/lib/influxdb/v1/health-metrics.js
Normal file
@@ -0,0 +1,156 @@
|
||||
import globals from '../../../globals.js';
|
||||
import { getFormattedTime, processAppDocuments } from '../shared/utils.js';
|
||||
|
||||
/**
|
||||
* Store health metrics from multiple Sense engines to InfluxDB v1
|
||||
*
|
||||
* @param {object} serverTags - Server tags for all measurements
|
||||
* @param {object} body - Health metrics data from Sense engine
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function storeHealthMetricsV1(serverTags, body) {
|
||||
try {
|
||||
// Process app names for different document types
|
||||
const [appNamesActive, sessionAppNamesActive] = await processAppDocuments(
|
||||
body.apps.active_docs
|
||||
);
|
||||
const [appNamesLoaded, sessionAppNamesLoaded] = await processAppDocuments(
|
||||
body.apps.loaded_docs
|
||||
);
|
||||
const [appNamesInMemory, sessionAppNamesInMemory] = await processAppDocuments(
|
||||
body.apps.in_memory_docs
|
||||
);
|
||||
|
||||
// Create datapoint array for v1 - plain objects with measurement, tags, fields
|
||||
const datapoint = [
|
||||
{
|
||||
measurement: 'sense_server',
|
||||
tags: serverTags,
|
||||
fields: {
|
||||
version: body.version,
|
||||
started: body.started,
|
||||
uptime: getFormattedTime(body.started),
|
||||
},
|
||||
},
|
||||
{
|
||||
measurement: 'mem',
|
||||
tags: serverTags,
|
||||
fields: {
|
||||
comitted: body.mem.committed,
|
||||
allocated: body.mem.allocated,
|
||||
free: body.mem.free,
|
||||
},
|
||||
},
|
||||
{
|
||||
measurement: 'apps',
|
||||
tags: serverTags,
|
||||
fields: {
|
||||
active_docs_count: body.apps.active_docs.length,
|
||||
loaded_docs_count: body.apps.loaded_docs.length,
|
||||
in_memory_docs_count: body.apps.in_memory_docs.length,
|
||||
|
||||
active_docs: globals.config.get(
|
||||
'Butler-SOS.influxdbConfig.includeFields.activeDocs'
|
||||
)
|
||||
? body.apps.active_docs
|
||||
: '',
|
||||
active_docs_names:
|
||||
globals.config.get('Butler-SOS.appNames.enableAppNameExtract') &&
|
||||
globals.config.get('Butler-SOS.influxdbConfig.includeFields.activeDocs')
|
||||
? appNamesActive.map((name) => `"${name}"`).join(',')
|
||||
: '',
|
||||
active_session_docs_names:
|
||||
globals.config.get('Butler-SOS.appNames.enableAppNameExtract') &&
|
||||
globals.config.get('Butler-SOS.influxdbConfig.includeFields.activeDocs')
|
||||
? sessionAppNamesActive.map((name) => `"${name}"`).join(',')
|
||||
: '',
|
||||
|
||||
loaded_docs: globals.config.get(
|
||||
'Butler-SOS.influxdbConfig.includeFields.loadedDocs'
|
||||
)
|
||||
? body.apps.loaded_docs
|
||||
: '',
|
||||
loaded_docs_names:
|
||||
globals.config.get('Butler-SOS.appNames.enableAppNameExtract') &&
|
||||
globals.config.get('Butler-SOS.influxdbConfig.includeFields.loadedDocs')
|
||||
? appNamesLoaded.map((name) => `"${name}"`).join(',')
|
||||
: '',
|
||||
loaded_session_docs_names:
|
||||
globals.config.get('Butler-SOS.appNames.enableAppNameExtract') &&
|
||||
globals.config.get('Butler-SOS.influxdbConfig.includeFields.loadedDocs')
|
||||
? sessionAppNamesLoaded.map((name) => `"${name}"`).join(',')
|
||||
: '',
|
||||
|
||||
in_memory_docs: globals.config.get(
|
||||
'Butler-SOS.influxdbConfig.includeFields.inMemoryDocs'
|
||||
)
|
||||
? body.apps.in_memory_docs
|
||||
: '',
|
||||
in_memory_docs_names:
|
||||
globals.config.get('Butler-SOS.appNames.enableAppNameExtract') &&
|
||||
globals.config.get('Butler-SOS.influxdbConfig.includeFields.inMemoryDocs')
|
||||
? appNamesInMemory.map((name) => `"${name}"`).join(',')
|
||||
: '',
|
||||
in_memory_session_docs_names:
|
||||
globals.config.get('Butler-SOS.appNames.enableAppNameExtract') &&
|
||||
globals.config.get('Butler-SOS.influxdbConfig.includeFields.inMemoryDocs')
|
||||
? sessionAppNamesInMemory.map((name) => `"${name}"`).join(',')
|
||||
: '',
|
||||
calls: body.apps.calls,
|
||||
selections: body.apps.selections,
|
||||
},
|
||||
},
|
||||
{
|
||||
measurement: 'cpu',
|
||||
tags: serverTags,
|
||||
fields: {
|
||||
total: body.cpu.total,
|
||||
},
|
||||
},
|
||||
{
|
||||
measurement: 'session',
|
||||
tags: serverTags,
|
||||
fields: {
|
||||
active: body.session.active,
|
||||
total: body.session.total,
|
||||
},
|
||||
},
|
||||
{
|
||||
measurement: 'users',
|
||||
tags: serverTags,
|
||||
fields: {
|
||||
active: body.users.active,
|
||||
total: body.users.total,
|
||||
},
|
||||
},
|
||||
{
|
||||
measurement: 'cache',
|
||||
tags: serverTags,
|
||||
fields: {
|
||||
hits: body.cache.hits,
|
||||
lookups: body.cache.lookups,
|
||||
added: body.cache.added,
|
||||
replaced: body.cache.replaced,
|
||||
bytes_added: body.cache.bytes_added,
|
||||
},
|
||||
},
|
||||
{
|
||||
measurement: 'saturated',
|
||||
tags: serverTags,
|
||||
fields: {
|
||||
saturated: body.saturated,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Write to InfluxDB v1 using node-influx library
|
||||
await globals.influx.writePoints(datapoint);
|
||||
|
||||
globals.logger.verbose(
|
||||
`INFLUXDB V1 HEALTH METRICS: Stored health data from server: ${serverTags.server_name}`
|
||||
);
|
||||
} catch (err) {
|
||||
globals.logger.error(`INFLUXDB V1 HEALTH METRICS: Error saving health data: ${err}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
210
src/lib/influxdb/v1/log-events.js
Normal file
210
src/lib/influxdb/v1/log-events.js
Normal file
@@ -0,0 +1,210 @@
|
||||
import globals from '../../../globals.js';
|
||||
|
||||
/**
|
||||
* Store log event to InfluxDB v1
|
||||
* Handles log events from different Sense sources
|
||||
*
|
||||
* @param {object} msg - Log event message
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function storeLogEventV1(msg) {
|
||||
try {
|
||||
globals.logger.debug(`LOG EVENT V1: ${JSON.stringify(msg)}`);
|
||||
|
||||
// Check if this is a supported source
|
||||
if (
|
||||
msg.source !== 'qseow-engine' &&
|
||||
msg.source !== 'qseow-proxy' &&
|
||||
msg.source !== 'qseow-scheduler' &&
|
||||
msg.source !== 'qseow-repository' &&
|
||||
msg.source !== 'qseow-qix-perf'
|
||||
) {
|
||||
globals.logger.warn(`LOG EVENT V1: Unsupported log event source: ${msg.source}`);
|
||||
return;
|
||||
}
|
||||
|
||||
let tags;
|
||||
let fields;
|
||||
|
||||
// Process each source type
|
||||
if (msg.source === 'qseow-engine') {
|
||||
tags = {
|
||||
host: msg.host,
|
||||
level: msg.level,
|
||||
source: msg.source,
|
||||
log_row: msg.log_row,
|
||||
subsystem: msg.subsystem,
|
||||
};
|
||||
|
||||
// Tags that are empty in some cases. Only add if they are non-empty
|
||||
if (msg?.user_full?.length > 0) tags.user_full = msg.user_full;
|
||||
if (msg?.user_directory?.length > 0) tags.user_directory = msg.user_directory;
|
||||
if (msg?.user_id?.length > 0) tags.user_id = msg.user_id;
|
||||
if (msg?.result_code?.length > 0) tags.result_code = msg.result_code;
|
||||
if (msg?.windows_user?.length > 0) tags.windows_user = msg.windows_user;
|
||||
if (msg?.task_id?.length > 0) tags.task_id = msg.task_id;
|
||||
if (msg?.task_name?.length > 0) tags.task_name = msg.task_name;
|
||||
if (msg?.app_id?.length > 0) tags.app_id = msg.app_id;
|
||||
if (msg?.app_name?.length > 0) tags.app_name = msg.app_name;
|
||||
if (msg?.engine_exe_version?.length > 0)
|
||||
tags.engine_exe_version = msg.engine_exe_version;
|
||||
|
||||
fields = {
|
||||
message: msg.message,
|
||||
exception_message: msg.exception_message,
|
||||
command: msg.command,
|
||||
result_code: msg.result_code,
|
||||
origin: msg.origin,
|
||||
context: msg.context,
|
||||
session_id: msg.session_id,
|
||||
raw_event: JSON.stringify(msg),
|
||||
};
|
||||
} else if (msg.source === 'qseow-proxy') {
|
||||
tags = {
|
||||
host: msg.host,
|
||||
level: msg.level,
|
||||
source: msg.source,
|
||||
log_row: msg.log_row,
|
||||
subsystem: msg.subsystem,
|
||||
};
|
||||
|
||||
// Tags that are empty in some cases. Only add if they are non-empty
|
||||
if (msg?.user_full?.length > 0) tags.user_full = msg.user_full;
|
||||
if (msg?.user_directory?.length > 0) tags.user_directory = msg.user_directory;
|
||||
if (msg?.user_id?.length > 0) tags.user_id = msg.user_id;
|
||||
if (msg?.result_code?.length > 0) tags.result_code = msg.result_code;
|
||||
|
||||
fields = {
|
||||
message: msg.message,
|
||||
exception_message: msg.exception_message,
|
||||
command: msg.command,
|
||||
result_code: msg.result_code,
|
||||
origin: msg.origin,
|
||||
context: msg.context,
|
||||
raw_event: JSON.stringify(msg),
|
||||
};
|
||||
} else if (msg.source === 'qseow-scheduler') {
|
||||
tags = {
|
||||
host: msg.host,
|
||||
level: msg.level,
|
||||
source: msg.source,
|
||||
log_row: msg.log_row,
|
||||
subsystem: msg.subsystem,
|
||||
};
|
||||
|
||||
// Tags that are empty in some cases. Only add if they are non-empty
|
||||
if (msg?.user_full?.length > 0) tags.user_full = msg.user_full;
|
||||
if (msg?.user_directory?.length > 0) tags.user_directory = msg.user_directory;
|
||||
if (msg?.user_id?.length > 0) tags.user_id = msg.user_id;
|
||||
if (msg?.task_id?.length > 0) tags.task_id = msg.task_id;
|
||||
if (msg?.task_name?.length > 0) tags.task_name = msg.task_name;
|
||||
|
||||
fields = {
|
||||
message: msg.message,
|
||||
exception_message: msg.exception_message,
|
||||
app_name: msg.app_name,
|
||||
app_id: msg.app_id,
|
||||
execution_id: msg.execution_id,
|
||||
raw_event: JSON.stringify(msg),
|
||||
};
|
||||
} else if (msg.source === 'qseow-repository') {
|
||||
tags = {
|
||||
host: msg.host,
|
||||
level: msg.level,
|
||||
source: msg.source,
|
||||
log_row: msg.log_row,
|
||||
subsystem: msg.subsystem,
|
||||
};
|
||||
|
||||
// Tags that are empty in some cases. Only add if they are non-empty
|
||||
if (msg?.user_full?.length > 0) tags.user_full = msg.user_full;
|
||||
if (msg?.user_directory?.length > 0) tags.user_directory = msg.user_directory;
|
||||
if (msg?.user_id?.length > 0) tags.user_id = msg.user_id;
|
||||
if (msg?.result_code?.length > 0) tags.result_code = msg.result_code;
|
||||
|
||||
fields = {
|
||||
message: msg.message,
|
||||
exception_message: msg.exception_message,
|
||||
command: msg.command,
|
||||
result_code: msg.result_code,
|
||||
origin: msg.origin,
|
||||
context: msg.context,
|
||||
raw_event: JSON.stringify(msg),
|
||||
};
|
||||
} else if (msg.source === 'qseow-qix-perf') {
|
||||
tags = {
|
||||
host: msg.host?.length > 0 ? msg.host : '<Unknown>',
|
||||
level: msg.level?.length > 0 ? msg.level : '<Unknown>',
|
||||
source: msg.source?.length > 0 ? msg.source : '<Unknown>',
|
||||
log_row: msg.log_row?.length > 0 ? msg.log_row : '-1',
|
||||
subsystem: msg.subsystem?.length > 0 ? msg.subsystem : '<Unknown>',
|
||||
method: msg.method?.length > 0 ? msg.method : '<Unknown>',
|
||||
object_type: msg.object_type?.length > 0 ? msg.object_type : '<Unknown>',
|
||||
proxy_session_id: msg.proxy_session_id?.length > 0 ? msg.proxy_session_id : '-1',
|
||||
session_id: msg.session_id?.length > 0 ? msg.session_id : '-1',
|
||||
event_activity_source:
|
||||
msg.event_activity_source?.length > 0 ? msg.event_activity_source : '<Unknown>',
|
||||
};
|
||||
|
||||
// Tags that are empty in some cases. Only add if they are non-empty
|
||||
if (msg?.user_full?.length > 0) tags.user_full = msg.user_full;
|
||||
if (msg?.user_directory?.length > 0) tags.user_directory = msg.user_directory;
|
||||
if (msg?.user_id?.length > 0) tags.user_id = msg.user_id;
|
||||
if (msg?.app_id?.length > 0) tags.app_id = msg.app_id;
|
||||
if (msg?.app_name?.length > 0) tags.app_name = msg.app_name;
|
||||
if (msg?.object_id?.length > 0) tags.object_id = msg.object_id;
|
||||
|
||||
fields = {
|
||||
app_id: msg.app_id,
|
||||
process_time: msg.process_time,
|
||||
work_time: msg.work_time,
|
||||
lock_time: msg.lock_time,
|
||||
validate_time: msg.validate_time,
|
||||
traverse_time: msg.traverse_time,
|
||||
handle: msg.handle,
|
||||
net_ram: msg.net_ram,
|
||||
peak_ram: msg.peak_ram,
|
||||
raw_event: JSON.stringify(msg),
|
||||
};
|
||||
}
|
||||
|
||||
// Add log event categories to tags if available
|
||||
// The msg.category array contains objects with properties 'name' and 'value'
|
||||
if (msg?.category?.length > 0) {
|
||||
msg.category.forEach((category) => {
|
||||
tags[category.name] = category.value;
|
||||
});
|
||||
}
|
||||
|
||||
// Add custom tags from config file to payload
|
||||
if (
|
||||
globals.config.has('Butler-SOS.logEvents.tags') &&
|
||||
globals.config.get('Butler-SOS.logEvents.tags') !== null &&
|
||||
globals.config.get('Butler-SOS.logEvents.tags').length > 0
|
||||
) {
|
||||
const configTags = globals.config.get('Butler-SOS.logEvents.tags');
|
||||
for (const item of configTags) {
|
||||
tags[item.name] = item.value;
|
||||
}
|
||||
}
|
||||
|
||||
const datapoint = [
|
||||
{
|
||||
measurement: 'log_event',
|
||||
tags,
|
||||
fields,
|
||||
},
|
||||
];
|
||||
|
||||
globals.logger.silly(
|
||||
`LOG EVENT V1: Influxdb datapoint: ${JSON.stringify(datapoint, null, 2)}`
|
||||
);
|
||||
|
||||
await globals.influx.writePoints(datapoint);
|
||||
|
||||
globals.logger.verbose('LOG EVENT V1: Sent log event data to InfluxDB');
|
||||
} catch (err) {
|
||||
globals.logger.error(`LOG EVENT V1: Error saving log event: ${err}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
151
src/lib/influxdb/v1/queue-metrics.js
Normal file
151
src/lib/influxdb/v1/queue-metrics.js
Normal file
@@ -0,0 +1,151 @@
|
||||
import globals from '../../../globals.js';
|
||||
|
||||
/**
|
||||
* Store user event queue metrics to InfluxDB v1
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function storeUserEventQueueMetricsV1() {
|
||||
try {
|
||||
// Check if queue metrics are enabled
|
||||
if (
|
||||
!globals.config.get(
|
||||
'Butler-SOS.userEvents.udpServerConfig.queueMetrics.influxdb.enable'
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get metrics from queue manager
|
||||
const queueManager = globals.udpQueueManagerUserActivity;
|
||||
if (!queueManager) {
|
||||
globals.logger.warn('USER EVENT QUEUE METRICS V1: Queue manager not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
const metrics = await queueManager.getMetrics();
|
||||
|
||||
// Get configuration
|
||||
const measurementName = globals.config.get(
|
||||
'Butler-SOS.userEvents.udpServerConfig.queueMetrics.influxdb.measurementName'
|
||||
);
|
||||
const configTags = globals.config.get(
|
||||
'Butler-SOS.userEvents.udpServerConfig.queueMetrics.influxdb.tags'
|
||||
);
|
||||
|
||||
const point = {
|
||||
measurement: measurementName,
|
||||
tags: {
|
||||
queue_type: 'user_events',
|
||||
host: globals.hostInfo.hostname,
|
||||
},
|
||||
fields: {
|
||||
queue_size: metrics.queueSize,
|
||||
queue_max_size: metrics.queueMaxSize,
|
||||
queue_utilization_pct: metrics.queueUtilizationPct,
|
||||
queue_pending: metrics.queuePending,
|
||||
messages_received: metrics.messagesReceived,
|
||||
messages_queued: metrics.messagesQueued,
|
||||
messages_processed: metrics.messagesProcessed,
|
||||
messages_failed: metrics.messagesFailed,
|
||||
messages_dropped_total: metrics.messagesDroppedTotal,
|
||||
messages_dropped_rate_limit: metrics.messagesDroppedRateLimit,
|
||||
messages_dropped_queue_full: metrics.messagesDroppedQueueFull,
|
||||
messages_dropped_size: metrics.messagesDroppedSize,
|
||||
processing_time_avg_ms: metrics.processingTimeAvgMs,
|
||||
processing_time_p95_ms: metrics.processingTimeP95Ms,
|
||||
processing_time_max_ms: metrics.processingTimeMaxMs,
|
||||
rate_limit_current: metrics.rateLimitCurrent,
|
||||
backpressure_active: metrics.backpressureActive,
|
||||
},
|
||||
};
|
||||
|
||||
// Add static tags from config file
|
||||
if (configTags && configTags.length > 0) {
|
||||
for (const item of configTags) {
|
||||
point.tags[item.name] = item.value;
|
||||
}
|
||||
}
|
||||
|
||||
await globals.influx.writePoints([point]);
|
||||
|
||||
globals.logger.verbose('USER EVENT QUEUE METRICS V1: Sent queue metrics data to InfluxDB');
|
||||
} catch (err) {
|
||||
globals.logger.error(`USER EVENT QUEUE METRICS V1: Error saving data: ${err}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store log event queue metrics to InfluxDB v1
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function storeLogEventQueueMetricsV1() {
|
||||
try {
|
||||
// Check if queue metrics are enabled
|
||||
if (
|
||||
!globals.config.get('Butler-SOS.logEvents.udpServerConfig.queueMetrics.influxdb.enable')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get metrics from queue manager
|
||||
const queueManager = globals.udpQueueManagerLogEvents;
|
||||
if (!queueManager) {
|
||||
globals.logger.warn('LOG EVENT QUEUE METRICS V1: Queue manager not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
const metrics = await queueManager.getMetrics();
|
||||
|
||||
// Get configuration
|
||||
const measurementName = globals.config.get(
|
||||
'Butler-SOS.logEvents.udpServerConfig.queueMetrics.influxdb.measurementName'
|
||||
);
|
||||
const configTags = globals.config.get(
|
||||
'Butler-SOS.logEvents.udpServerConfig.queueMetrics.influxdb.tags'
|
||||
);
|
||||
|
||||
const point = {
|
||||
measurement: measurementName,
|
||||
tags: {
|
||||
queue_type: 'log_events',
|
||||
host: globals.hostInfo.hostname,
|
||||
},
|
||||
fields: {
|
||||
queue_size: metrics.queueSize,
|
||||
queue_max_size: metrics.queueMaxSize,
|
||||
queue_utilization_pct: metrics.queueUtilizationPct,
|
||||
queue_pending: metrics.queuePending,
|
||||
messages_received: metrics.messagesReceived,
|
||||
messages_queued: metrics.messagesQueued,
|
||||
messages_processed: metrics.messagesProcessed,
|
||||
messages_failed: metrics.messagesFailed,
|
||||
messages_dropped_total: metrics.messagesDroppedTotal,
|
||||
messages_dropped_rate_limit: metrics.messagesDroppedRateLimit,
|
||||
messages_dropped_queue_full: metrics.messagesDroppedQueueFull,
|
||||
messages_dropped_size: metrics.messagesDroppedSize,
|
||||
processing_time_avg_ms: metrics.processingTimeAvgMs,
|
||||
processing_time_p95_ms: metrics.processingTimeP95Ms,
|
||||
processing_time_max_ms: metrics.processingTimeMaxMs,
|
||||
rate_limit_current: metrics.rateLimitCurrent,
|
||||
backpressure_active: metrics.backpressureActive,
|
||||
},
|
||||
};
|
||||
|
||||
// Add static tags from config file
|
||||
if (configTags && configTags.length > 0) {
|
||||
for (const item of configTags) {
|
||||
point.tags[item.name] = item.value;
|
||||
}
|
||||
}
|
||||
|
||||
await globals.influx.writePoints([point]);
|
||||
|
||||
globals.logger.verbose('LOG EVENT QUEUE METRICS V1: Sent queue metrics data to InfluxDB');
|
||||
} catch (err) {
|
||||
globals.logger.error(`LOG EVENT QUEUE METRICS V1: Error saving data: ${err}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
39
src/lib/influxdb/v1/sessions.js
Normal file
39
src/lib/influxdb/v1/sessions.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import globals from '../../../globals.js';
|
||||
|
||||
/**
|
||||
* Store proxy session data to InfluxDB v1
|
||||
*
|
||||
* @param {object} userSessions - User session data including datapointInfluxdb array
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function storeSessionsV1(userSessions) {
|
||||
try {
|
||||
globals.logger.silly(
|
||||
`PROXY SESSIONS V1: Influxdb datapoint for server "${userSessions.host}", virtual proxy "${userSessions.virtualProxy}": ${JSON.stringify(
|
||||
userSessions.datapointInfluxdb,
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
);
|
||||
|
||||
// Data points are already in InfluxDB v1 format (plain objects)
|
||||
// Write array of measurements: user_session_summary, user_session_list, user_session_details
|
||||
await globals.influx.writePoints(userSessions.datapointInfluxdb);
|
||||
|
||||
globals.logger.debug(
|
||||
`PROXY SESSIONS V1: Session count for server "${userSessions.host}", virtual proxy "${userSessions.virtualProxy}": ${userSessions.sessionCount}`
|
||||
);
|
||||
globals.logger.debug(
|
||||
`PROXY SESSIONS V1: User list for server "${userSessions.host}", virtual proxy "${userSessions.virtualProxy}": ${userSessions.uniqueUserList}`
|
||||
);
|
||||
|
||||
globals.logger.verbose(
|
||||
`PROXY SESSIONS V1: Sent user session data to InfluxDB for server "${userSessions.host}", virtual proxy "${userSessions.virtualProxy}"`
|
||||
);
|
||||
} catch (err) {
|
||||
globals.logger.error(
|
||||
`PROXY SESSIONS V1: Error saving user session data: ${globals.getErrorMessage(err)}`
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
72
src/lib/influxdb/v1/user-events.js
Normal file
72
src/lib/influxdb/v1/user-events.js
Normal file
@@ -0,0 +1,72 @@
|
||||
import globals from '../../../globals.js';
|
||||
|
||||
/**
|
||||
* Store user event to InfluxDB v1
|
||||
*
|
||||
* @param {object} msg - User event message
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function storeUserEventV1(msg) {
|
||||
try {
|
||||
globals.logger.debug(`USER EVENT V1: ${JSON.stringify(msg)}`);
|
||||
|
||||
// First prepare tags relating to the actual user event, then add tags defined in the config file
|
||||
// The config file tags can for example be used to separate data from DEV/TEST/PROD environments
|
||||
const tags = {
|
||||
host: msg.host,
|
||||
event_action: msg.command,
|
||||
userFull: `${msg.user_directory}\\${msg.user_id}`,
|
||||
userDirectory: msg.user_directory,
|
||||
userId: msg.user_id,
|
||||
origin: msg.origin,
|
||||
};
|
||||
|
||||
// Add app id and name to tags if available
|
||||
if (msg?.appId) tags.appId = msg.appId;
|
||||
if (msg?.appName) tags.appName = msg.appName;
|
||||
|
||||
// Add user agent info to tags if available
|
||||
if (msg?.ua?.browser?.name) tags.uaBrowserName = msg?.ua?.browser?.name;
|
||||
if (msg?.ua?.browser?.major) tags.uaBrowserMajorVersion = msg?.ua?.browser?.major;
|
||||
if (msg?.ua?.os?.name) tags.uaOsName = msg?.ua?.os?.name;
|
||||
if (msg?.ua?.os?.version) tags.uaOsVersion = msg?.ua?.os?.version;
|
||||
|
||||
// Add custom tags from config file to payload
|
||||
if (
|
||||
globals.config.has('Butler-SOS.userEvents.tags') &&
|
||||
globals.config.get('Butler-SOS.userEvents.tags') !== null &&
|
||||
globals.config.get('Butler-SOS.userEvents.tags').length > 0
|
||||
) {
|
||||
const configTags = globals.config.get('Butler-SOS.userEvents.tags');
|
||||
for (const item of configTags) {
|
||||
tags[item.name] = item.value;
|
||||
}
|
||||
}
|
||||
|
||||
const datapoint = [
|
||||
{
|
||||
measurement: 'user_events',
|
||||
tags,
|
||||
fields: {
|
||||
userFull: tags.userFull,
|
||||
userId: tags.userId,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Add app id and name to fields if available
|
||||
if (msg?.appId) datapoint[0].fields.appId = msg.appId;
|
||||
if (msg?.appName) datapoint[0].fields.appName = msg.appName;
|
||||
|
||||
globals.logger.silly(
|
||||
`USER EVENT V1: Influxdb datapoint: ${JSON.stringify(datapoint, null, 2)}`
|
||||
);
|
||||
|
||||
await globals.influx.writePoints(datapoint);
|
||||
|
||||
globals.logger.verbose('USER EVENT V1: Sent user event data to InfluxDB');
|
||||
} catch (err) {
|
||||
globals.logger.error(`USER EVENT V1: Error saving user event: ${err}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
56
src/lib/influxdb/v2/butler-memory.js
Normal file
56
src/lib/influxdb/v2/butler-memory.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import { Point } from '@influxdata/influxdb-client';
|
||||
import globals from '../../../globals.js';
|
||||
|
||||
/**
|
||||
* Store Butler SOS memory usage to InfluxDB v2
|
||||
*
|
||||
* @param {object} memory - Memory usage data
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function storeButlerMemoryV2(memory) {
|
||||
try {
|
||||
const butlerVersion = globals.appVersion;
|
||||
|
||||
// Create write API with options
|
||||
const writeOptions = {
|
||||
flushInterval: 5000,
|
||||
maxRetries: 2,
|
||||
};
|
||||
|
||||
const org = globals.config.get('Butler-SOS.influxdbConfig.v2Config.org');
|
||||
const bucketName = globals.config.get('Butler-SOS.influxdbConfig.v2Config.bucket');
|
||||
|
||||
const writeApi = globals.influx.getWriteApi(org, bucketName, 'ns', writeOptions);
|
||||
|
||||
if (!writeApi) {
|
||||
globals.logger.warn('MEMORY USAGE V2: Influxdb write API object not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create point using v2 Point class
|
||||
const point = new Point('butlersos_memory_usage')
|
||||
.tag('butler_sos_instance', memory.instanceTag)
|
||||
.tag('version', butlerVersion)
|
||||
.floatField('heap_used', memory.heapUsedMByte)
|
||||
.floatField('heap_total', memory.heapTotalMByte)
|
||||
.floatField('external', memory.externalMemoryMByte)
|
||||
.floatField('process_memory', memory.processMemoryMByte);
|
||||
|
||||
globals.logger.silly(
|
||||
`MEMORY USAGE V2: Influxdb datapoint for Butler SOS memory usage: ${JSON.stringify(
|
||||
point,
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
);
|
||||
|
||||
await writeApi.writePoint(point);
|
||||
|
||||
globals.logger.verbose('MEMORY USAGE V2: Sent Butler SOS memory usage data to InfluxDB');
|
||||
} catch (err) {
|
||||
globals.logger.error(
|
||||
`MEMORY USAGE V2: Error saving Butler SOS memory data: ${globals.getErrorMessage(err)}`
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
216
src/lib/influxdb/v2/event-counts.js
Normal file
216
src/lib/influxdb/v2/event-counts.js
Normal file
@@ -0,0 +1,216 @@
|
||||
import { Point } from '@influxdata/influxdb-client';
|
||||
import globals from '../../../globals.js';
|
||||
|
||||
/**
|
||||
* Store event counts to InfluxDB v2
|
||||
* Aggregates and stores counts for log and user events
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function storeEventCountV2() {
|
||||
try {
|
||||
// Get array of log events
|
||||
const logEvents = await globals.udpEvents.getLogEvents();
|
||||
const userEvents = await globals.udpEvents.getUserEvents();
|
||||
|
||||
globals.logger.debug(`EVENT COUNT V2: Log events: ${JSON.stringify(logEvents, null, 2)}`);
|
||||
globals.logger.debug(`EVENT COUNT V2: User events: ${JSON.stringify(userEvents, null, 2)}`);
|
||||
|
||||
// Are there any events to store?
|
||||
if (logEvents.length === 0 && userEvents.length === 0) {
|
||||
globals.logger.verbose('EVENT COUNT V2: No events to store in InfluxDB');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create write API with options
|
||||
const writeOptions = {
|
||||
flushInterval: 5000,
|
||||
maxRetries: 2,
|
||||
};
|
||||
|
||||
const org = globals.config.get('Butler-SOS.influxdbConfig.v2Config.org');
|
||||
const bucketName = globals.config.get('Butler-SOS.influxdbConfig.v2Config.bucket');
|
||||
|
||||
const writeApi = globals.influx.getWriteApi(org, bucketName, 'ns', writeOptions);
|
||||
|
||||
if (!writeApi) {
|
||||
globals.logger.warn('EVENT COUNT V2: Influxdb write API object not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const points = [];
|
||||
|
||||
// Get measurement name to use for event counts
|
||||
const measurementName = globals.config.get(
|
||||
'Butler-SOS.qlikSenseEvents.eventCount.influxdb.measurementName'
|
||||
);
|
||||
|
||||
// Loop through data in log events and create datapoints
|
||||
for (const event of logEvents) {
|
||||
const point = new Point(measurementName)
|
||||
.tag('event_type', 'log')
|
||||
.tag('source', event.source)
|
||||
.tag('host', event.host)
|
||||
.tag('subsystem', event.subsystem)
|
||||
.intField('counter', event.counter);
|
||||
|
||||
// Add static tags from config file
|
||||
if (
|
||||
globals.config.has('Butler-SOS.qlikSenseEvents.eventCount.influxdb.tags') &&
|
||||
globals.config.get('Butler-SOS.qlikSenseEvents.eventCount.influxdb.tags') !==
|
||||
null &&
|
||||
globals.config.get('Butler-SOS.qlikSenseEvents.eventCount.influxdb.tags').length > 0
|
||||
) {
|
||||
const configTags = globals.config.get(
|
||||
'Butler-SOS.qlikSenseEvents.eventCount.influxdb.tags'
|
||||
);
|
||||
for (const item of configTags) {
|
||||
point.tag(item.name, item.value);
|
||||
}
|
||||
}
|
||||
|
||||
points.push(point);
|
||||
}
|
||||
|
||||
// Loop through data in user events and create datapoints
|
||||
for (const event of userEvents) {
|
||||
const point = new Point(measurementName)
|
||||
.tag('event_type', 'user')
|
||||
.tag('source', event.source)
|
||||
.tag('host', event.host)
|
||||
.tag('subsystem', event.subsystem)
|
||||
.intField('counter', event.counter);
|
||||
|
||||
// Add static tags from config file
|
||||
if (
|
||||
globals.config.has('Butler-SOS.qlikSenseEvents.eventCount.influxdb.tags') &&
|
||||
globals.config.get('Butler-SOS.qlikSenseEvents.eventCount.influxdb.tags') !==
|
||||
null &&
|
||||
globals.config.get('Butler-SOS.qlikSenseEvents.eventCount.influxdb.tags').length > 0
|
||||
) {
|
||||
const configTags = globals.config.get(
|
||||
'Butler-SOS.qlikSenseEvents.eventCount.influxdb.tags'
|
||||
);
|
||||
for (const item of configTags) {
|
||||
point.tag(item.name, item.value);
|
||||
}
|
||||
}
|
||||
|
||||
points.push(point);
|
||||
}
|
||||
|
||||
await writeApi.writePoints(points);
|
||||
|
||||
globals.logger.verbose('EVENT COUNT V2: Sent event count data to InfluxDB');
|
||||
} catch (err) {
|
||||
globals.logger.error(`EVENT COUNT V2: Error saving data: ${err}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store rejected event counts to InfluxDB v2
|
||||
* Tracks events that were rejected due to validation failures or rate limiting
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function storeRejectedEventCountV2() {
|
||||
try {
|
||||
// Get array of rejected log events
|
||||
const rejectedLogEvents = await globals.rejectedEvents.getRejectedLogEvents();
|
||||
|
||||
globals.logger.debug(
|
||||
`REJECTED EVENT COUNT V2: Rejected log events: ${JSON.stringify(
|
||||
rejectedLogEvents,
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
);
|
||||
|
||||
// Are there any events to store?
|
||||
if (rejectedLogEvents.length === 0) {
|
||||
globals.logger.verbose('REJECTED EVENT COUNT V2: No events to store in InfluxDB');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create write API with options
|
||||
const writeOptions = {
|
||||
flushInterval: 5000,
|
||||
maxRetries: 2,
|
||||
};
|
||||
|
||||
const org = globals.config.get('Butler-SOS.influxdbConfig.v2Config.org');
|
||||
const bucketName = globals.config.get('Butler-SOS.influxdbConfig.v2Config.bucket');
|
||||
|
||||
const writeApi = globals.influx.getWriteApi(org, bucketName, 'ns', writeOptions);
|
||||
|
||||
if (!writeApi) {
|
||||
globals.logger.warn('REJECTED EVENT COUNT V2: Influxdb write API object not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const points = [];
|
||||
|
||||
// Get measurement name to use for rejected events
|
||||
const measurementName = globals.config.get(
|
||||
'Butler-SOS.qlikSenseEvents.rejectedEventCount.influxdb.measurementName'
|
||||
);
|
||||
|
||||
// Loop through data in rejected log events and create datapoints
|
||||
for (const event of rejectedLogEvents) {
|
||||
if (event.source === 'qseow-qix-perf') {
|
||||
// For qix-perf events, include app info and performance metrics
|
||||
let point = new Point(measurementName)
|
||||
.tag('source', event.source)
|
||||
.tag('app_id', event.appId)
|
||||
.tag('method', event.method)
|
||||
.tag('object_type', event.objectType)
|
||||
.intField('counter', event.counter)
|
||||
.floatField('process_time', event.processTime);
|
||||
|
||||
if (event?.appName?.length > 0) {
|
||||
point.tag('app_name', event.appName).tag('app_name_set', 'true');
|
||||
} else {
|
||||
point.tag('app_name_set', 'false');
|
||||
}
|
||||
|
||||
// Add static tags from config file
|
||||
if (
|
||||
globals.config.has(
|
||||
'Butler-SOS.logEvents.enginePerformanceMonitor.trackRejectedEvents.tags'
|
||||
) &&
|
||||
globals.config.get(
|
||||
'Butler-SOS.logEvents.enginePerformanceMonitor.trackRejectedEvents.tags'
|
||||
) !== null &&
|
||||
globals.config.get(
|
||||
'Butler-SOS.logEvents.enginePerformanceMonitor.trackRejectedEvents.tags'
|
||||
).length > 0
|
||||
) {
|
||||
const configTags = globals.config.get(
|
||||
'Butler-SOS.logEvents.enginePerformanceMonitor.trackRejectedEvents.tags'
|
||||
);
|
||||
for (const item of configTags) {
|
||||
point.tag(item.name, item.value);
|
||||
}
|
||||
}
|
||||
|
||||
points.push(point);
|
||||
} else {
|
||||
const point = new Point(measurementName)
|
||||
.tag('source', event.source)
|
||||
.intField('counter', event.counter);
|
||||
|
||||
points.push(point);
|
||||
}
|
||||
}
|
||||
|
||||
await writeApi.writePoints(points);
|
||||
|
||||
globals.logger.verbose(
|
||||
'REJECTED EVENT COUNT V2: Sent rejected event count data to InfluxDB'
|
||||
);
|
||||
} catch (err) {
|
||||
globals.logger.error(`REJECTED EVENT COUNT V2: Error saving data: ${err}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
148
src/lib/influxdb/v2/health-metrics.js
Normal file
148
src/lib/influxdb/v2/health-metrics.js
Normal file
@@ -0,0 +1,148 @@
|
||||
import { Point } from '@influxdata/influxdb-client';
|
||||
import globals from '../../../globals.js';
|
||||
import { getFormattedTime, processAppDocuments } from '../shared/utils.js';
|
||||
|
||||
/**
|
||||
* Store health metrics from multiple Sense engines to InfluxDB v2
|
||||
*
|
||||
* @param {string} serverName - The name of the Qlik Sense server
|
||||
* @param {string} host - The hostname or IP of the Qlik Sense server
|
||||
* @param {object} body - Health metrics data from Sense engine
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function storeHealthMetricsV2(serverName, host, body) {
|
||||
try {
|
||||
// Find writeApi for the server specified by serverName
|
||||
const writeApi = globals.influxWriteApi.find(
|
||||
(element) => element.serverName === serverName
|
||||
);
|
||||
|
||||
if (!writeApi) {
|
||||
globals.logger.warn(
|
||||
`HEALTH METRICS V2: Influxdb write API object not found for host ${host}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Process app names for different document types
|
||||
const [appNamesActive, sessionAppNamesActive] = await processAppDocuments(
|
||||
body.apps.active_docs
|
||||
);
|
||||
const [appNamesLoaded, sessionAppNamesLoaded] = await processAppDocuments(
|
||||
body.apps.loaded_docs
|
||||
);
|
||||
const [appNamesInMemory, sessionAppNamesInMemory] = await processAppDocuments(
|
||||
body.apps.in_memory_docs
|
||||
);
|
||||
|
||||
const formattedTime = getFormattedTime(body.started);
|
||||
|
||||
// Create points using v2 Point class
|
||||
const points = [
|
||||
new Point('sense_server')
|
||||
.stringField('version', body.version)
|
||||
.stringField('started', body.started)
|
||||
.stringField('uptime', formattedTime),
|
||||
|
||||
new Point('mem')
|
||||
.floatField('comitted', body.mem.committed)
|
||||
.floatField('allocated', body.mem.allocated)
|
||||
.floatField('free', body.mem.free),
|
||||
|
||||
new Point('apps')
|
||||
.intField('active_docs_count', body.apps.active_docs.length)
|
||||
.intField('loaded_docs_count', body.apps.loaded_docs.length)
|
||||
.intField('in_memory_docs_count', body.apps.in_memory_docs.length)
|
||||
.stringField(
|
||||
'active_docs',
|
||||
globals.config.get('Butler-SOS.influxdbConfig.includeFields.activeDocs')
|
||||
? body.apps.active_docs
|
||||
: ''
|
||||
)
|
||||
.stringField(
|
||||
'active_docs_names',
|
||||
globals.config.get('Butler-SOS.appNames.enableAppNameExtract') &&
|
||||
globals.config.get('Butler-SOS.influxdbConfig.includeFields.activeDocs')
|
||||
? appNamesActive.toString()
|
||||
: ''
|
||||
)
|
||||
.stringField(
|
||||
'active_session_docs_names',
|
||||
globals.config.get('Butler-SOS.appNames.enableAppNameExtract') &&
|
||||
globals.config.get('Butler-SOS.influxdbConfig.includeFields.activeDocs')
|
||||
? sessionAppNamesActive.toString()
|
||||
: ''
|
||||
)
|
||||
.stringField(
|
||||
'loaded_docs',
|
||||
globals.config.get('Butler-SOS.influxdbConfig.includeFields.loadedDocs')
|
||||
? body.apps.loaded_docs
|
||||
: ''
|
||||
)
|
||||
.stringField(
|
||||
'loaded_docs_names',
|
||||
globals.config.get('Butler-SOS.appNames.enableAppNameExtract') &&
|
||||
globals.config.get('Butler-SOS.influxdbConfig.includeFields.loadedDocs')
|
||||
? appNamesLoaded.toString()
|
||||
: ''
|
||||
)
|
||||
.stringField(
|
||||
'loaded_session_docs_names',
|
||||
globals.config.get('Butler-SOS.appNames.enableAppNameExtract') &&
|
||||
globals.config.get('Butler-SOS.influxdbConfig.includeFields.loadedDocs')
|
||||
? sessionAppNamesLoaded.toString()
|
||||
: ''
|
||||
)
|
||||
.stringField(
|
||||
'in_memory_docs',
|
||||
globals.config.get('Butler-SOS.influxdbConfig.includeFields.inMemoryDocs')
|
||||
? body.apps.in_memory_docs
|
||||
: ''
|
||||
)
|
||||
.stringField(
|
||||
'in_memory_docs_names',
|
||||
globals.config.get('Butler-SOS.appNames.enableAppNameExtract') &&
|
||||
globals.config.get('Butler-SOS.influxdbConfig.includeFields.inMemoryDocs')
|
||||
? appNamesInMemory.toString()
|
||||
: ''
|
||||
)
|
||||
.stringField(
|
||||
'in_memory_session_docs_names',
|
||||
globals.config.get('Butler-SOS.appNames.enableAppNameExtract') &&
|
||||
globals.config.get('Butler-SOS.influxdbConfig.includeFields.inMemoryDocs')
|
||||
? sessionAppNamesInMemory.toString()
|
||||
: ''
|
||||
)
|
||||
.uintField('calls', body.apps.calls)
|
||||
.uintField('selections', body.apps.selections),
|
||||
|
||||
new Point('cpu').floatField('total', body.cpu.total),
|
||||
|
||||
new Point('session')
|
||||
.uintField('active', body.session.active)
|
||||
.uintField('total', body.session.total),
|
||||
|
||||
new Point('users')
|
||||
.uintField('active', body.users.active)
|
||||
.uintField('total', body.users.total),
|
||||
|
||||
new Point('cache')
|
||||
.uintField('hits', body.cache.hits)
|
||||
.uintField('lookups', body.cache.lookups)
|
||||
.intField('added', body.cache.added)
|
||||
.intField('replaced', body.cache.replaced)
|
||||
.intField('bytes_added', body.cache.bytes_added),
|
||||
|
||||
new Point('saturated').booleanField('saturated', body.saturated),
|
||||
];
|
||||
|
||||
await writeApi.writeAPI.writePoints(points);
|
||||
|
||||
globals.logger.verbose(`HEALTH METRICS V2: Stored health data from server: ${serverName}`);
|
||||
} catch (err) {
|
||||
globals.logger.error(
|
||||
`HEALTH METRICS V2: Error saving health data: ${globals.getErrorMessage(err)}`
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
197
src/lib/influxdb/v2/log-events.js
Normal file
197
src/lib/influxdb/v2/log-events.js
Normal file
@@ -0,0 +1,197 @@
|
||||
import { Point } from '@influxdata/influxdb-client';
|
||||
import globals from '../../../globals.js';
|
||||
|
||||
/**
|
||||
* Store log event to InfluxDB v2
|
||||
* Handles log events from different Sense sources
|
||||
*
|
||||
* @param {object} msg - Log event message
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function storeLogEventV2(msg) {
|
||||
try {
|
||||
globals.logger.debug(`LOG EVENT V2: ${JSON.stringify(msg)}`);
|
||||
|
||||
// Check if this is a supported source
|
||||
if (
|
||||
msg.source !== 'qseow-engine' &&
|
||||
msg.source !== 'qseow-proxy' &&
|
||||
msg.source !== 'qseow-scheduler' &&
|
||||
msg.source !== 'qseow-repository' &&
|
||||
msg.source !== 'qseow-qix-perf'
|
||||
) {
|
||||
globals.logger.warn(`LOG EVENT V2: Unsupported log event source: ${msg.source}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create write API with options
|
||||
const writeOptions = {
|
||||
flushInterval: 5000,
|
||||
maxRetries: 2,
|
||||
};
|
||||
|
||||
const org = globals.config.get('Butler-SOS.influxdbConfig.v2Config.org');
|
||||
const bucketName = globals.config.get('Butler-SOS.influxdbConfig.v2Config.bucket');
|
||||
|
||||
const writeApi = globals.influx.getWriteApi(org, bucketName, 'ns', writeOptions);
|
||||
|
||||
if (!writeApi) {
|
||||
globals.logger.warn('LOG EVENT V2: Influxdb write API object not found');
|
||||
return;
|
||||
}
|
||||
|
||||
let point;
|
||||
|
||||
// Process each source type
|
||||
if (msg.source === 'qseow-engine') {
|
||||
point = new Point('log_event')
|
||||
.tag('host', msg.host)
|
||||
.tag('level', msg.level)
|
||||
.tag('source', msg.source)
|
||||
.tag('log_row', msg.log_row)
|
||||
.tag('subsystem', msg.subsystem)
|
||||
.stringField('message', msg.message)
|
||||
.stringField('exception_message', msg.exception_message)
|
||||
.stringField('command', msg.command)
|
||||
.stringField('result_code', msg.result_code)
|
||||
.stringField('origin', msg.origin)
|
||||
.stringField('context', msg.context)
|
||||
.stringField('session_id', msg.session_id)
|
||||
.stringField('raw_event', JSON.stringify(msg));
|
||||
|
||||
// Tags that are empty in some cases. Only add if they are non-empty
|
||||
if (msg?.user_full?.length > 0) point.tag('user_full', msg.user_full);
|
||||
if (msg?.user_directory?.length > 0) point.tag('user_directory', msg.user_directory);
|
||||
if (msg?.user_id?.length > 0) point.tag('user_id', msg.user_id);
|
||||
if (msg?.result_code?.length > 0) point.tag('result_code', msg.result_code);
|
||||
if (msg?.windows_user?.length > 0) point.tag('windows_user', msg.windows_user);
|
||||
if (msg?.task_id?.length > 0) point.tag('task_id', msg.task_id);
|
||||
if (msg?.task_name?.length > 0) point.tag('task_name', msg.task_name);
|
||||
if (msg?.app_id?.length > 0) point.tag('app_id', msg.app_id);
|
||||
if (msg?.app_name?.length > 0) point.tag('app_name', msg.app_name);
|
||||
if (msg?.engine_exe_version?.length > 0)
|
||||
point.tag('engine_exe_version', msg.engine_exe_version);
|
||||
} else if (msg.source === 'qseow-proxy') {
|
||||
point = new Point('log_event')
|
||||
.tag('host', msg.host)
|
||||
.tag('level', msg.level)
|
||||
.tag('source', msg.source)
|
||||
.tag('log_row', msg.log_row)
|
||||
.tag('subsystem', msg.subsystem)
|
||||
.stringField('message', msg.message)
|
||||
.stringField('exception_message', msg.exception_message)
|
||||
.stringField('command', msg.command)
|
||||
.stringField('result_code', msg.result_code)
|
||||
.stringField('origin', msg.origin)
|
||||
.stringField('context', msg.context)
|
||||
.stringField('raw_event', JSON.stringify(msg));
|
||||
|
||||
// Tags that are empty in some cases. Only add if they are non-empty
|
||||
if (msg?.user_full?.length > 0) point.tag('user_full', msg.user_full);
|
||||
if (msg?.user_directory?.length > 0) point.tag('user_directory', msg.user_directory);
|
||||
if (msg?.user_id?.length > 0) point.tag('user_id', msg.user_id);
|
||||
if (msg?.result_code?.length > 0) point.tag('result_code', msg.result_code);
|
||||
} else if (msg.source === 'qseow-scheduler') {
|
||||
point = new Point('log_event')
|
||||
.tag('host', msg.host)
|
||||
.tag('level', msg.level)
|
||||
.tag('source', msg.source)
|
||||
.tag('log_row', msg.log_row)
|
||||
.tag('subsystem', msg.subsystem)
|
||||
.stringField('message', msg.message)
|
||||
.stringField('exception_message', msg.exception_message)
|
||||
.stringField('app_name', msg.app_name)
|
||||
.stringField('app_id', msg.app_id)
|
||||
.stringField('execution_id', msg.execution_id)
|
||||
.stringField('raw_event', JSON.stringify(msg));
|
||||
|
||||
// Tags that are empty in some cases. Only add if they are non-empty
|
||||
if (msg?.user_full?.length > 0) point.tag('user_full', msg.user_full);
|
||||
if (msg?.user_directory?.length > 0) point.tag('user_directory', msg.user_directory);
|
||||
if (msg?.user_id?.length > 0) point.tag('user_id', msg.user_id);
|
||||
if (msg?.task_id?.length > 0) point.tag('task_id', msg.task_id);
|
||||
if (msg?.task_name?.length > 0) point.tag('task_name', msg.task_name);
|
||||
} else if (msg.source === 'qseow-repository') {
|
||||
point = new Point('log_event')
|
||||
.tag('host', msg.host)
|
||||
.tag('level', msg.level)
|
||||
.tag('source', msg.source)
|
||||
.tag('log_row', msg.log_row)
|
||||
.tag('subsystem', msg.subsystem)
|
||||
.stringField('message', msg.message)
|
||||
.stringField('exception_message', msg.exception_message)
|
||||
.stringField('command', msg.command)
|
||||
.stringField('result_code', msg.result_code)
|
||||
.stringField('origin', msg.origin)
|
||||
.stringField('context', msg.context)
|
||||
.stringField('raw_event', JSON.stringify(msg));
|
||||
|
||||
// Tags that are empty in some cases. Only add if they are non-empty
|
||||
if (msg?.user_full?.length > 0) point.tag('user_full', msg.user_full);
|
||||
if (msg?.user_directory?.length > 0) point.tag('user_directory', msg.user_directory);
|
||||
if (msg?.user_id?.length > 0) point.tag('user_id', msg.user_id);
|
||||
if (msg?.result_code?.length > 0) point.tag('result_code', msg.result_code);
|
||||
} else if (msg.source === 'qseow-qix-perf') {
|
||||
point = new Point('log_event')
|
||||
.tag('host', msg.host)
|
||||
.tag('level', msg.level)
|
||||
.tag('source', msg.source)
|
||||
.tag('log_row', msg.log_row)
|
||||
.tag('subsystem', msg.subsystem)
|
||||
.tag('method', msg.method)
|
||||
.tag('object_type', msg.object_type)
|
||||
.tag('proxy_session_id', msg.proxy_session_id)
|
||||
.tag('session_id', msg.session_id)
|
||||
.tag('event_activity_source', msg.event_activity_source)
|
||||
.stringField('app_id', msg.app_id)
|
||||
.floatField('process_time', parseFloat(msg.process_time))
|
||||
.floatField('work_time', parseFloat(msg.work_time))
|
||||
.floatField('lock_time', parseFloat(msg.lock_time))
|
||||
.floatField('validate_time', parseFloat(msg.validate_time))
|
||||
.floatField('traverse_time', parseFloat(msg.traverse_time))
|
||||
.stringField('handle', msg.handle)
|
||||
.intField('net_ram', parseInt(msg.net_ram))
|
||||
.intField('peak_ram', parseInt(msg.peak_ram))
|
||||
.stringField('raw_event', JSON.stringify(msg));
|
||||
|
||||
// Tags that are empty in some cases. Only add if they are non-empty
|
||||
if (msg?.user_full?.length > 0) point.tag('user_full', msg.user_full);
|
||||
if (msg?.user_directory?.length > 0) point.tag('user_directory', msg.user_directory);
|
||||
if (msg?.user_id?.length > 0) point.tag('user_id', msg.user_id);
|
||||
if (msg?.app_id?.length > 0) point.tag('app_id', msg.app_id);
|
||||
if (msg?.app_name?.length > 0) point.tag('app_name', msg.app_name);
|
||||
if (msg?.object_id?.length > 0) point.tag('object_id', msg.object_id);
|
||||
}
|
||||
|
||||
// Add log event categories to tags if available
|
||||
// The msg.category array contains objects with properties 'name' and 'value'
|
||||
if (msg?.category?.length > 0) {
|
||||
msg.category.forEach((category) => {
|
||||
point.tag(category.name, category.value);
|
||||
});
|
||||
}
|
||||
|
||||
// Add custom tags from config file to payload
|
||||
if (
|
||||
globals.config.has('Butler-SOS.logEvents.tags') &&
|
||||
globals.config.get('Butler-SOS.logEvents.tags') !== null &&
|
||||
globals.config.get('Butler-SOS.logEvents.tags').length > 0
|
||||
) {
|
||||
const configTags = globals.config.get('Butler-SOS.logEvents.tags');
|
||||
for (const item of configTags) {
|
||||
point.tag(item.name, item.value);
|
||||
}
|
||||
}
|
||||
|
||||
globals.logger.silly(`LOG EVENT V2: Influxdb datapoint: ${JSON.stringify(point, null, 2)}`);
|
||||
|
||||
await writeApi.writePoint(point);
|
||||
|
||||
globals.logger.verbose('LOG EVENT V2: Sent log event data to InfluxDB');
|
||||
} catch (err) {
|
||||
globals.logger.error(
|
||||
`LOG EVENT V2: Error saving log event: ${globals.getErrorMessage(err)}`
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
174
src/lib/influxdb/v2/queue-metrics.js
Normal file
174
src/lib/influxdb/v2/queue-metrics.js
Normal file
@@ -0,0 +1,174 @@
|
||||
import { Point } from '@influxdata/influxdb-client';
|
||||
import globals from '../../../globals.js';
|
||||
|
||||
/**
|
||||
* Store user event queue metrics to InfluxDB v2
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function storeUserEventQueueMetricsV2() {
|
||||
try {
|
||||
// Check if queue metrics are enabled
|
||||
if (
|
||||
!globals.config.get(
|
||||
'Butler-SOS.userEvents.udpServerConfig.queueMetrics.influxdb.enable'
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get metrics from queue manager
|
||||
const queueManager = globals.udpQueueManagerUserActivity;
|
||||
if (!queueManager) {
|
||||
globals.logger.warn('USER EVENT QUEUE METRICS V2: Queue manager not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
const metrics = await queueManager.getMetrics();
|
||||
|
||||
// Get configuration
|
||||
const measurementName = globals.config.get(
|
||||
'Butler-SOS.userEvents.udpServerConfig.queueMetrics.influxdb.measurementName'
|
||||
);
|
||||
const configTags = globals.config.get(
|
||||
'Butler-SOS.userEvents.udpServerConfig.queueMetrics.influxdb.tags'
|
||||
);
|
||||
|
||||
// Create write API with options
|
||||
const writeOptions = {
|
||||
flushInterval: 5000,
|
||||
maxRetries: 2,
|
||||
};
|
||||
|
||||
const org = globals.config.get('Butler-SOS.influxdbConfig.v2Config.org');
|
||||
const bucketName = globals.config.get('Butler-SOS.influxdbConfig.v2Config.bucket');
|
||||
|
||||
const writeApi = globals.influx.getWriteApi(org, bucketName, 'ns', writeOptions);
|
||||
|
||||
if (!writeApi) {
|
||||
globals.logger.warn('USER EVENT QUEUE METRICS V2: Influxdb write API object not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const point = new Point(measurementName)
|
||||
.tag('queue_type', 'user_events')
|
||||
.tag('host', globals.hostInfo.hostname)
|
||||
.intField('queue_size', metrics.queueSize)
|
||||
.intField('queue_max_size', metrics.queueMaxSize)
|
||||
.floatField('queue_utilization_pct', metrics.queueUtilizationPct)
|
||||
.intField('queue_pending', metrics.queuePending)
|
||||
.intField('messages_received', metrics.messagesReceived)
|
||||
.intField('messages_queued', metrics.messagesQueued)
|
||||
.intField('messages_processed', metrics.messagesProcessed)
|
||||
.intField('messages_failed', metrics.messagesFailed)
|
||||
.intField('messages_dropped_total', metrics.messagesDroppedTotal)
|
||||
.intField('messages_dropped_rate_limit', metrics.messagesDroppedRateLimit)
|
||||
.intField('messages_dropped_queue_full', metrics.messagesDroppedQueueFull)
|
||||
.intField('messages_dropped_size', metrics.messagesDroppedSize)
|
||||
.floatField('processing_time_avg_ms', metrics.processingTimeAvgMs)
|
||||
.floatField('processing_time_p95_ms', metrics.processingTimeP95Ms)
|
||||
.floatField('processing_time_max_ms', metrics.processingTimeMaxMs)
|
||||
.intField('rate_limit_current', metrics.rateLimitCurrent)
|
||||
.intField('backpressure_active', metrics.backpressureActive);
|
||||
|
||||
// Add static tags from config file
|
||||
if (configTags && configTags.length > 0) {
|
||||
for (const item of configTags) {
|
||||
point.tag(item.name, item.value);
|
||||
}
|
||||
}
|
||||
|
||||
writeApi.writePoint(point);
|
||||
await writeApi.close();
|
||||
|
||||
globals.logger.verbose('USER EVENT QUEUE METRICS V2: Sent queue metrics data to InfluxDB');
|
||||
} catch (err) {
|
||||
globals.logger.error(`USER EVENT QUEUE METRICS V2: Error saving data: ${err}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store log event queue metrics to InfluxDB v2
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function storeLogEventQueueMetricsV2() {
|
||||
try {
|
||||
// Check if queue metrics are enabled
|
||||
if (
|
||||
!globals.config.get('Butler-SOS.logEvents.udpServerConfig.queueMetrics.influxdb.enable')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get metrics from queue manager
|
||||
const queueManager = globals.udpQueueManagerLogEvents;
|
||||
if (!queueManager) {
|
||||
globals.logger.warn('LOG EVENT QUEUE METRICS V2: Queue manager not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
const metrics = await queueManager.getMetrics();
|
||||
|
||||
// Get configuration
|
||||
const measurementName = globals.config.get(
|
||||
'Butler-SOS.logEvents.udpServerConfig.queueMetrics.influxdb.measurementName'
|
||||
);
|
||||
const configTags = globals.config.get(
|
||||
'Butler-SOS.logEvents.udpServerConfig.queueMetrics.influxdb.tags'
|
||||
);
|
||||
|
||||
// Create write API with options
|
||||
const writeOptions = {
|
||||
flushInterval: 5000,
|
||||
maxRetries: 2,
|
||||
};
|
||||
|
||||
const org = globals.config.get('Butler-SOS.influxdbConfig.v2Config.org');
|
||||
const bucketName = globals.config.get('Butler-SOS.influxdbConfig.v2Config.bucket');
|
||||
|
||||
const writeApi = globals.influx.getWriteApi(org, bucketName, 'ns', writeOptions);
|
||||
|
||||
if (!writeApi) {
|
||||
globals.logger.warn('LOG EVENT QUEUE METRICS V2: Influxdb write API object not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const point = new Point(measurementName)
|
||||
.tag('queue_type', 'log_events')
|
||||
.tag('host', globals.hostInfo.hostname)
|
||||
.intField('queue_size', metrics.queueSize)
|
||||
.intField('queue_max_size', metrics.queueMaxSize)
|
||||
.floatField('queue_utilization_pct', metrics.queueUtilizationPct)
|
||||
.intField('queue_pending', metrics.queuePending)
|
||||
.intField('messages_received', metrics.messagesReceived)
|
||||
.intField('messages_queued', metrics.messagesQueued)
|
||||
.intField('messages_processed', metrics.messagesProcessed)
|
||||
.intField('messages_failed', metrics.messagesFailed)
|
||||
.intField('messages_dropped_total', metrics.messagesDroppedTotal)
|
||||
.intField('messages_dropped_rate_limit', metrics.messagesDroppedRateLimit)
|
||||
.intField('messages_dropped_queue_full', metrics.messagesDroppedQueueFull)
|
||||
.intField('messages_dropped_size', metrics.messagesDroppedSize)
|
||||
.floatField('processing_time_avg_ms', metrics.processingTimeAvgMs)
|
||||
.floatField('processing_time_p95_ms', metrics.processingTimeP95Ms)
|
||||
.floatField('processing_time_max_ms', metrics.processingTimeMaxMs)
|
||||
.intField('rate_limit_current', metrics.rateLimitCurrent)
|
||||
.intField('backpressure_active', metrics.backpressureActive);
|
||||
|
||||
// Add static tags from config file
|
||||
if (configTags && configTags.length > 0) {
|
||||
for (const item of configTags) {
|
||||
point.tag(item.name, item.value);
|
||||
}
|
||||
}
|
||||
|
||||
writeApi.writePoint(point);
|
||||
await writeApi.close();
|
||||
|
||||
globals.logger.verbose('LOG EVENT QUEUE METRICS V2: Sent queue metrics data to InfluxDB');
|
||||
} catch (err) {
|
||||
globals.logger.error(`LOG EVENT QUEUE METRICS V2: Error saving data: ${err}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
44
src/lib/influxdb/v2/sessions.js
Normal file
44
src/lib/influxdb/v2/sessions.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import globals from '../../../globals.js';
|
||||
|
||||
/**
|
||||
* Store proxy session data to InfluxDB v2
|
||||
*
|
||||
* @param {object} userSessions - User session data including datapointInfluxdb array
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function storeSessionsV2(userSessions) {
|
||||
try {
|
||||
// Find writeApi for the server specified by serverName
|
||||
const writeApi = globals.influxWriteApi.find(
|
||||
(element) => element.serverName === userSessions.serverName
|
||||
);
|
||||
|
||||
if (!writeApi) {
|
||||
globals.logger.warn(
|
||||
`PROXY SESSIONS V2: Influxdb write API object not found for host ${userSessions.host}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
globals.logger.silly(
|
||||
`PROXY SESSIONS V2: Influxdb datapoint for server "${userSessions.host}", virtual proxy "${userSessions.virtualProxy}": ${JSON.stringify(
|
||||
userSessions.datapointInfluxdb,
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
);
|
||||
|
||||
// Data points are already in InfluxDB v2 format (Point objects)
|
||||
// Write array of measurements: user_session_summary, user_session_list, user_session_details
|
||||
await writeApi.writeAPI.writePoints(userSessions.datapointInfluxdb);
|
||||
|
||||
globals.logger.verbose(
|
||||
`PROXY SESSIONS V2: Sent user session data to InfluxDB for server "${userSessions.host}", virtual proxy "${userSessions.virtualProxy}"`
|
||||
);
|
||||
} catch (err) {
|
||||
globals.logger.error(
|
||||
`PROXY SESSIONS V2: Error saving user session data: ${globals.getErrorMessage(err)}`
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
80
src/lib/influxdb/v2/user-events.js
Normal file
80
src/lib/influxdb/v2/user-events.js
Normal file
@@ -0,0 +1,80 @@
|
||||
import { Point } from '@influxdata/influxdb-client';
|
||||
import globals from '../../../globals.js';
|
||||
|
||||
/**
|
||||
* Store user event to InfluxDB v2
|
||||
*
|
||||
* @param {object} msg - User event message
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function storeUserEventV2(msg) {
|
||||
try {
|
||||
globals.logger.debug(`USER EVENT V2: ${JSON.stringify(msg)}`);
|
||||
|
||||
// Create write API with options
|
||||
const writeOptions = {
|
||||
flushInterval: 5000,
|
||||
maxRetries: 2,
|
||||
};
|
||||
|
||||
const org = globals.config.get('Butler-SOS.influxdbConfig.v2Config.org');
|
||||
const bucketName = globals.config.get('Butler-SOS.influxdbConfig.v2Config.bucket');
|
||||
|
||||
const writeApi = globals.influx.getWriteApi(org, bucketName, 'ns', writeOptions);
|
||||
|
||||
if (!writeApi) {
|
||||
globals.logger.warn('USER EVENT V2: Influxdb write API object not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create point using v2 Point class
|
||||
const point = new Point('user_events')
|
||||
.tag('host', msg.host)
|
||||
.tag('event_action', msg.command)
|
||||
.tag('userFull', `${msg.user_directory}\\${msg.user_id}`)
|
||||
.tag('userDirectory', msg.user_directory)
|
||||
.tag('userId', msg.user_id)
|
||||
.tag('origin', msg.origin)
|
||||
.stringField('userFull', `${msg.user_directory}\\${msg.user_id}`)
|
||||
.stringField('userId', msg.user_id);
|
||||
|
||||
// Add app id and name to tags if available
|
||||
if (msg?.appId) point.tag('appId', msg.appId);
|
||||
if (msg?.appName) point.tag('appName', msg.appName);
|
||||
|
||||
// Add user agent info to tags if available
|
||||
if (msg?.ua?.browser?.name) point.tag('uaBrowserName', msg?.ua?.browser?.name);
|
||||
if (msg?.ua?.browser?.major) point.tag('uaBrowserMajorVersion', msg?.ua?.browser?.major);
|
||||
if (msg?.ua?.os?.name) point.tag('uaOsName', msg?.ua?.os?.name);
|
||||
if (msg?.ua?.os?.version) point.tag('uaOsVersion', msg?.ua?.os?.version);
|
||||
|
||||
// Add custom tags from config file to payload
|
||||
if (
|
||||
globals.config.has('Butler-SOS.userEvents.tags') &&
|
||||
globals.config.get('Butler-SOS.userEvents.tags') !== null &&
|
||||
globals.config.get('Butler-SOS.userEvents.tags').length > 0
|
||||
) {
|
||||
const configTags = globals.config.get('Butler-SOS.userEvents.tags');
|
||||
for (const item of configTags) {
|
||||
point.tag(item.name, item.value);
|
||||
}
|
||||
}
|
||||
|
||||
// Add app id and name to fields if available
|
||||
if (msg?.appId) point.stringField('appId', msg.appId);
|
||||
if (msg?.appName) point.stringField('appName', msg.appName);
|
||||
|
||||
globals.logger.silly(
|
||||
`USER EVENT V2: Influxdb datapoint: ${JSON.stringify(point, null, 2)}`
|
||||
);
|
||||
|
||||
await writeApi.writePoint(point);
|
||||
|
||||
globals.logger.verbose('USER EVENT V2: Sent user event data to InfluxDB');
|
||||
} catch (err) {
|
||||
globals.logger.error(
|
||||
`USER EVENT V2: Error saving user event: ${globals.getErrorMessage(err)}`
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
23
src/lib/influxdb/v3/__tests__/health-metrics.test.js
Normal file
23
src/lib/influxdb/v3/__tests__/health-metrics.test.js
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Tests for v3 health metrics module
|
||||
*
|
||||
* Note: These tests are skipped due to complex ES module mocking requirements.
|
||||
* Full integration tests with actual InfluxDB connections are performed separately.
|
||||
* The refactored code is functionally tested through the main post-to-influxdb tests.
|
||||
*/
|
||||
|
||||
import { jest } from '@jest/globals';
|
||||
|
||||
describe.skip('v3/health-metrics', () => {
|
||||
test('module exports postHealthMetricsToInfluxdbV3 function', async () => {
|
||||
const healthMetrics = await import('../health-metrics.js');
|
||||
expect(healthMetrics.postHealthMetricsToInfluxdbV3).toBeDefined();
|
||||
expect(typeof healthMetrics.postHealthMetricsToInfluxdbV3).toBe('function');
|
||||
});
|
||||
|
||||
test('module can be imported without errors', async () => {
|
||||
expect(async () => {
|
||||
await import('../health-metrics.js');
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
52
src/lib/influxdb/v3/butler-memory.js
Normal file
52
src/lib/influxdb/v3/butler-memory.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Point as Point3 } from '@influxdata/influxdb3-client';
|
||||
import globals from '../../../globals.js';
|
||||
import { isInfluxDbEnabled } from '../shared/utils.js';
|
||||
|
||||
/**
|
||||
* Posts Butler SOS memory usage metrics to InfluxDB v3.
|
||||
*
|
||||
* This function captures memory usage metrics from the Butler SOS process itself
|
||||
* and stores them in InfluxDB v3.
|
||||
*
|
||||
* @param {object} memory - Memory usage data object
|
||||
* @param {string} memory.instanceTag - Instance identifier tag
|
||||
* @param {number} memory.heapUsedMByte - Heap used in MB
|
||||
* @param {number} memory.heapTotalMByte - Total heap size in MB
|
||||
* @param {number} memory.externalMemoryMByte - External memory usage in MB
|
||||
* @param {number} memory.processMemoryMByte - Process memory usage in MB
|
||||
* @returns {Promise<void>} Promise that resolves when data has been posted to InfluxDB
|
||||
*/
|
||||
export async function postButlerSOSMemoryUsageToInfluxdbV3(memory) {
|
||||
globals.logger.debug(`MEMORY USAGE V3: Memory usage ${JSON.stringify(memory, null, 2)})`);
|
||||
|
||||
// Get Butler version
|
||||
const butlerVersion = globals.appVersion;
|
||||
|
||||
// Only write to InfluxDB if the global influx object has been initialized
|
||||
if (!isInfluxDbEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const database = globals.config.get('Butler-SOS.influxdbConfig.v3Config.database');
|
||||
|
||||
// Create point for v3
|
||||
const point = new Point3('butlersos_memory_usage')
|
||||
.setTag('butler_sos_instance', memory.instanceTag)
|
||||
.setTag('version', butlerVersion)
|
||||
.setFloatField('heap_used', memory.heapUsedMByte)
|
||||
.setFloatField('heap_total', memory.heapTotalMByte)
|
||||
.setFloatField('external', memory.externalMemoryMByte)
|
||||
.setFloatField('process_memory', memory.processMemoryMByte);
|
||||
|
||||
try {
|
||||
// Convert point to line protocol and write directly
|
||||
await globals.influx.write(point.toLineProtocol(), database);
|
||||
globals.logger.debug(`MEMORY USAGE V3: Wrote data to InfluxDB v3`);
|
||||
} catch (err) {
|
||||
globals.logger.error(
|
||||
`MEMORY USAGE V3: Error saving memory usage data to InfluxDB v3! ${globals.getErrorMessage(err)}`
|
||||
);
|
||||
}
|
||||
|
||||
globals.logger.verbose('MEMORY USAGE V3: Sent Butler SOS memory usage data to InfluxDB');
|
||||
}
|
||||
249
src/lib/influxdb/v3/event-counts.js
Normal file
249
src/lib/influxdb/v3/event-counts.js
Normal file
@@ -0,0 +1,249 @@
|
||||
import { Point as Point3 } from '@influxdata/influxdb3-client';
|
||||
import globals from '../../../globals.js';
|
||||
import { isInfluxDbEnabled } from '../shared/utils.js';
|
||||
|
||||
/**
|
||||
* Store event count in InfluxDB v3
|
||||
*
|
||||
* @description
|
||||
* This function reads arrays of log and user events from the `udpEvents` object,
|
||||
* and stores the data in InfluxDB v3. The data is written to a measurement named after
|
||||
* the `Butler-SOS.qlikSenseEvents.eventCount.influxdb.measurementName` config setting.
|
||||
*
|
||||
* @returns {Promise<void>} Promise that resolves when data has been posted to InfluxDB
|
||||
* @throws {Error} Error if unable to write data to InfluxDB
|
||||
*/
|
||||
export async function storeEventCountInfluxDBV3() {
|
||||
// Get array of log events
|
||||
const logEvents = await globals.udpEvents.getLogEvents();
|
||||
const userEvents = await globals.udpEvents.getUserEvents();
|
||||
|
||||
// Debug
|
||||
globals.logger.debug(
|
||||
`EVENT COUNT INFLUXDB V3: Log events: ${JSON.stringify(logEvents, null, 2)}`
|
||||
);
|
||||
globals.logger.debug(
|
||||
`EVENT COUNT INFLUXDB V3: User events: ${JSON.stringify(userEvents, null, 2)}`
|
||||
);
|
||||
|
||||
// Are there any events to store?
|
||||
if (logEvents.length === 0 && userEvents.length === 0) {
|
||||
globals.logger.verbose('EVENT COUNT INFLUXDB V3: No events to store in InfluxDB');
|
||||
return;
|
||||
}
|
||||
|
||||
// Only write to InfluxDB if the global influx object has been initialized
|
||||
if (!isInfluxDbEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const database = globals.config.get('Butler-SOS.influxdbConfig.v3Config.database');
|
||||
|
||||
try {
|
||||
// Store data for each log event
|
||||
for (const logEvent of logEvents) {
|
||||
const tags = {
|
||||
butler_sos_instance: globals.options.instanceTag,
|
||||
event_type: 'log',
|
||||
source: logEvent.source,
|
||||
host: logEvent.host,
|
||||
subsystem: logEvent.subsystem,
|
||||
};
|
||||
|
||||
// Add static tags defined in config file, if any
|
||||
if (
|
||||
globals.config.has('Butler-SOS.qlikSenseEvents.eventCount.influxdb.tags') &&
|
||||
Array.isArray(
|
||||
globals.config.get('Butler-SOS.qlikSenseEvents.eventCount.influxdb.tags')
|
||||
)
|
||||
) {
|
||||
const configTags = globals.config.get(
|
||||
'Butler-SOS.qlikSenseEvents.eventCount.influxdb.tags'
|
||||
);
|
||||
|
||||
configTags.forEach((tag) => {
|
||||
tags[tag.name] = tag.value;
|
||||
});
|
||||
}
|
||||
|
||||
const point = new Point3(
|
||||
globals.config.get('Butler-SOS.qlikSenseEvents.eventCount.influxdb.measurementName')
|
||||
)
|
||||
.setTag('event_type', 'log')
|
||||
.setTag('source', logEvent.source)
|
||||
.setTag('host', logEvent.host)
|
||||
.setTag('subsystem', logEvent.subsystem)
|
||||
.setIntegerField('counter', logEvent.counter);
|
||||
|
||||
// Add additional tags to point
|
||||
Object.keys(tags).forEach((key) => {
|
||||
point.setTag(key, tags[key]);
|
||||
});
|
||||
|
||||
await globals.influx.write(point.toLineProtocol(), database);
|
||||
globals.logger.debug(`EVENT COUNT INFLUXDB V3: Wrote log event data to InfluxDB v3`);
|
||||
}
|
||||
|
||||
// Loop through data in user events and create datapoints
|
||||
for (const event of userEvents) {
|
||||
const tags = {
|
||||
butler_sos_instance: globals.options.instanceTag,
|
||||
event_type: 'user',
|
||||
source: event.source,
|
||||
host: event.host,
|
||||
subsystem: event.subsystem,
|
||||
};
|
||||
|
||||
// Add static tags defined in config file, if any
|
||||
if (
|
||||
globals.config.has('Butler-SOS.qlikSenseEvents.eventCount.influxdb.tags') &&
|
||||
Array.isArray(
|
||||
globals.config.get('Butler-SOS.qlikSenseEvents.eventCount.influxdb.tags')
|
||||
)
|
||||
) {
|
||||
const configTags = globals.config.get(
|
||||
'Butler-SOS.qlikSenseEvents.eventCount.influxdb.tags'
|
||||
);
|
||||
|
||||
configTags.forEach((tag) => {
|
||||
tags[tag.name] = tag.value;
|
||||
});
|
||||
}
|
||||
|
||||
const point = new Point3(
|
||||
globals.config.get('Butler-SOS.qlikSenseEvents.eventCount.influxdb.measurementName')
|
||||
)
|
||||
.setTag('event_type', 'user')
|
||||
.setTag('source', event.source)
|
||||
.setTag('host', event.host)
|
||||
.setTag('subsystem', event.subsystem)
|
||||
.setIntegerField('counter', event.counter);
|
||||
|
||||
// Add additional tags to point
|
||||
Object.keys(tags).forEach((key) => {
|
||||
point.setTag(key, tags[key]);
|
||||
});
|
||||
|
||||
await globals.influx.write(point.toLineProtocol(), database);
|
||||
globals.logger.debug(`EVENT COUNT INFLUXDB V3: Wrote user event data to InfluxDB v3`);
|
||||
}
|
||||
|
||||
globals.logger.verbose(
|
||||
'EVENT COUNT INFLUXDB V3: Sent Butler SOS event count data to InfluxDB'
|
||||
);
|
||||
} catch (err) {
|
||||
globals.logger.error(
|
||||
`EVENT COUNT INFLUXDB V3: Error writing data to InfluxDB: ${globals.getErrorMessage(err)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store rejected event count in InfluxDB v3
|
||||
*
|
||||
* @description
|
||||
* This function reads an array of rejected log events from the `rejectedEvents` object,
|
||||
* and stores the data in InfluxDB v3. The data is written to a measurement named after
|
||||
* the `Butler-SOS.qlikSenseEvents.rejectedEventCount.influxdb.measurementName` config setting.
|
||||
*
|
||||
* @returns {Promise<void>} Promise that resolves when data has been posted to InfluxDB
|
||||
* @throws {Error} Error if unable to write data to InfluxDB
|
||||
*/
|
||||
export async function storeRejectedEventCountInfluxDBV3() {
|
||||
// Get array of rejected log events
|
||||
const rejectedLogEvents = await globals.rejectedEvents.getRejectedLogEvents();
|
||||
|
||||
// Debug
|
||||
globals.logger.debug(
|
||||
`REJECTED EVENT COUNT INFLUXDB V3: Rejected log events: ${JSON.stringify(
|
||||
rejectedLogEvents,
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
);
|
||||
|
||||
// Are there any events to store?
|
||||
if (rejectedLogEvents.length === 0) {
|
||||
globals.logger.verbose('REJECTED EVENT COUNT INFLUXDB V3: No events to store in InfluxDB');
|
||||
return;
|
||||
}
|
||||
|
||||
// Only write to InfluxDB if the global influx object has been initialized
|
||||
if (!isInfluxDbEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const database = globals.config.get('Butler-SOS.influxdbConfig.v3Config.database');
|
||||
|
||||
try {
|
||||
const points = [];
|
||||
const measurementName = globals.config.get(
|
||||
'Butler-SOS.qlikSenseEvents.rejectedEventCount.influxdb.measurementName'
|
||||
);
|
||||
|
||||
rejectedLogEvents.forEach((event) => {
|
||||
globals.logger.debug(`REJECTED LOG EVENT INFLUXDB V3: ${JSON.stringify(event)}`);
|
||||
|
||||
if (event.source === 'qseow-qix-perf') {
|
||||
let point = new Point3(measurementName)
|
||||
.setTag('source', event.source)
|
||||
.setTag('object_type', event.objectType)
|
||||
.setTag('method', event.method)
|
||||
.setIntegerField('counter', event.counter)
|
||||
.setFloatField('process_time', event.processTime);
|
||||
|
||||
// Add app_id and app_name if available
|
||||
if (event?.appId) {
|
||||
point.setTag('app_id', event.appId);
|
||||
}
|
||||
if (event?.appName?.length > 0) {
|
||||
point.setTag('app_name', event.appName);
|
||||
point.setTag('app_name_set', 'true');
|
||||
} else {
|
||||
point.setTag('app_name_set', 'false');
|
||||
}
|
||||
|
||||
// Add static tags defined in config file, if any
|
||||
if (
|
||||
globals.config.has(
|
||||
'Butler-SOS.logEvents.enginePerformanceMonitor.trackRejectedEvents.tags'
|
||||
) &&
|
||||
Array.isArray(
|
||||
globals.config.get(
|
||||
'Butler-SOS.logEvents.enginePerformanceMonitor.trackRejectedEvents.tags'
|
||||
)
|
||||
)
|
||||
) {
|
||||
const configTags = globals.config.get(
|
||||
'Butler-SOS.logEvents.enginePerformanceMonitor.trackRejectedEvents.tags'
|
||||
);
|
||||
for (const item of configTags) {
|
||||
point.setTag(item.name, item.value);
|
||||
}
|
||||
}
|
||||
|
||||
points.push(point);
|
||||
} else {
|
||||
let point = new Point3(measurementName)
|
||||
.setTag('source', event.source)
|
||||
.setIntegerField('counter', event.counter);
|
||||
|
||||
points.push(point);
|
||||
}
|
||||
});
|
||||
|
||||
// Write to InfluxDB
|
||||
for (const point of points) {
|
||||
await globals.influx.write(point.toLineProtocol(), database);
|
||||
}
|
||||
globals.logger.debug(`REJECT LOG EVENT INFLUXDB V3: Wrote data to InfluxDB v3`);
|
||||
|
||||
globals.logger.verbose(
|
||||
'REJECT LOG EVENT INFLUXDB V3: Sent Butler SOS rejected event count data to InfluxDB'
|
||||
);
|
||||
} catch (err) {
|
||||
globals.logger.error(
|
||||
`REJECTED LOG EVENT INFLUXDB V3: Error writing data to InfluxDB: ${globals.getErrorMessage(err)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
204
src/lib/influxdb/v3/health-metrics.js
Normal file
204
src/lib/influxdb/v3/health-metrics.js
Normal file
@@ -0,0 +1,204 @@
|
||||
import { Point as Point3 } from '@influxdata/influxdb3-client';
|
||||
import globals from '../../../globals.js';
|
||||
import {
|
||||
getFormattedTime,
|
||||
processAppDocuments,
|
||||
isInfluxDbEnabled,
|
||||
applyTagsToPoint3,
|
||||
} from '../shared/utils.js';
|
||||
|
||||
/**
|
||||
* Posts health metrics data from Qlik Sense to InfluxDB v3.
|
||||
*
|
||||
* This function processes health data from the Sense engine's healthcheck API and
|
||||
* formats it for storage in InfluxDB v3. It handles various metrics including:
|
||||
* - CPU usage
|
||||
* - Memory usage
|
||||
* - Cache metrics
|
||||
* - Active/loaded/in-memory apps
|
||||
* - Session counts
|
||||
* - User counts
|
||||
*
|
||||
* @param {string} serverName - The name of the Qlik Sense server
|
||||
* @param {string} host - The hostname or IP of the Qlik Sense server
|
||||
* @param {object} body - The health metrics data from Sense engine healthcheck API
|
||||
* @param {object} serverTags - Tags to associate with the metrics
|
||||
* @returns {Promise<void>} Promise that resolves when data has been posted to InfluxDB
|
||||
*/
|
||||
export async function postHealthMetricsToInfluxdbV3(serverName, host, body, serverTags) {
|
||||
// Calculate server uptime
|
||||
const formattedTime = getFormattedTime(body.started);
|
||||
|
||||
// Build tags structure that will be passed to InfluxDB
|
||||
globals.logger.debug(
|
||||
`HEALTH METRICS TO INFLUXDB V3: Health data: Tags sent to InfluxDB: ${JSON.stringify(
|
||||
serverTags
|
||||
)}`
|
||||
);
|
||||
|
||||
globals.logger.debug(
|
||||
`HEALTH METRICS TO INFLUXDB V3: Number of apps active: ${body.apps.active_docs.length}`
|
||||
);
|
||||
globals.logger.debug(
|
||||
`HEALTH METRICS TO INFLUXDB V3: Number of apps loaded: ${body.apps.loaded_docs.length}`
|
||||
);
|
||||
globals.logger.debug(
|
||||
`HEALTH METRICS TO INFLUXDB V3: Number of apps in memory: ${body.apps.in_memory_docs.length}`
|
||||
);
|
||||
|
||||
// Get active app names
|
||||
const { appNames: appNamesActive, sessionAppNames: sessionAppNamesActive } =
|
||||
await processAppDocuments(body.apps.active_docs, 'HEALTH METRICS TO INFLUXDB V3', 'active');
|
||||
|
||||
// Get loaded app names
|
||||
const { appNames: appNamesLoaded, sessionAppNames: sessionAppNamesLoaded } =
|
||||
await processAppDocuments(body.apps.loaded_docs, 'HEALTH METRICS TO INFLUXDB V3', 'loaded');
|
||||
|
||||
// Get in memory app names
|
||||
const { appNames: appNamesInMemory, sessionAppNames: sessionAppNamesInMemory } =
|
||||
await processAppDocuments(
|
||||
body.apps.in_memory_docs,
|
||||
'HEALTH METRICS TO INFLUXDB V3',
|
||||
'in memory'
|
||||
);
|
||||
|
||||
// Only write to InfluxDB if the global influx object has been initialized
|
||||
if (!isInfluxDbEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only write to InfluxDB if the global influxWriteApi object has been initialized
|
||||
if (!globals.influxWriteApi) {
|
||||
globals.logger.warn(
|
||||
'HEALTH METRICS V3: Influxdb write API object not initialized. Data will not be sent to InfluxDB'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find writeApi for the server specified by serverName
|
||||
const writeApi = globals.influxWriteApi.find((element) => element.serverName === serverName);
|
||||
|
||||
// Ensure that the writeApi object was found
|
||||
if (!writeApi) {
|
||||
globals.logger.warn(
|
||||
`HEALTH METRICS V3: Influxdb write API object not found for host ${host}. Data will not be sent to InfluxDB`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get database from config
|
||||
const database = globals.config.get('Butler-SOS.influxdbConfig.v3Config.database');
|
||||
|
||||
// Create a new point with the data to be written to InfluxDB v3
|
||||
const points = [
|
||||
new Point3('sense_server')
|
||||
.setStringField('version', body.version)
|
||||
.setStringField('started', body.started)
|
||||
.setStringField('uptime', formattedTime),
|
||||
|
||||
new Point3('mem')
|
||||
.setFloatField('comitted', body.mem.committed)
|
||||
.setFloatField('allocated', body.mem.allocated)
|
||||
.setFloatField('free', body.mem.free),
|
||||
|
||||
new Point3('apps')
|
||||
.setIntegerField('active_docs_count', body.apps.active_docs.length)
|
||||
.setIntegerField('loaded_docs_count', body.apps.loaded_docs.length)
|
||||
.setIntegerField('in_memory_docs_count', body.apps.in_memory_docs.length)
|
||||
.setStringField(
|
||||
'active_docs',
|
||||
globals.config.get('Butler-SOS.influxdbConfig.includeFields.activeDocs')
|
||||
? body.apps.active_docs
|
||||
: ''
|
||||
)
|
||||
.setStringField(
|
||||
'active_docs_names',
|
||||
globals.config.get('Butler-SOS.appNames.enableAppNameExtract') &&
|
||||
globals.config.get('Butler-SOS.influxdbConfig.includeFields.activeDocs')
|
||||
? appNamesActive.toString()
|
||||
: ''
|
||||
)
|
||||
.setStringField(
|
||||
'active_session_docs_names',
|
||||
globals.config.get('Butler-SOS.appNames.enableAppNameExtract') &&
|
||||
globals.config.get('Butler-SOS.influxdbConfig.includeFields.activeDocs')
|
||||
? sessionAppNamesActive.toString()
|
||||
: ''
|
||||
)
|
||||
.setStringField(
|
||||
'loaded_docs',
|
||||
globals.config.get('Butler-SOS.influxdbConfig.includeFields.loadedDocs')
|
||||
? body.apps.loaded_docs
|
||||
: ''
|
||||
)
|
||||
.setStringField(
|
||||
'loaded_docs_names',
|
||||
globals.config.get('Butler-SOS.appNames.enableAppNameExtract') &&
|
||||
globals.config.get('Butler-SOS.influxdbConfig.includeFields.loadedDocs')
|
||||
? appNamesLoaded.toString()
|
||||
: ''
|
||||
)
|
||||
.setStringField(
|
||||
'loaded_session_docs_names',
|
||||
globals.config.get('Butler-SOS.appNames.enableAppNameExtract') &&
|
||||
globals.config.get('Butler-SOS.influxdbConfig.includeFields.loadedDocs')
|
||||
? sessionAppNamesLoaded.toString()
|
||||
: ''
|
||||
)
|
||||
.setStringField(
|
||||
'in_memory_docs',
|
||||
globals.config.get('Butler-SOS.influxdbConfig.includeFields.inMemoryDocs')
|
||||
? body.apps.in_memory_docs
|
||||
: ''
|
||||
)
|
||||
.setStringField(
|
||||
'in_memory_docs_names',
|
||||
globals.config.get('Butler-SOS.appNames.enableAppNameExtract') &&
|
||||
globals.config.get('Butler-SOS.influxdbConfig.includeFields.inMemoryDocs')
|
||||
? appNamesInMemory.toString()
|
||||
: ''
|
||||
)
|
||||
.setStringField(
|
||||
'in_memory_session_docs_names',
|
||||
globals.config.get('Butler-SOS.appNames.enableAppNameExtract') &&
|
||||
globals.config.get('Butler-SOS.influxdbConfig.includeFields.inMemoryDocs')
|
||||
? sessionAppNamesInMemory.toString()
|
||||
: ''
|
||||
)
|
||||
.setIntegerField('calls', body.apps.calls)
|
||||
.setIntegerField('selections', body.apps.selections),
|
||||
|
||||
new Point3('cpu').setIntegerField('total', body.cpu.total),
|
||||
|
||||
new Point3('session')
|
||||
.setIntegerField('active', body.session.active)
|
||||
.setIntegerField('total', body.session.total),
|
||||
|
||||
new Point3('users')
|
||||
.setIntegerField('active', body.users.active)
|
||||
.setIntegerField('total', body.users.total),
|
||||
|
||||
new Point3('cache')
|
||||
.setIntegerField('hits', body.cache.hits)
|
||||
.setIntegerField('lookups', body.cache.lookups)
|
||||
.setIntegerField('added', body.cache.added)
|
||||
.setIntegerField('replaced', body.cache.replaced)
|
||||
.setIntegerField('bytes_added', body.cache.bytes_added),
|
||||
|
||||
new Point3('saturated').setBooleanField('saturated', body.saturated),
|
||||
];
|
||||
|
||||
// Write to InfluxDB
|
||||
try {
|
||||
for (const point of points) {
|
||||
// Apply server tags to each point
|
||||
applyTagsToPoint3(point, serverTags);
|
||||
await globals.influx.write(point.toLineProtocol(), database);
|
||||
}
|
||||
globals.logger.debug(`HEALTH METRICS V3: Wrote data to InfluxDB v3`);
|
||||
} catch (err) {
|
||||
globals.logger.error(
|
||||
`HEALTH METRICS V3: Error saving health data to InfluxDB v3! ${globals.getErrorMessage(err)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
203
src/lib/influxdb/v3/log-events.js
Normal file
203
src/lib/influxdb/v3/log-events.js
Normal file
@@ -0,0 +1,203 @@
|
||||
import { Point as Point3 } from '@influxdata/influxdb3-client';
|
||||
import globals from '../../../globals.js';
|
||||
import { isInfluxDbEnabled } from '../shared/utils.js';
|
||||
|
||||
/**
|
||||
* Post log event to InfluxDB v3
|
||||
*
|
||||
* @description
|
||||
* Handles log events from 5 different Qlik Sense sources:
|
||||
* - qseow-engine: Engine log events
|
||||
* - qseow-proxy: Proxy log events
|
||||
* - qseow-scheduler: Scheduler log events
|
||||
* - qseow-repository: Repository log events
|
||||
* - qseow-qix-perf: QIX performance metrics
|
||||
*
|
||||
* Each source has specific fields and tags that are written to InfluxDB.
|
||||
*
|
||||
* @param {object} msg - The log event message
|
||||
* @returns {Promise<void>} Promise that resolves when data has been posted to InfluxDB
|
||||
* @throws {Error} Error if unable to write data to InfluxDB
|
||||
*/
|
||||
export async function postLogEventToInfluxdbV3(msg) {
|
||||
globals.logger.debug(`LOG EVENT INFLUXDB V3: ${msg})`);
|
||||
|
||||
try {
|
||||
// Only write to InfluxDB if the global influx object has been initialized
|
||||
if (!isInfluxDbEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify the message source is valid
|
||||
if (
|
||||
msg.source !== 'qseow-engine' &&
|
||||
msg.source !== 'qseow-proxy' &&
|
||||
msg.source !== 'qseow-scheduler' &&
|
||||
msg.source !== 'qseow-repository' &&
|
||||
msg.source !== 'qseow-qix-perf'
|
||||
) {
|
||||
globals.logger.warn(
|
||||
`LOG EVENT INFLUXDB V3: Unknown log event source: ${msg.source}. Skipping.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const database = globals.config.get('Butler-SOS.influxdbConfig.v3Config.database');
|
||||
let point;
|
||||
|
||||
// Handle each message type with its specific fields
|
||||
if (msg.source === 'qseow-engine') {
|
||||
// Engine fields: message, exception_message, command, result_code, origin, context, session_id, raw_event
|
||||
point = new Point3('log_event')
|
||||
.setTag('host', msg.host)
|
||||
.setTag('level', msg.level)
|
||||
.setTag('source', msg.source)
|
||||
.setTag('log_row', msg.log_row)
|
||||
.setTag('subsystem', msg.subsystem || 'n/a')
|
||||
.setStringField('message', msg.message)
|
||||
.setStringField('exception_message', msg.exception_message || '')
|
||||
.setStringField('command', msg.command || '')
|
||||
.setStringField('result_code', msg.result_code || '')
|
||||
.setStringField('origin', msg.origin || '')
|
||||
.setStringField('context', msg.context || '')
|
||||
.setStringField('session_id', msg.session_id || '')
|
||||
.setStringField('raw_event', JSON.stringify(msg));
|
||||
|
||||
// Conditional tags
|
||||
if (msg?.user_full?.length > 0) point.setTag('user_full', msg.user_full);
|
||||
if (msg?.user_directory?.length > 0) point.setTag('user_directory', msg.user_directory);
|
||||
if (msg?.user_id?.length > 0) point.setTag('user_id', msg.user_id);
|
||||
if (msg?.result_code?.length > 0) point.setTag('result_code', msg.result_code);
|
||||
if (msg?.windows_user?.length > 0) point.setTag('windows_user', msg.windows_user);
|
||||
if (msg?.task_id?.length > 0) point.setTag('task_id', msg.task_id);
|
||||
if (msg?.task_name?.length > 0) point.setTag('task_name', msg.task_name);
|
||||
if (msg?.app_id?.length > 0) point.setTag('app_id', msg.app_id);
|
||||
if (msg?.app_name?.length > 0) point.setTag('app_name', msg.app_name);
|
||||
if (msg?.engine_exe_version?.length > 0)
|
||||
point.setTag('engine_exe_version', msg.engine_exe_version);
|
||||
} else if (msg.source === 'qseow-proxy') {
|
||||
// Proxy fields: message, exception_message, command, result_code, origin, context, raw_event
|
||||
point = new Point3('log_event')
|
||||
.setTag('host', msg.host)
|
||||
.setTag('level', msg.level)
|
||||
.setTag('source', msg.source)
|
||||
.setTag('log_row', msg.log_row)
|
||||
.setTag('subsystem', msg.subsystem || 'n/a')
|
||||
.setStringField('message', msg.message)
|
||||
.setStringField('exception_message', msg.exception_message || '')
|
||||
.setStringField('command', msg.command || '')
|
||||
.setStringField('result_code', msg.result_code || '')
|
||||
.setStringField('origin', msg.origin || '')
|
||||
.setStringField('context', msg.context || '')
|
||||
.setStringField('raw_event', JSON.stringify(msg));
|
||||
|
||||
// Conditional tags
|
||||
if (msg?.user_full?.length > 0) point.setTag('user_full', msg.user_full);
|
||||
if (msg?.user_directory?.length > 0) point.setTag('user_directory', msg.user_directory);
|
||||
if (msg?.user_id?.length > 0) point.setTag('user_id', msg.user_id);
|
||||
if (msg?.result_code?.length > 0) point.setTag('result_code', msg.result_code);
|
||||
} else if (msg.source === 'qseow-scheduler') {
|
||||
// Scheduler fields: message, exception_message, app_name, app_id, execution_id, raw_event
|
||||
point = new Point3('log_event')
|
||||
.setTag('host', msg.host)
|
||||
.setTag('level', msg.level)
|
||||
.setTag('source', msg.source)
|
||||
.setTag('log_row', msg.log_row)
|
||||
.setTag('subsystem', msg.subsystem || 'n/a')
|
||||
.setStringField('message', msg.message)
|
||||
.setStringField('exception_message', msg.exception_message || '')
|
||||
.setStringField('app_name', msg.app_name || '')
|
||||
.setStringField('app_id', msg.app_id || '')
|
||||
.setStringField('execution_id', msg.execution_id || '')
|
||||
.setStringField('raw_event', JSON.stringify(msg));
|
||||
|
||||
// Conditional tags
|
||||
if (msg?.user_full?.length > 0) point.setTag('user_full', msg.user_full);
|
||||
if (msg?.user_directory?.length > 0) point.setTag('user_directory', msg.user_directory);
|
||||
if (msg?.user_id?.length > 0) point.setTag('user_id', msg.user_id);
|
||||
if (msg?.task_id?.length > 0) point.setTag('task_id', msg.task_id);
|
||||
if (msg?.task_name?.length > 0) point.setTag('task_name', msg.task_name);
|
||||
} else if (msg.source === 'qseow-repository') {
|
||||
// Repository fields: message, exception_message, command, result_code, origin, context, raw_event
|
||||
point = new Point3('log_event')
|
||||
.setTag('host', msg.host)
|
||||
.setTag('level', msg.level)
|
||||
.setTag('source', msg.source)
|
||||
.setTag('log_row', msg.log_row)
|
||||
.setTag('subsystem', msg.subsystem || 'n/a')
|
||||
.setStringField('message', msg.message)
|
||||
.setStringField('exception_message', msg.exception_message || '')
|
||||
.setStringField('command', msg.command || '')
|
||||
.setStringField('result_code', msg.result_code || '')
|
||||
.setStringField('origin', msg.origin || '')
|
||||
.setStringField('context', msg.context || '')
|
||||
.setStringField('raw_event', JSON.stringify(msg));
|
||||
|
||||
// Conditional tags
|
||||
if (msg?.user_full?.length > 0) point.setTag('user_full', msg.user_full);
|
||||
if (msg?.user_directory?.length > 0) point.setTag('user_directory', msg.user_directory);
|
||||
if (msg?.user_id?.length > 0) point.setTag('user_id', msg.user_id);
|
||||
if (msg?.result_code?.length > 0) point.setTag('result_code', msg.result_code);
|
||||
} else if (msg.source === 'qseow-qix-perf') {
|
||||
// QIX Performance fields: app_id, process_time, work_time, lock_time, validate_time, traverse_time, handle, net_ram, peak_ram, raw_event
|
||||
point = new Point3('log_event')
|
||||
.setTag('host', msg.host || '<Unknown>')
|
||||
.setTag('level', msg.level || '<Unknown>')
|
||||
.setTag('source', msg.source || '<Unknown>')
|
||||
.setTag('log_row', msg.log_row || '-1')
|
||||
.setTag('subsystem', msg.subsystem || '<Unknown>')
|
||||
.setTag('method', msg.method || '<Unknown>')
|
||||
.setTag('object_type', msg.object_type || '<Unknown>')
|
||||
.setTag('proxy_session_id', msg.proxy_session_id || '-1')
|
||||
.setTag('session_id', msg.session_id || '-1')
|
||||
.setTag('event_activity_source', msg.event_activity_source || '<Unknown>')
|
||||
.setStringField('app_id', msg.app_id || '')
|
||||
.setFloatField('process_time', msg.process_time)
|
||||
.setFloatField('work_time', msg.work_time)
|
||||
.setFloatField('lock_time', msg.lock_time)
|
||||
.setFloatField('validate_time', msg.validate_time)
|
||||
.setFloatField('traverse_time', msg.traverse_time)
|
||||
.setIntegerField('handle', msg.handle)
|
||||
.setIntegerField('net_ram', msg.net_ram)
|
||||
.setIntegerField('peak_ram', msg.peak_ram)
|
||||
.setStringField('raw_event', JSON.stringify(msg));
|
||||
|
||||
// Conditional tags
|
||||
if (msg?.user_full?.length > 0) point.setTag('user_full', msg.user_full);
|
||||
if (msg?.user_directory?.length > 0) point.setTag('user_directory', msg.user_directory);
|
||||
if (msg?.user_id?.length > 0) point.setTag('user_id', msg.user_id);
|
||||
if (msg?.app_id?.length > 0) point.setTag('app_id', msg.app_id);
|
||||
if (msg?.app_name?.length > 0) point.setTag('app_name', msg.app_name);
|
||||
if (msg?.object_id?.length > 0) point.setTag('object_id', msg.object_id);
|
||||
}
|
||||
|
||||
// Add log event categories to tags if available
|
||||
// The msg.category array contains objects with properties 'name' and 'value'
|
||||
if (msg?.category?.length > 0) {
|
||||
msg.category.forEach((category) => {
|
||||
point.setTag(category.name, category.value);
|
||||
});
|
||||
}
|
||||
|
||||
// Add custom tags from config file
|
||||
if (
|
||||
globals.config.has('Butler-SOS.logEvents.tags') &&
|
||||
globals.config.get('Butler-SOS.logEvents.tags') !== null &&
|
||||
globals.config.get('Butler-SOS.logEvents.tags').length > 0
|
||||
) {
|
||||
const configTags = globals.config.get('Butler-SOS.logEvents.tags');
|
||||
for (const item of configTags) {
|
||||
point.setTag(item.name, item.value);
|
||||
}
|
||||
}
|
||||
|
||||
await globals.influx.write(point.toLineProtocol(), database);
|
||||
globals.logger.debug(`LOG EVENT INFLUXDB V3: Wrote data to InfluxDB v3`);
|
||||
|
||||
globals.logger.verbose('LOG EVENT INFLUXDB V3: Sent Butler SOS log event data to InfluxDB');
|
||||
} catch (err) {
|
||||
globals.logger.error(
|
||||
`LOG EVENT INFLUXDB V3: Error saving log event to InfluxDB! ${globals.getErrorMessage(err)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
181
src/lib/influxdb/v3/queue-metrics.js
Normal file
181
src/lib/influxdb/v3/queue-metrics.js
Normal file
@@ -0,0 +1,181 @@
|
||||
import { Point as Point3 } from '@influxdata/influxdb3-client';
|
||||
import globals from '../../../globals.js';
|
||||
import { isInfluxDbEnabled } from '../shared/utils.js';
|
||||
|
||||
/**
|
||||
* Store user event queue metrics to InfluxDB v3
|
||||
*
|
||||
* @description
|
||||
* Retrieves metrics from the user event queue manager and stores them in InfluxDB v3
|
||||
* for monitoring queue health, backpressure, dropped messages, and processing performance.
|
||||
*
|
||||
* @returns {Promise<void>} Promise that resolves when data has been posted to InfluxDB
|
||||
* @throws {Error} Error if unable to write data to InfluxDB
|
||||
*/
|
||||
export async function postUserEventQueueMetricsToInfluxdbV3() {
|
||||
try {
|
||||
// Check if queue metrics are enabled
|
||||
if (
|
||||
!globals.config.get(
|
||||
'Butler-SOS.userEvents.udpServerConfig.queueMetrics.influxdb.enable'
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get metrics from queue manager
|
||||
const queueManager = globals.udpQueueManagerUserActivity;
|
||||
if (!queueManager) {
|
||||
globals.logger.warn(
|
||||
'USER EVENT QUEUE METRICS INFLUXDB V3: Queue manager not initialized'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Only write to InfluxDB if the global influx object has been initialized
|
||||
if (!isInfluxDbEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const metrics = await queueManager.getMetrics();
|
||||
|
||||
// Get configuration
|
||||
const measurementName = globals.config.get(
|
||||
'Butler-SOS.userEvents.udpServerConfig.queueMetrics.influxdb.measurementName'
|
||||
);
|
||||
const configTags = globals.config.get(
|
||||
'Butler-SOS.userEvents.udpServerConfig.queueMetrics.influxdb.tags'
|
||||
);
|
||||
|
||||
const database = globals.config.get('Butler-SOS.influxdbConfig.v3Config.database');
|
||||
|
||||
const point = new Point3(measurementName)
|
||||
.setTag('queue_type', 'user_events')
|
||||
.setTag('host', globals.hostInfo.hostname)
|
||||
.setIntegerField('queue_size', metrics.queueSize)
|
||||
.setIntegerField('queue_max_size', metrics.queueMaxSize)
|
||||
.setFloatField('queue_utilization_pct', metrics.queueUtilizationPct)
|
||||
.setIntegerField('queue_pending', metrics.queuePending)
|
||||
.setIntegerField('messages_received', metrics.messagesReceived)
|
||||
.setIntegerField('messages_queued', metrics.messagesQueued)
|
||||
.setIntegerField('messages_processed', metrics.messagesProcessed)
|
||||
.setIntegerField('messages_failed', metrics.messagesFailed)
|
||||
.setIntegerField('messages_dropped_total', metrics.messagesDroppedTotal)
|
||||
.setIntegerField('messages_dropped_rate_limit', metrics.messagesDroppedRateLimit)
|
||||
.setIntegerField('messages_dropped_queue_full', metrics.messagesDroppedQueueFull)
|
||||
.setIntegerField('messages_dropped_size', metrics.messagesDroppedSize)
|
||||
.setFloatField('processing_time_avg_ms', metrics.processingTimeAvgMs)
|
||||
.setFloatField('processing_time_p95_ms', metrics.processingTimeP95Ms)
|
||||
.setFloatField('processing_time_max_ms', metrics.processingTimeMaxMs)
|
||||
.setIntegerField('rate_limit_current', metrics.rateLimitCurrent)
|
||||
.setIntegerField('backpressure_active', metrics.backpressureActive);
|
||||
|
||||
// Add static tags from config file
|
||||
if (configTags && configTags.length > 0) {
|
||||
for (const item of configTags) {
|
||||
point.setTag(item.name, item.value);
|
||||
}
|
||||
}
|
||||
|
||||
await globals.influx.write(point.toLineProtocol(), database);
|
||||
|
||||
globals.logger.verbose(
|
||||
'USER EVENT QUEUE METRICS INFLUXDB V3: Sent queue metrics data to InfluxDB v3'
|
||||
);
|
||||
|
||||
// Clear metrics after writing
|
||||
await queueManager.clearMetrics();
|
||||
} catch (err) {
|
||||
globals.logger.error(
|
||||
`USER EVENT QUEUE METRICS INFLUXDB V3: Error posting queue metrics: ${globals.getErrorMessage(err)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store log event queue metrics to InfluxDB v3
|
||||
*
|
||||
* @description
|
||||
* Retrieves metrics from the log event queue manager and stores them in InfluxDB v3
|
||||
* for monitoring queue health, backpressure, dropped messages, and processing performance.
|
||||
*
|
||||
* @returns {Promise<void>} Promise that resolves when data has been posted to InfluxDB
|
||||
* @throws {Error} Error if unable to write data to InfluxDB
|
||||
*/
|
||||
export async function postLogEventQueueMetricsToInfluxdbV3() {
|
||||
try {
|
||||
// Check if queue metrics are enabled
|
||||
if (
|
||||
!globals.config.get('Butler-SOS.logEvents.udpServerConfig.queueMetrics.influxdb.enable')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get metrics from queue manager
|
||||
const queueManager = globals.udpQueueManagerLogEvents;
|
||||
if (!queueManager) {
|
||||
globals.logger.warn(
|
||||
'LOG EVENT QUEUE METRICS INFLUXDB V3: Queue manager not initialized'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Only write to InfluxDB if the global influx object has been initialized
|
||||
if (!isInfluxDbEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const metrics = await queueManager.getMetrics();
|
||||
|
||||
// Get configuration
|
||||
const measurementName = globals.config.get(
|
||||
'Butler-SOS.logEvents.udpServerConfig.queueMetrics.influxdb.measurementName'
|
||||
);
|
||||
const configTags = globals.config.get(
|
||||
'Butler-SOS.logEvents.udpServerConfig.queueMetrics.influxdb.tags'
|
||||
);
|
||||
|
||||
const database = globals.config.get('Butler-SOS.influxdbConfig.v3Config.database');
|
||||
|
||||
const point = new Point3(measurementName)
|
||||
.setTag('queue_type', 'log_events')
|
||||
.setTag('host', globals.hostInfo.hostname)
|
||||
.setIntegerField('queue_size', metrics.queueSize)
|
||||
.setIntegerField('queue_max_size', metrics.queueMaxSize)
|
||||
.setFloatField('queue_utilization_pct', metrics.queueUtilizationPct)
|
||||
.setIntegerField('queue_pending', metrics.queuePending)
|
||||
.setIntegerField('messages_received', metrics.messagesReceived)
|
||||
.setIntegerField('messages_queued', metrics.messagesQueued)
|
||||
.setIntegerField('messages_processed', metrics.messagesProcessed)
|
||||
.setIntegerField('messages_failed', metrics.messagesFailed)
|
||||
.setIntegerField('messages_dropped_total', metrics.messagesDroppedTotal)
|
||||
.setIntegerField('messages_dropped_rate_limit', metrics.messagesDroppedRateLimit)
|
||||
.setIntegerField('messages_dropped_queue_full', metrics.messagesDroppedQueueFull)
|
||||
.setIntegerField('messages_dropped_size', metrics.messagesDroppedSize)
|
||||
.setFloatField('processing_time_avg_ms', metrics.processingTimeAvgMs)
|
||||
.setFloatField('processing_time_p95_ms', metrics.processingTimeP95Ms)
|
||||
.setFloatField('processing_time_max_ms', metrics.processingTimeMaxMs)
|
||||
.setIntegerField('rate_limit_current', metrics.rateLimitCurrent)
|
||||
.setIntegerField('backpressure_active', metrics.backpressureActive);
|
||||
|
||||
// Add static tags from config file
|
||||
if (configTags && configTags.length > 0) {
|
||||
for (const item of configTags) {
|
||||
point.setTag(item.name, item.value);
|
||||
}
|
||||
}
|
||||
|
||||
await globals.influx.write(point.toLineProtocol(), database);
|
||||
|
||||
globals.logger.verbose(
|
||||
'LOG EVENT QUEUE METRICS INFLUXDB V3: Sent queue metrics data to InfluxDB v3'
|
||||
);
|
||||
|
||||
// Clear metrics after writing
|
||||
await queueManager.clearMetrics();
|
||||
} catch (err) {
|
||||
globals.logger.error(
|
||||
`LOG EVENT QUEUE METRICS INFLUXDB V3: Error posting queue metrics: ${globals.getErrorMessage(err)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
67
src/lib/influxdb/v3/sessions.js
Normal file
67
src/lib/influxdb/v3/sessions.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import { Point as Point3 } from '@influxdata/influxdb3-client';
|
||||
import globals from '../../../globals.js';
|
||||
import { isInfluxDbEnabled } from '../shared/utils.js';
|
||||
|
||||
/**
|
||||
* Posts proxy sessions data to InfluxDB v3.
|
||||
*
|
||||
* This function takes user session data from Qlik Sense proxy and formats it for storage
|
||||
* in InfluxDB v3. It creates three measurements:
|
||||
* - user_session_summary: Summary with count and user list
|
||||
* - user_session_list: List of users (for compatibility)
|
||||
* - user_session_details: Individual session details for each active session
|
||||
*
|
||||
* @param {object} userSessions - User session data containing information about active sessions
|
||||
* @param {string} userSessions.host - The hostname of the server
|
||||
* @param {string} userSessions.virtualProxy - The virtual proxy name
|
||||
* @param {string} userSessions.serverName - Server name
|
||||
* @param {number} userSessions.sessionCount - Number of sessions
|
||||
* @param {string} userSessions.uniqueUserList - Comma-separated list of unique users
|
||||
* @param {Array} userSessions.datapointInfluxdb - Array of datapoints including individual sessions
|
||||
* @returns {Promise<void>} Promise that resolves when data has been posted to InfluxDB
|
||||
*/
|
||||
export async function postProxySessionsToInfluxdbV3(userSessions) {
|
||||
globals.logger.debug(`PROXY SESSIONS V3: User sessions: ${JSON.stringify(userSessions)}`);
|
||||
|
||||
globals.logger.silly(
|
||||
`PROXY SESSIONS V3: Data for server "${userSessions.host}", virtual proxy "${userSessions.virtualProxy}"`
|
||||
);
|
||||
|
||||
// Only write to InfluxDB if the global influx object has been initialized
|
||||
if (!isInfluxDbEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get database from config
|
||||
const database = globals.config.get('Butler-SOS.influxdbConfig.v3Config.database');
|
||||
|
||||
// Write all datapoints to InfluxDB
|
||||
// The datapointInfluxdb array contains summary points and individual session details
|
||||
try {
|
||||
if (userSessions.datapointInfluxdb && userSessions.datapointInfluxdb.length > 0) {
|
||||
for (const point of userSessions.datapointInfluxdb) {
|
||||
await globals.influx.write(point.toLineProtocol(), database);
|
||||
}
|
||||
globals.logger.debug(
|
||||
`PROXY SESSIONS V3: Wrote ${userSessions.datapointInfluxdb.length} datapoints to InfluxDB v3`
|
||||
);
|
||||
} else {
|
||||
globals.logger.warn('PROXY SESSIONS V3: No datapoints to write to InfluxDB v3');
|
||||
}
|
||||
} catch (err) {
|
||||
globals.logger.error(
|
||||
`PROXY SESSIONS V3: Error saving user session data to InfluxDB v3! ${globals.getErrorMessage(err)}`
|
||||
);
|
||||
}
|
||||
|
||||
globals.logger.debug(
|
||||
`PROXY SESSIONS V3: Session count for server "${userSessions.host}", virtual proxy "${userSessions.virtualProxy}": ${userSessions.sessionCount}`
|
||||
);
|
||||
globals.logger.debug(
|
||||
`PROXY SESSIONS V3: User list for server "${userSessions.host}", virtual proxy "${userSessions.virtualProxy}": ${userSessions.uniqueUserList}`
|
||||
);
|
||||
|
||||
globals.logger.verbose(
|
||||
`PROXY SESSIONS V3: Sent user session data to InfluxDB for server "${userSessions.host}", virtual proxy "${userSessions.virtualProxy}"`
|
||||
);
|
||||
}
|
||||
87
src/lib/influxdb/v3/user-events.js
Normal file
87
src/lib/influxdb/v3/user-events.js
Normal file
@@ -0,0 +1,87 @@
|
||||
import { Point as Point3 } from '@influxdata/influxdb3-client';
|
||||
import globals from '../../../globals.js';
|
||||
import { isInfluxDbEnabled } from '../shared/utils.js';
|
||||
|
||||
/**
|
||||
* Posts a user event to InfluxDB v3.
|
||||
*
|
||||
* @param {object} msg - The event to be posted to InfluxDB. The object should contain the following properties:
|
||||
* - host: The hostname of the Qlik Sense server that the user event originated from.
|
||||
* - command: The command (e.g. OpenApp, CreateApp, etc.) that the user event corresponds to.
|
||||
* - user_directory: The user directory of the user who triggered the event.
|
||||
* - user_id: The user ID of the user who triggered the event.
|
||||
* - origin: The origin of the event (e.g. Qlik Sense, QlikView, etc.).
|
||||
* - appId: The ID of the app that the event corresponds to (if applicable).
|
||||
* - appName: The name of the app that the event corresponds to (if applicable).
|
||||
* - ua: An object containing user agent information (if available).
|
||||
* @returns {Promise<void>} - A promise that resolves when the event has been posted to InfluxDB.
|
||||
*/
|
||||
export async function postUserEventToInfluxdbV3(msg) {
|
||||
globals.logger.debug(`USER EVENT INFLUXDB V3: ${msg})`);
|
||||
|
||||
// Only write to InfluxDB if the global influx object has been initialized
|
||||
if (!isInfluxDbEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const database = globals.config.get('Butler-SOS.influxdbConfig.v3Config.database');
|
||||
|
||||
// Create a new point with the data to be written to InfluxDB v3
|
||||
const point = new Point3('user_events')
|
||||
.setTag('host', msg.host)
|
||||
.setTag('event_action', msg.command)
|
||||
.setTag('userFull', `${msg.user_directory}\\${msg.user_id}`)
|
||||
.setTag('userDirectory', msg.user_directory)
|
||||
.setTag('userId', msg.user_id)
|
||||
.setTag('origin', msg.origin)
|
||||
.setStringField('userFull', `${msg.user_directory}\\${msg.user_id}`)
|
||||
.setStringField('userId', msg.user_id);
|
||||
|
||||
// Add app id and name to tags and fields if available
|
||||
if (msg?.appId) {
|
||||
point.setTag('appId', msg.appId);
|
||||
point.setStringField('appId', msg.appId);
|
||||
}
|
||||
if (msg?.appName) {
|
||||
point.setTag('appName', msg.appName);
|
||||
point.setStringField('appName', msg.appName);
|
||||
}
|
||||
|
||||
// Add user agent info to tags if available
|
||||
if (msg?.ua?.browser?.name) point.setTag('uaBrowserName', msg?.ua?.browser?.name);
|
||||
if (msg?.ua?.browser?.major) point.setTag('uaBrowserMajorVersion', msg?.ua?.browser?.major);
|
||||
if (msg?.ua?.os?.name) point.setTag('uaOsName', msg?.ua?.os?.name);
|
||||
if (msg?.ua?.os?.version) point.setTag('uaOsVersion', msg?.ua?.os?.version);
|
||||
|
||||
// Add custom tags from config file to payload
|
||||
if (
|
||||
globals.config.has('Butler-SOS.userEvents.tags') &&
|
||||
globals.config.get('Butler-SOS.userEvents.tags') !== null &&
|
||||
globals.config.get('Butler-SOS.userEvents.tags').length > 0
|
||||
) {
|
||||
const configTags = globals.config.get('Butler-SOS.userEvents.tags');
|
||||
for (const item of configTags) {
|
||||
point.setTag(item.name, item.value);
|
||||
}
|
||||
}
|
||||
|
||||
globals.logger.silly(
|
||||
`USER EVENT INFLUXDB V3: Influxdb datapoint for Butler SOS user event: ${JSON.stringify(
|
||||
point,
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
);
|
||||
|
||||
// Write to InfluxDB
|
||||
try {
|
||||
await globals.influx.write(point.toLineProtocol(), database);
|
||||
globals.logger.debug(`USER EVENT INFLUXDB V3: Wrote data to InfluxDB v3`);
|
||||
} catch (err) {
|
||||
globals.logger.error(
|
||||
`USER EVENT INFLUXDB V3: Error saving user event to InfluxDB v3! ${globals.getErrorMessage(err)}`
|
||||
);
|
||||
}
|
||||
|
||||
globals.logger.verbose('USER EVENT INFLUXDB V3: Sent Butler SOS user event data to InfluxDB');
|
||||
}
|
||||
@@ -6,10 +6,12 @@ import https from 'https';
|
||||
import path from 'path';
|
||||
import axios from 'axios';
|
||||
import { Point } from '@influxdata/influxdb-client';
|
||||
import { Point as Point3 } from '@influxdata/influxdb3-client';
|
||||
|
||||
import globals from '../globals.js';
|
||||
import { postProxySessionsToInfluxdb } from './post-to-influxdb.js';
|
||||
import { postProxySessionsToInfluxdb } from './influxdb/index.js';
|
||||
import { postProxySessionsToNewRelic } from './post-to-new-relic.js';
|
||||
import { applyTagsToPoint3 } from './influxdb/shared/utils.js';
|
||||
import { postUserSessionsToMQTT } from './post-to-mqtt.js';
|
||||
import { getServerTags } from './servertags.js';
|
||||
import { saveUserSessionMetricsToPrometheus } from './prom-client.js';
|
||||
@@ -99,9 +101,18 @@ function prepUserSessionMetrics(serverName, host, virtualProxy, body, tags) {
|
||||
.stringField('session_user_id_list', userProxySessionsData.uniqueUserList),
|
||||
];
|
||||
} else if (globals.config.get('Butler-SOS.influxdbConfig.version') === 3) {
|
||||
// Create empty array for InfluxDB v3
|
||||
// Individual session datapoints will be added later
|
||||
userProxySessionsData.datapointInfluxdb = [];
|
||||
// Create data points for InfluxDB v3
|
||||
const summaryPoint = new Point3('user_session_summary')
|
||||
.setIntegerField('session_count', userProxySessionsData.sessionCount)
|
||||
.setStringField('session_user_id_list', userProxySessionsData.uniqueUserList);
|
||||
applyTagsToPoint3(summaryPoint, userProxySessionsData.tags);
|
||||
|
||||
const listPoint = new Point3('user_session_list')
|
||||
.setIntegerField('session_count', userProxySessionsData.sessionCount)
|
||||
.setStringField('session_user_id_list', userProxySessionsData.uniqueUserList);
|
||||
applyTagsToPoint3(listPoint, userProxySessionsData.tags);
|
||||
|
||||
userProxySessionsData.datapointInfluxdb = [summaryPoint, listPoint];
|
||||
}
|
||||
|
||||
// Prometheus specific.
|
||||
@@ -189,9 +200,18 @@ function prepUserSessionMetrics(serverName, host, virtualProxy, body, tags) {
|
||||
.stringField('user_directory', bodyItem.UserDirectory)
|
||||
.stringField('user_id', bodyItem.UserId);
|
||||
} else if (globals.config.get('Butler-SOS.influxdbConfig.version') === 3) {
|
||||
// For v3, session details are not stored as individual points
|
||||
// Only summary data is stored, so we skip individual session datapoints
|
||||
sessionDatapoint = null;
|
||||
// Create data point for InfluxDB v3
|
||||
sessionDatapoint = new Point3('user_session_details')
|
||||
.setStringField('session_id', bodyItem.SessionId)
|
||||
.setStringField('user_directory', bodyItem.UserDirectory)
|
||||
.setStringField('user_id', bodyItem.UserId);
|
||||
// Apply all tags including server tags and session-specific tags
|
||||
applyTagsToPoint3(sessionDatapoint, userProxySessionsData.tags);
|
||||
// Add individual session tags
|
||||
sessionDatapoint
|
||||
.setTag('user_session_id', bodyItem.SessionId)
|
||||
.setTag('user_session_user_directory', bodyItem.UserDirectory)
|
||||
.setTag('user_session_user_id', bodyItem.UserId);
|
||||
}
|
||||
|
||||
if (sessionDatapoint) {
|
||||
|
||||
Reference in New Issue
Block a user