How to use res.timings.duration for groups?

Hello, guys!
I need to calculate the load metric for each of the API methods per request. Is it correct to do this using the timings.duration metric in the following script ?

import {group, check} from "k6";
import {
allSportsDuration,
eventTypesDuration,
eventDetailsDuration,
eventExternalInfoDuration,
eventsDuration,
favouritesChampsDuration,
highlightsDuration,
liveEventsDuration,
liveNowDuration,
menuBySportDuration,
topSportMenuDuration,
topSportsDuration,
upcomingDuration,
staticTranslationsDuration,
marketTypeGroupsDuration,
selectionCodesDuration,
breadCrumbNavItemDuration,
marketNamesDuration,
centralEventsDuration,
eventsByNameDuration,
eventByCodeDuration,
popularBetsDuration,
errorRate,
options
} from "../config.js";
import {getAllSports} from "../api/allSports.js";
import {getEvents} from "../api/events.js";
import {getFavouritesChamps} from "../api/favouritesChamps.js";
import {getHighlights} from "../api/highlights.js";
import {getMenuBySport} from "../api/menuBySport.js";
import {getTopSportMenu} from "../api/topSportMenu.js";
import {getTopSports} from "../api/topSports.js";
import {getUpcoming} from "../api/upcoming.js";
import {getEventDetails} from "../api/eventDetails.js";
import {getEventExternalInfo} from "../api/eventExternalInfo.js";
import {getLiveEvents} from "../api/liveEvents.js";
import {getLiveNow} from "../api/liveNow.js";
import {getEventTypes} from "../api/eventTypes.js";
import {getEventByCode} from "../api/eventByCode.js";
import {getBreadCrumbNavItem} from "../api/getBreadCrumbNavItem.js";
import {getMarketNames} from "../api/getMarketNames.js";
import {getSelectionCodes} from "../api/getSelectionCodes.js";
import {getMarketTypeGroups} from "../api/getMarketTypeGroups.js";
import {getStaticTranslations} from "../api/getStaticTranslations.js";
import {getPopularBets} from "../api/popularBets.js";
import {getCentralEvents} from "../api/getCentralEvents.js";
import {getEventsByName} from "../api/eventsByName.js";
import {
    getEventIdsFromEvents,
    getTopSportIds,
    getSportIdsFromAllSports,
    getSportIdsWithLiveEventsFromAllSports,
    getEventCodes
} from "../helpers/helpers.js";

export {options};

let eventIds = [];
let eventCodes = [];
let sportids = [];
let liveSportIds = [];
let topSportsIds = [];
let langIds = [2, 3, 4, 5, 6, 7, 8, 9, 12, 13, 15, 17, 36, 37, 38, 39, 40, 41, 51, 58, 59, 60, 63, 64, 65, 66, 67, 68, 69, 74, 75, 79, 81, 82];


