Skip to content

Commit

Permalink
Created src folder and moved code to this folder
Browse files Browse the repository at this point in the history
  • Loading branch information
Juan Carlos Martin committed Nov 14, 2023
1 parent 749295b commit a875e66
Show file tree
Hide file tree
Showing 9 changed files with 216 additions and 109 deletions.
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:18-alpine
FROM --platform=linux/amd64 node:18-alpine

RUN mkdir -p /usr/src/app && \
chown -R node:node /usr/src/app
Expand All @@ -10,4 +10,4 @@ RUN npm install

COPY . /usr/src/app

CMD ["node", "index.js"]
CMD ["node", "src/index.js"]
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,18 @@ The `build-release.sh` script does the following:

Call the script without parameters for a description.

### MAC Troubleshooting

When building the image using a MAC with M1 or M2, you may find that the execution of the image generates errors.
This is because the platform is incorrect when building the image.
To solve this issue, you need to export the following environment variable before performing the build:

```
export DOCKER_DEFAULT_PLATFORM=linux/amd64
```

With this adjustment, the image should be built correctly.

### Jenkinsfile

We have also provided a sample `Jenkinsfile` which you can use to build the project **for a specific release, from source code** on your own Jenkins infrastructure. Please note: These scripts all download the code from **this** repository on GitHub; in case you want to build from a different fork, you will need to do a couple of adaptions.
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"test": "jest"
},
"dependencies": {
"axios": "1.5.1",
"axios": "1.6.1",
"prom-client": "15.0.0",
"winston": "3.11.0"
},
Expand Down
42 changes: 15 additions & 27 deletions index.js → src/functions.js
Original file line number Diff line number Diff line change
@@ -1,38 +1,28 @@
'use strict'

const axios = require('axios')
const logger = require('./logger')

let PUSHGATEWAY_URL = resolve('PUSHGATEWAY_URL', 'http://localhost:9091')
if (!PUSHGATEWAY_URL.endsWith('/'))
PUSHGATEWAY_URL += '/'
logger.info(`Pushgateway URL: ${PUSHGATEWAY_URL}`)

const INTERVAL_SECONDS = resolve('PRUNE_INTERVAL', 60)
const PRUNE_THRESHOLD_SECONDS = resolve('PRUNE_THRESHOLD', 600)
logger.info(`Prune interval: ${INTERVAL_SECONDS} seconds.`)
logger.info(`Prune threshold: ${PRUNE_THRESHOLD_SECONDS} seconds.`)
const METRIC_NAME = 'push_time_seconds';

