grafana / synthetic-monitoring-agent

Synthetic Monitoring Agent
https://grafana.com/docs/grafana-cloud/how-do-i/synthetic-monitoring/
Apache License 2.0
157 stars 20 forks source link

[Fix] MultiHTTP body variable replacement #717

Closed The-9880 closed 1 month ago

The-9880 commented 1 month ago

This follows up from https://github.com/grafana/synthetic-monitoring-agent/pull/713 to close https://github.com/grafana/synthetic-monitoring-agent/issues/637.

I ran into a JS issue where Object has no method 'replace', because the result of b64decode was an ArrayBuffer.

Also, replace returns a copy, so you have to assign it to the original body variable or else it won't get into the request.

Example output (search for `body =` statements) ### script.js ```js import http from 'k6/http'; import { check, fail } from 'k6'; import { test } from 'k6/execution'; // TODO(mem): conditionally import these modules // - import encoding if base64 decoding is required // - import jsonpath if there are json assertions import encoding from 'k6/encoding'; import jsonpath from 'https://jslib.k6.io/jsonpath/1.0.2/index.js'; import { URL } from 'https://jslib.k6.io/url/1.0.0/index.js'; export const options = { scenarios: { default: { executor: 'shared-iterations', tags: { // TODO(mem): build tags out of options for the check? environment: 'production', }, // exec: 'runner', maxDuration: '10s', // TODO(mem): this would be the timeout for the check gracefulStop: '1s', }, }, dns: { ttl: '2m', // TODO(mem): this doesn't need to be much higher than the maxDuration select: 'first', // TODO(mem): we can build this maps to IP option in checks, more or less policy: 'preferIPv4', // preferIPv6, onlyIPv4, onlyIPv6, any }, // TODO(mem): we can build this out of check options insecureSkipTLSVerify: false, tlsVersion: { // TODO(mem): we can build this out of check options min: 'tls1.2', max: 'tls1.3', }, // TODO(mem): we can build this out of agent version userAgent: 'synthetic-monitoring-agent/v0.14.3 (linux amd64; g64b8bab; +https://github.com/grafana/synthetic-monitoring-agent)', maxRedirects: 10, blacklistIPs: ['10.0.0.0/8'], blockHostnames: ['*.cluster.local'], // k6 options vus: 1, // linger: false, summaryTimeUnit: 's', discardResponseBodies: false, // enable only if there are checks? }; function assertHeader(headers, name, matcher) { const lcName = name.toLowerCase(); const values = Object.entries(headers). filter(h => h[0].toLowerCase() === lcName). map(h => h[1]); if (values.find(v => matcher(v)) !== undefined) { return true; } else if (values.length === 0) { console.warn(`'${name}' not present in response`); } else { values.forEach(v => console.warn(`'${name}' has the value '${v}'`)); } return false; } export default function () { let response; let body; let url; let currentCheck; let match; const logResponse = false const vars = {}; console.log("Starting request to https://test-api.k6.io/public/crocodiles/1/?format=json..."); try { url = new URL('https://test-api.k6.io/public/crocodiles/1/?format\\u003Djson'); } catch (e) { console.error("Invalid URL: https://test-api.k6.io/public/crocodiles/1/?format=json"); fail() } body = null; response = http.request('GET', url.toString(), body, { // TODO(mem): build params out of options for the check tags: { name: '0', // TODO(mem): give the user some control over this? __raw_url__: 'https://test-api.k6.io/public/crocodiles/1/?format=json', }, redirects: 0 }); console.log("Response received from https://test-api.k6.io/public/crocodiles/1/?format=json, status", response.status); if (logResponse) { const body = response.body || '' console.log("Response body received from https://test-api.k6.io/public/crocodiles/1/?format=json:", body.slice(0, 1000)); } if (response.error) { console.error("Request error:" + url.toString() + ": " + response.error) } vars['crocName'] = jsonpath.query(response.json(), 'name')[0]; vars['sex'] = jsonpath.query(response.json(), 'sex')[0]; console.log("Starting request to https://eolexw7b7ourxuo.m.pipedream.net..."); try { url = new URL('https://eolexw7b7ourxuo.m.pipedream.net'); } catch (e) { console.error("Invalid URL: https://eolexw7b7ourxuo.m.pipedream.net"); fail() } body = encoding.b64decode("eyJtZXNzYWdlIjoiVGhlIGNyb2MgbmFtZWQgJHtjcm9jTmFtZX0gaXMgb2Ygc2V4ICR7c2V4fSJ9", 'rawstd', "s"); body = body.replace('${crocName}', vars['crocName']); body = body.replace('${sex}', vars['sex']); response = http.request('POST', url.toString(), body, { // TODO(mem): build params out of options for the check tags: { name: '1', // TODO(mem): give the user some control over this? __raw_url__: 'https://eolexw7b7ourxuo.m.pipedream.net', }, redirects: 0, headers: { 'Content-Type': "application/json", "x-test-header": vars['crocName'] } }); console.log("Response received from https://eolexw7b7ourxuo.m.pipedream.net, status", response.status); if (logResponse) { const body = response.body || '' console.log("Response body received from https://eolexw7b7ourxuo.m.pipedream.net:", body.slice(0, 1000)); } if (response.error) { console.error("Request error:" + url.toString() + ": " + response.error) } } ```
The-9880 commented 1 month ago

MDN says:

A string pattern will only be replaced once

Can this be a problemn in our use case? It might not be common, but someone may at some point try to use the same variable twice in the body 🤔

replaceAll seems like a drop-in replacement without this downside.

🤦 Thank you, yes this works