export default function () {    
group("allSports", function () {
    const allSportsResult = getAllSports(langIds);
    sportids = getSportIdsFromAllSports(allSportsResult);
    liveSportIds = getSportIdsWithLiveEventsFromAllSports(allSportsResult);
    let success = check(allSportsResult, {
        "status is 200 OK": (allSportsResult) => allSportsResult.status === 200,
        "content-type is application/json": (allSportsResult) => allSportsResult.headers['Content-Type'] === "application/json; charset=utf-8",
    });
    errorRate.add(!success);
    allSportsDuration.add(allSportsResult.timings.duration);
});
group("menuBySport", function () {
    const menuBySportRes = getMenuBySport(langIds, sportids);
    let success = check(menuBySportRes, {
        "status is 200 OK": (menuBySportRes) => menuBySportRes.status === 200,
        "content-type is application/json": (menuBySportRes) => menuBySportRes.headers['Content-Type'] === "application/json; charset=utf-8",
    });
    errorRate.add(!success);
    menuBySportDuration.add(menuBySportRes.timings.duration);
});

group("getEvents", function () {
    const getEventsRes = getEvents(langIds, sportids);
    eventIds = getEventIdsFromEvents(getEventsRes);
    eventCodes = getEventCodes(getEventsRes);
    let success = check(getEventsRes, {
        "status is 200 OK": (getEventsRes) => getEventsRes.status === 200,
        "content-type is application/json": (getEventsRes) => getEventsRes.headers['Content-Type'] === "application/json; charset=utf-8",
        // "events count is equal 10": (res) => JSON.parse(res.body).Result.EventsCount === 10,
    });
    errorRate.add(!success);
    eventsDuration.add(getEventsRes.timings.duration);
});

group("topSports", function () {
    const topSportsRes = getTopSports(langIds);
    topSportsIds = getTopSportIds(topSportsRes);
    let success = check(topSportsRes, {
        "status is 200 OK": (topSportsRes) => topSportsRes.status === 200,
        "content-type is application/json": (topSportsRes) => topSportsRes.headers['Content-Type'] === "application/json; charset=utf-8",
    });
    errorRate.add(!success);
    topSportsDuration.add(topSportsRes.timings.duration);
});

group("upcoming", function () {
    const upcomingRes = getUpcoming(langIds, topSportsIds);
    let success = check(upcomingRes, {
        "status is 200 OK": (upcomingRes) => upcomingRes.status === 200,
        "content-type is application/json": (upcomingRes) => upcomingRes.headers['Content-Type'] === "application/json; charset=utf-8",
        // "events count is equal 10": (res) => JSON.parse(res.body).Result.EventsCount === 10,
    });
    errorRate.add(!success);
    upcomingDuration.add(upcomingRes.timings.duration);
});

group("topSportMenu", function () {
    const topSportMenuRes = getTopSportMenu(langIds);
    let success = check(topSportMenuRes, {
        "status is 200 OK": (topSportMenuRes) => topSportMenuRes.status === 200,
        "content-type is application/json": (topSportMenuRes) => topSportMenuRes.headers['Content-Type'] === "application/json; charset=utf-8",
    });
    errorRate.add(!success);
    topSportMenuDuration.add(topSportMenuRes.timings.duration);
});


group("highlights", function () {
    const highlightsRes = getHighlights(langIds, topSportsIds);
    let success = check(highlightsRes, {
        "status is 200 OK": (highlightsRes) => highlightsRes.status === 200,
        "content-type is application/json": (highlightsRes) => highlightsRes.headers['Content-Type'] === "application/json; charset=utf-8",
        // "events count is equal 10": (res) => JSON.parse(res.body).Result.EventsCount === 10,
    });
    errorRate.add(!success);
    highlightsDuration.add(highlightsRes.timings.duration);
});

group("eventDetails", function () {
    const eventDetailsRes = getEventDetails(langIds, eventIds);
    let success = check(eventDetailsRes, {
        "status is 200 OK": (eventDetailsRes) => eventDetailsRes.status === 200,
        "content-type is application/json": (eventDetailsRes) => eventDetailsRes.headers['Content-Type'] === "application/json; charset=utf-8",
    });
    errorRate.add(!success);
    eventDetailsDuration.add(eventDetailsRes.timings.duration);
});

group("favouritesChamps", function () {
    const favouritesChampsRes = getFavouritesChamps(langIds);
    let success = check(favouritesChampsRes, {
        "status is 200 OK": (favouritesChampsRes) => favouritesChampsRes.status === 200,
        "content-type is application/json": (favouritesChampsRes) => favouritesChampsRes.headers['Content-Type'] === "application/json; charset=utf-8",
    });
    errorRate.add(!success);
    favouritesChampsDuration.add(favouritesChampsRes.timings.duration);
});


group("eventExternalInfo", function () {
    const eventExternalInfoRes = getEventExternalInfo(langIds, eventIds);
    let success = check(eventExternalInfoRes, {
        "status is 200 OK": (eventExternalInfoRes) => eventExternalInfoRes.status === 200,
        "content-type is application/json": (eventExternalInfoRes) => eventExternalInfoRes.headers['Content-Type'] === "application/json; charset=utf-8",
    });
    errorRate.add(!success);
    eventExternalInfoDuration.add(eventExternalInfoRes.timings.duration);
});

group("GetEventTypes", function () {
    const eventTypesRes = getEventTypes(langIds, sportids);
    let success = check(eventTypesRes, {
        "status is 200 OK": (eventTypesRes) => eventTypesRes.status === 200,
        "content-type is application/json": (eventTypesRes) => eventTypesRes.headers['Content-Type'] === "application/json; charset=utf-8",
    });
    errorRate.add(!success);
    eventTypesDuration.add(eventTypesRes.timings.duration);
});

group("GetEventByCode", function () {
    const eventByCodeRes = getEventByCode(langIds, eventCodes);
    let success = check(eventByCodeRes, {
        "status is 200 OK": (eventByCodeRes) => eventByCodeRes.status === 200,
        "content-type is application/json": (eventByCodeRes) => eventByCodeRes.headers['Content-Type'] === "application/json; charset=utf-8",
    });
    errorRate.add(!success);
    eventByCodeDuration.add(eventByCodeRes.timings.duration);
});

group("GetBreadCrumbNavItem", function () {
    const breadCrumbNavItemRes = getBreadCrumbNavItem(langIds, sportids);
    let success = check(breadCrumbNavItemRes, {
        "status is 200 OK": (breadCrumbNavItemRes) => breadCrumbNavItemRes.status === 200,
        "content-type is application/json": (breadCrumbNavItemRes) => breadCrumbNavItemRes.headers['Content-Type'] === "application/json; charset=utf-8",
    });
    errorRate.add(!success);
    breadCrumbNavItemDuration.add(breadCrumbNavItemRes.timings.duration);
});

group("GetMarketNames", function () {
    const marketNamesRes = getMarketNames(langIds, sportids);
    let success = check(marketNamesRes, {
        "status is 200 OK": (marketNamesRes) => marketNamesRes.status === 200,
        "content-type is application/json": (marketNamesRes) => marketNamesRes.headers['Content-Type'] === "application/json; charset=utf-8",
    });
    errorRate.add(!success);
    marketNamesDuration.add(marketNamesRes.timings.duration);
});

group("GetSelectionCodes", function () {
    const selectionCodesRes = getSelectionCodes(langIds);
    let success = check(selectionCodesRes, {
        "status is 200 OK": (selectionCodesRes) => selectionCodesRes.status === 200,
        "content-type is application/json": (selectionCodesRes) => selectionCodesRes.headers['Content-Type'] === "application/json; charset=utf-8",
    });
    errorRate.add(!success);
    selectionCodesDuration.add(selectionCodesRes.timings.duration);
});

group("GetMarketTypeGroups", function () {
    const marketTypeGroupsRes = getMarketTypeGroups(langIds, sportids);
    let success = check(marketTypeGroupsRes, {
        "status is 200 OK": (marketTypeGroupsRes) => marketTypeGroupsRes.status === 200,
        "content-type is application/json": (marketTypeGroupsRes) => marketTypeGroupsRes.headers['Content-Type'] === "application/json; charset=utf-8",
    });
    errorRate.add(!success);
    marketTypeGroupsDuration.add(marketTypeGroupsRes.timings.duration);
});

group("GetStaticTranslations", function () {
    const staticTranslationsRes = getStaticTranslations(langIds);
    let success = check(staticTranslationsRes, {
        "status is 200 OK": (staticTranslationsRes) => staticTranslationsRes.status === 200,
        "content-type is application/json": (staticTranslationsRes) => staticTranslationsRes.headers['Content-Type'] === "application/json; charset=utf-8",
    });
    errorRate.add(!success);
    staticTranslationsDuration.add(staticTranslationsRes.timings.duration);
});

group("GetPopularBets", function () {
    const popularBetsRes = getPopularBets(langIds);
    let success = check(popularBetsRes, {
        "status is 200 OK": (popularBetsRes) => popularBetsRes.status === 200,
        "content-type is application/json": (popularBetsRes) => popularBetsRes.headers['Content-Type'] === "application/json; charset=utf-8",
    });
    errorRate.add(!success);
    popularBetsDuration.add(popularBetsRes.timings.duration);
});

group("GetCentralEvents", function () {
    const centralEventsRes = getCentralEvents(langIds);
    let success = check(centralEventsRes, {
        "status is 200 OK": (centralEventsRes) => centralEventsRes.status === 200,
        "content-type is application/json": (centralEventsRes) => centralEventsRes.headers['Content-Type'] === "application/json; charset=utf-8",
    });
    errorRate.add(!success);
    centralEventsDuration.add(centralEventsRes.timings.duration);
});

group("GetEventsByName", function () {
    const eventsByNameResp = getEventsByName(langIds);
    let success = check(eventsByNameResp, {
        "status is 200 OK": (eventsByNameResp) => eventsByNameResp.status === 200,
        "content-type is application/json": (eventsByNameResp) => eventsByNameResp.headers['Content-Type'] === "application/json; charset=utf-8",
    });
    errorRate.add(!success);
    eventsByNameDuration.add(eventsByNameResp.timings.duration);
});

group("liveEvents", function () {
    let liveEventsResp = getLiveEvents(langIds, liveSportIds);
    let success = check(liveEventsResp, {
        "status is 200 OK": (liveEventsResp) => liveEventsResp.status === 200,
        "content-type is application/json": (liveEventsResp) => liveEventsResp.headers['Content-Type'] === "application/json; charset=utf-8",
    });
    errorRate.add(!success);
    liveEventsDuration.add(liveEventsResp.timings.duration);

    liveEventsResp = getLiveEvents(langIds, liveSportIds);
    success = check(liveEventsResp, {
        "status is 200 OK": (liveEventsResp) => liveEventsResp.status === 200,
        "content-type is application/json": (liveEventsResp) => liveEventsResp.headers['Content-Type'] === "application/json; charset=utf-8",
    });
    errorRate.add(!success);
    liveEventsDuration.add(liveEventsResp.timings.duration);

    liveEventsResp = getLiveEvents(langIds, liveSportIds);
    success = check(liveEventsResp, {
        "status is 200 OK": (liveEventsResp) => liveEventsResp.status === 200,
        "content-type is application/json": (liveEventsResp) => liveEventsResp.headers['Content-Type'] === "application/json; charset=utf-8",
    });

    errorRate.add(!success);
    liveEventsDuration.add(liveEventsResp.timings.duration);

    liveEventsResp = getLiveEvents(langIds, liveSportIds);
    success = check(liveEventsResp, {
        "status is 200 OK": (liveEventsResp) => liveEventsResp.status === 200,
        "content-type is application/json": (liveEventsResp) => liveEventsResp.headers['Content-Type'] === "application/json; charset=utf-8",
    });

    errorRate.add(!success);
    liveEventsDuration.add(liveEventsResp.timings.duration);

    liveEventsResp = getLiveEvents(langIds, liveSportIds);
    success = check(liveEventsResp, {
        "status is 200 OK": (liveEventsResp) => liveEventsResp.status === 200,
        "content-type is application/json": (liveEventsResp) => liveEventsResp.headers['Content-Type'] === "application/json; charset=utf-8",
    });

    errorRate.add(!success);
    liveEventsDuration.add(liveEventsResp.timings.duration);

    liveEventsResp = getLiveEvents(langIds, liveSportIds);
    success = check(liveEventsResp, {
        "status is 200 OK": (liveEventsResp) => liveEventsResp.status === 200,
        "content-type is application/json": (liveEventsResp) => liveEventsResp.headers['Content-Type'] === "application/json; charset=utf-8",
    });

    errorRate.add(!success);
    liveEventsDuration.add(liveEventsResp.timings.duration);

});

group("liveNow", function () {
    let liveNowResp = getLiveNow(langIds, topSportsIds);
    let success = check(liveNowResp, {
        "status is 200 OK": (liveNowResp) => liveNowResp.status === 200,
        "content-type is application/json": (liveNowResp) => liveNowResp.headers['Content-Type'] === "application/json; charset=utf-8",
    });

    errorRate.add(!success);
    liveNowDuration.add(liveNowResp.timings.duration);

    liveNowResp = getLiveNow(langIds, topSportsIds);
    success = check(liveNowResp, {
        "status is 200 OK": (liveNowResp) => liveNowResp.status === 200,
        "content-type is application/json": (liveNowResp) => liveNowResp.headers['Content-Type'] === "application/json; charset=utf-8",
    });

    errorRate.add(!success);
    liveNowDuration.add(liveNowResp.timings.duration);

    liveNowResp = getLiveNow(langIds, topSportsIds);
    success = check(liveNowResp, {
        "status is 200 OK": (liveNowResp) => liveNowResp.status === 200,
        "content-type is application/json": (liveNowResp) => liveNowResp.headers['Content-Type'] === "application/json; charset=utf-8",
    });

    errorRate.add(!success);
    liveNowDuration.add(liveNowResp.timings.duration);

    liveNowResp = getLiveNow(langIds, topSportsIds);
    success = check(liveNowResp, {
        "status is 200 OK": (liveNowResp) => liveNowResp.status === 200,
        "content-type is application/json": (liveNowResp) => liveNowResp.headers['Content-Type'] === "application/json; charset=utf-8",
    });

    errorRate.add(!success);
    liveNowDuration.add(liveNowResp.timings.duration);

    liveNowResp = getLiveNow(langIds, topSportsIds);
    success = check(liveNowResp, {
        "status is 200 OK": (liveNowResp) => liveNowResp.status === 200,
        "content-type is application/json": (liveNowResp) => liveNowResp.headers['Content-Type'] === "application/json; charset=utf-8",
    });

    errorRate.add(!success);
    liveNowDuration.add(liveNowResp.timings.duration);

    liveNowResp = getLiveNow(langIds, topSportsIds);
    success = check(liveNowResp, {
        "status is 200 OK": (liveNowResp) => liveNowResp.status === 200,
        "content-type is application/json": (liveNowResp) => liveNowResp.headers['Content-Type'] === "application/json; charset=utf-8",
    });

    errorRate.add(!success);
    liveNowDuration.add(liveNowResp.timings.duration);
});

};