async function pruneGroups() {
async function pruneGroups(pushgatewayUrl, pruneThresholdSeconds) {
logger.info('Starting prune process...');

// Get metrics request from Prometheus push gateway
let metrics = null;
try {
metrics = await getMetrics(PUSHGATEWAY_URL);
metrics = await getMetrics(pushgatewayUrl);
} catch (e) {
throw new Error(`GET /metrics from ${PUSHGATEWAY_URL} failed. Cause: ${e}`)
throw new Error(`GET /metrics from ${pushgatewayUrl} failed. Cause: ${e}`)
}

// Get 'push_time_seconds' groups and filter the ones that are above pruneThresholdSeconds
const groupings = parseGroupings(metrics)
const filteredGroupings = filterOldGroupings(groupings)
const filteredGroupings = filterOldGroupings(groupings, pruneThresholdSeconds)
logger.info(`Found ${groupings.length} grouping(s), of which ${filteredGroupings.length} will be pruned`)

if (filteredGroupings.length > 0) {
filteredGroupings.map((filteredGroup) => {
try {
deleteGrouping(filteredGroup)
deleteGrouping(filteredGroup, pushgatewayUrl)
} catch (e) {
logger.error(`Pruning group ${filteredGroup} failed.`)
}
Expand All @@ -57,9 +47,9 @@ function resolve(envVar, defaultValue) {
return defaultValue
}

async function getMetrics() {
async function getMetrics(pushgatewayUrl) {
logger.debug('getMetrics()')
const getMetricsResponse = await axios.get(PUSHGATEWAY_URL + 'metrics', {
const getMetricsResponse = await axios.get(pushgatewayUrl + 'metrics', {
timeout: 2000
});

Expand All @@ -81,7 +71,7 @@ function parseGroupings(metrics) {
const pushGroups = []
for (let i = 0; i < lines.length; ++i) {
const line = lines[i]
if (line.startsWith("push_time_seconds")) {
if (line.startsWith(METRIC_NAME)) {
const labels = parseLabels(line.substring(line.indexOf('{') + 1, line.indexOf('}')))
const timestamp = new Date(parseFloat(line.substring(line.indexOf('}') + 1).trim()) * 1000)
pushGroups.push({
Expand Down Expand Up @@ -113,12 +103,12 @@ function parseLabels(labels) {
return labelMap
}

function filterOldGroupings(groupings) {
function filterOldGroupings(groupings, pruneThresholdSeconds) {
logger.debug('filterOldGroupings()');
const filteredGroupings = []
const now = new Date()
for (let i = 0; i < groupings.length; ++i) {
if ((now - groupings[i].timestamp) > PRUNE_THRESHOLD_SECONDS * 1000) {
if ((now - groupings[i].timestamp) > pruneThresholdSeconds * 1000) {
filteredGroupings.push(groupings[i])
}
}
Expand All @@ -127,7 +117,7 @@ function filterOldGroupings(groupings) {
return filteredGroupings
}

async function deleteGrouping(grouping) {
async function deleteGrouping(grouping, pushgatewayUrl) {
logger.debug('deleteGrouping()', grouping)

const job = grouping.labels.job
Expand All @@ -141,7 +131,7 @@ async function deleteGrouping(grouping) {
return;
}

const url = PUSHGATEWAY_URL + encodeURIComponent(`metrics/job/${job}/${labelName}/${labelValue}`)
const url = pushgatewayUrl + encodeURIComponent(`metrics/job/${job}/${labelName}/${labelValue}`)
logger.debug(`Delete URL: ${url}`)
const deleteResponse = await axios.delete(url, {
timeout: 2000
Expand All @@ -159,7 +149,6 @@ async function deleteGrouping(grouping) {

logger.debug(`DELETE ${url} succeeded, status code ${deleteResponse.status}`)
logger.info('Deleted grouping', grouping.labels)
return;
}

function findLabelName(labels) {
Expand All @@ -171,9 +160,8 @@ function findLabelName(labels) {
return null
}

const interval = setInterval(pruneGroups, INTERVAL_SECONDS * 1000)

module.exports = {
resolve,
pruneGroups,
interval
parseLabels
}
22 changes: 22 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const { resolve, pruneGroups } = require('./functions')
const logger = require('./logger')

let PUSHGATEWAY_URL = resolve('PUSHGATEWAY_URL', 'http://localhost:9091')
if (!PUSHGATEWAY_URL.endsWith('/'))
PUSHGATEWAY_URL += '/'
logger.info(`Pushgateway URL: ${PUSHGATEWAY_URL}`)

const INTERVAL_SECONDS = resolve('PRUNE_INTERVAL', 60)
const PRUNE_THRESHOLD_SECONDS = resolve('PRUNE_THRESHOLD', 600)
logger.info(`Prune interval: ${INTERVAL_SECONDS} seconds.`)
logger.info(`Prune threshold: ${PRUNE_THRESHOLD_SECONDS} seconds.`)

const interval = setInterval(
() => pruneGroups(PUSHGATEWAY_URL, PRUNE_THRESHOLD_SECONDS),
INTERVAL_SECONDS * 1000
)

module.exports = {
pruneGroups,
interval
}
2 changes: 1 addition & 1 deletion logger.js → src/logger.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const winston = require('winston')

let logLevel = process.env.DEBUG == 'true' ? 'debug' : 'info'
let logLevel = process.env.DEBUG === 'true' ? 'debug' : 'info'

const logger = winston.createLogger({
transports: [
Expand Down
159 changes: 159 additions & 0 deletions test/functions.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
const axios = require('axios');
const { resolve, pruneGroups, parseLabels } = require('./../src/functions')

jest.mock('axios');

const DEFAULT_METRICS_DATA =
'go_gc_duration_seconds{quantile="0"} 2.56e-05\n' +
'# HELP push_time_seconds Last Unix time when changing this group in the Pushgateway succeeded.\n' +
'# TYPE push_time_seconds gauge\n' +
'push_time_seconds{instance="instance_1",job="application_1"} 1.5806880000000000e+09\n' +
'push_time_seconds{instance="instance_2",job="application_2"} 1.5831936000000000e+09\n' +
'push_time_seconds{instance="instance_3",job="application_3"} 1.5858720000000000e+09\n' +
'# HELP pushgateway_http_requests_total Total HTTP requests processed by the Pushgateway, excluding scrapes.\n' +
'# TYPE pushgateway_http_requests_total counter\n' +
'pushgateway_http_requests_total{code="200",handler="healthy",method="get"} 12550\n' +
'pushgateway_http_requests_total{code="200",handler="push",method="post"} 8445\n' +
'pushgateway_http_requests_total{code="200",handler="ready",method="get"} 12550'

function mockProcessEnv(envVar, envValue) {
process.env[envVar] = envValue;
}

function mockGetMetricsResponse(metricsData = DEFAULT_METRICS_DATA, statusCode = 200) {
axios.get.mockResolvedValue({
status: statusCode,
data: metricsData
});
}

function mockDeleteMetricsResponse(statusCode = 200) {
axios.delete.mockResolvedValue({
status: statusCode
});
}

describe('Functions test', () => {
describe('Prune groups', () => {

beforeAll(() => {
jest.useFakeTimers('modern');
jest.setSystemTime(Date.UTC(2020, 2, 3));

mockGetMetricsResponse();
mockDeleteMetricsResponse();
});

beforeEach(() => {
expect(axios.delete).toHaveBeenCalledTimes(0);
});

afterEach(() => {
jest.clearAllMocks();
});

afterAll(() => {
jest.useRealTimers();
});

test('Simple prune process with one metric to be prune', async () => {
await pruneGroups('PUSHGATEWAY_URL', 60);

expect(axios.delete).toHaveBeenCalledTimes(1);
});

test('Metric with no instance should not be deleted', async () => {
const metricsData =
'push_time_seconds{instance="",job="application_1"} 1.5806844000000000e+09';

mockGetMetricsResponse(metricsData);

await pruneGroups('PUSHGATEWAY_URL', 60);

expect(axios.delete).toHaveBeenCalledTimes(0);
});

test('When GET metric has an error raise an exception', async () => {
mockGetMetricsResponse('', 500);

const request = pruneGroups('PUSHGATEWAY_URL', 60);

await expect(request).rejects.toThrow();
});
})

describe('Resolve function test', () => {
const envVar = 'TEST_ENV_VAR';
const defaultValue = 'default_value';
const defaultProcessEnvValues = { ...process.env };

beforeEach(() => {
jest.resetModules();
process.env = { ...defaultProcessEnvValues };
});

afterAll(() => {
process.env = defaultProcessEnvValues; // Restore old environment
});

test('Return process env value string when specified', () => {
const expectedValue = 'expected_value';
mockProcessEnv(envVar, expectedValue);

const responseValue = resolve(envVar, defaultValue);

expect(responseValue).toBe(expectedValue);
});

test('Return process env value integer when specified', () => {
const expectedValue = 123;
mockProcessEnv(envVar, expectedValue);

const responseValue = resolve(envVar, defaultValue);

expect(responseValue).toBe(expectedValue);
});

test('Return default process env value when no specified', () => {
const responseValue = resolve(envVar, defaultValue);

expect(responseValue).toBe(defaultValue);
});
});

describe('Parse labels function test', () => {
test('Return the specific labels', () => {
const stringLabels = 'instance="instance_1",job="application_1"';
const expectedValue = {
instance: 'instance_1',
job: 'application_1'
};

const responseValue = parseLabels(stringLabels);

expect(responseValue).toEqual(expectedValue);
});

test('Return empty string in labels that does not have a value', () => {
const stringLabels = 'instance="",job="application_1"';
const expectedValue = {
instance: '',
job: 'application_1'
};

const responseValue = parseLabels(stringLabels);

expect(responseValue).toEqual(expectedValue);
});

test('Return empty if no labels', () => {
const stringLabels = '';
const expectedValue = {};

const responseValue = parseLabels(stringLabels);

expect(responseValue).toEqual(expectedValue);
});
});
})

Loading

0 comments on commit a875e66

Please sign in to comment.