For example for one of the tested methods I’m calculation metric by the following command:

allSportsDuration.add(allSportsResult.timings.duration);

Metric variable was added in separate js script as:
export let allSportsDuration = new Trend(“all_sports_duration”);

Hi @Daniil,

your title:

How to use res.timings.duration for groups?

and

I need to calculate the load metric for each of the API methods per request. Is it correct to do this using the timings.duration metric in the following script?

don’t add up for me … :frowning: I guess you changed what you meant to ask or how you phrased the question between writing title and the body.

I expect that what you want is to group “same” requests together and see their average/max/p95 requests duration?

If you want to do it using only k6 and the summary output. What you are doing is probably what you should be.

I would argue you have outgrown the summary output and should be using another result output for better analyzing and visualizing.

Every request in k6 is measured and metrics for it are emitted with additional tags including url and name . url is obviously just that - the URL used, the name is a “special” tag that is either the url or if set can be used to group different URLs together. As the docs show this is mostly useful when there are query parameters which make the url different but this actually are the same requests. Also obviously you can use it to make very long and unreadable urls more readable by just naming them stuff. I propose using the json output to with your script to see what other tags there are and their values :D.

The k6 cloud automatically groups requests per their name, which when it isn’t set is just the url. This can be done in any other result output, albeit you will need to do it. If you choose Grafana you can reuse another user’s Grafana dashboards for example this one seems to have grouping by name, given screenshot,, but I haven’t used it :D.

If you still want to use summary output only, you could specify a threshold per each different request , for example using its name and it will be shown in the output:

import http from "k6/http";
export let options = {
    thresholds : {
            'http_req_duration{name:"my cool request"}': ["max>0"],
            'http_req_duration{name:"not a cool request"}': ["max>0"],
    }
}
export default function() {
    let res = http.get("https://httpbin.test.k6.io/status/400", {"tags": {"name": "my cool request"}});
    res = http.get("https://httpbin.test.k6.io/status/401", {"tags": {"name": "not a cool request"}});
}

would produce:


The threshold can be whatever it’s there only to make k6 actually put this in the output. The major upside is that you won’t need to add a ton of additional metrics and always adding to them. The major downside is that you can wrongly define a threshold and it will not tell you anything as it hasn’t failed :frowning: .

2 Likes

Thanks for the sharing. Indeed, this is a better way to display the tag response time.

Typically, we need to add the check and only want those pass request to show in the overall summary result. May I check that if I only want to pass request response time in http_req_duration, how do I do so?

 let res = http.get("https://httpbin.test.k6.io/status/400", {"tags": {"name": "my cool request"}});

@xyngfei, I’m not sure I completely understand what you want to do, but you can also filter by status in the thresholds:

import http from "k6/http";
export let options = {
    thresholds: {
        'http_req_duration{status:"200",name:"cool request"}': ["max>0"],
    }
}
export default function () {
    http.get("https://httpbin.test.k6.io/status/200", { "tags": { "name": "cool request" } });
    http.get("https://httpbin.test.k6.io/status/401", { "tags": { "name": "cool request" } });
}

And for even more complicated use cases, you can use custom metrics to measure only the metrics you are interested in:

import http from "k6/http";
import { Trend } from "k6/metrics";

let okReqsDuration = new Trend("ok_requests_duration", true);

function myGet(url) {
    let res = http.get(url)
    // you can put whatever condition or check you want here
    if (res.status == 200) {
        okReqsDuration.add(res.timings.duration);
    }
}

export let options = {
    thresholds: {
        'ok_requests_duration': ["avg<300"],
    }
}

export default function () {
    myGet("https://httpbin.org/status/200");
    myGet("https://httpbin.org/status/400");
}

The second example above is the current way I am doing to measure individual transaction response time.

However, I tried with the following example

export let options = {
thresholds: {
‘http_req_duration{status:“200”,name:“cool request”}’: [“max>0”],
}
}

It still shows the request time details under tag

I am sorry, I don’t understand what the issue is :confused:

Basically I was simulating a bad/fail request. The request return 5xx. Based on my custom metrics, it didn’t store the response time of the fail request.

However, if I am using the tag, it will just consolidate all requests response time regardless of any return status. Any suggestion to use tag to filter out of bad/fail request.

Well, notice that the threshold is http_req_duration{status:“200”,name:“cool request”} - this means it applies only to the http_req_duration which have tags status=200 AND name=cool request

yeah, somehow, the request has thrown HTTP 500. By right, I have the status:“200”, why it will still display the response statistic?