Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow user-defined tags based on response data for built-in metrics (e.g. value of a certain HTTP header) #3940

Open
frittentheke opened this issue Sep 9, 2024 · 5 comments
Labels

Comments

@frittentheke
Copy link

Feature Description

I know it's possible to add tags at various stages and abstraction levels: https://grafana.com/docs/k6/latest/using-k6/tags-and-groups/#user-defined-tags. But this has to happen before the operation and apparently it's not possible to dynamically extract a tag based on the response data, e.g. when calling fail.

In my case I'd like to extract more details from 4xx or 5xx errors (certain HTTP headers indicating a more detailed reason for the failure). I know I can create a custom metric and apply a tag based on the response. But I'd much rather add another tag or two to the built-in metrics.

A somewhat related question might be this one here: #680

Suggested Solution (optional)

No response

Already existing or connected issues / PRs (optional)

No response

@joanlopez
Copy link
Contributor

Hey @frittentheke,

Thanks for your request. Could you please extend your proposal?
Like, how these tags would be defined, and accounted, with a example script.

We would appreciate a detailed feature request, so we can dig straight into possible implementations, their the pros & cons, feasibility, etc - rather than designing a new feature from scratch.

🙇🏻

@frittentheke
Copy link
Author

Thanks @joanlopez for getting back to me this quickly!

Thanks for your request. Could you please extend your proposal? Like, how these tags would be defined, and accounted, with a example script.

Certainly! The sole difference to the existing capabilities of the check function is the time additional (user-defined) tags are defined.

Currently one has to set the tags and their value statically prior to calling check():

import http from 'k6/http';
import { check, fail } from 'k6';

export default function () {
  const res = http.get('https://httpbin.test.k6.io');
  const checkOutput = check(
    res,
    {
      'response code was 200': (res) => res.status == 200,
      'body size was 1234 bytes': (res) => res.body.length == 1234,
    },
    { myTag: "I'm a tag" }
  );

  if (!checkOutput) {
    fail('unexpected response');
  }
}

I like to use data from the (HTTP) response though to enrich e.g. the k6_http_req_failed_rate(HTTPReqFailed) with tag apart from error, error_code, status, ...

In my particular case the intended tag values come from certain headers in the 5xx and 4xx responses to which currently (

func (t *transport) measureAndEmitMetrics(unfReq *unfinishedRequest) *finishedRequest {
?) only static tags can be applied. To further point to my particular use-case, take the x-envoy-overloaded header (envoyproxy/envoy#15106) which I'd like to distinguish in my metrics from other 503 responses - so to separate 503 from Envoy (Istio Service Mesh) and the application itself.

But my use-case is NOT limited to failed requests. Successful responses could also be distinguished by certain headers and their value in the response, for example by indicating if a request was served from a cache (e.g. AWS Cloudfront X-Cache header).

While I certainly can introduce a [custom metric ](https://grafana.com/docs/k6/latest/using-k6/metrics/create-custom-metrics) and extract the headers before e.g. calling fail(), I much rather like for all those rich built-in metrics (

func (t *transport) measureAndEmitMetrics(unfReq *unfinishedRequest) *finishedRequest {
) to be distinct for certain tags.

@joanlopez
Copy link
Contributor

Thanks for the extended answer @frittentheke! 🙇🏻

Could you provide one or two ways of how would you like to define such metrics, with an example like the one you provided to show how it could be achieved now? Like, having a setTag (or similar) function that would be called within the check callback? Or what?

Thanks!

@frittentheke
Copy link
Author

Could you provide one or two ways of how would you like to define such metrics, with an example like the one you provided to show how it could be achieved now? Like, having a setTag (or similar) function that would be called within the check callback? Or what?

Currently one has to use a custom metric to use data from the response:

export function httpErrorCheck(response: Response): void {
    if (response.status !== 200) {
        httpErrorCounter.add(1, { "x-envoy-overloaded": response.headers["x-envoy-overloaded"] } })
        fail(`Response Status Code was: ${response.status}`);
    }
}
  1. I'd either like user defined tags which are NOT static, but reference something from the response. Or the ability to add a list of response headers to the list of system-tags. Those are also referencing information from the response e.g. tls_version.

With just defining headers that should be extracted and added to the built-in metrics as tags, this solution is a little clunky.

  1. As you, @joanlopez , suggested, a configurable callback to "extract" and set tags from the response makes the most sense. There already is so much data extraction towards tags happening in
    if unfReq.err != nil {
    result.errorCode, result.errorMsg = errorCodeForError(unfReq.err)
    tagsAndMeta.SetSystemTagOrMetaIfEnabled(enabledTags, metrics.TagError, result.errorMsg)
    tagsAndMeta.SetSystemTagOrMetaIfEnabled(enabledTags, metrics.TagErrorCode, strconv.Itoa(int(result.errorCode)))
    tagsAndMeta.SetSystemTagOrMetaIfEnabled(enabledTags, metrics.TagStatus, "0")
    } else {
    tagsAndMeta.SetSystemTagOrMetaIfEnabled(enabledTags, metrics.TagStatus, strconv.Itoa(unfReq.response.StatusCode))
    if unfReq.response.StatusCode >= 400 {
    result.errorCode = errCode(1000 + unfReq.response.StatusCode)
    tagsAndMeta.SetSystemTagOrMetaIfEnabled(enabledTags, metrics.TagErrorCode, strconv.Itoa(int(result.errorCode)))
    }
    tagsAndMeta.SetSystemTagOrMetaIfEnabled(enabledTags, metrics.TagProto, unfReq.response.Proto)
    if unfReq.response.TLS != nil {
    tlsInfo, oscp := netext.ParseTLSConnState(unfReq.response.TLS)
    tagsAndMeta.SetSystemTagOrMetaIfEnabled(enabledTags, metrics.TagTLSVersion, tlsInfo.Version)
    tagsAndMeta.SetSystemTagOrMetaIfEnabled(enabledTags, metrics.TagOCSPStatus, oscp.Status)
    result.tlsInfo = tlsInfo
    }
    }
    if enabledTags.Has(metrics.TagIP) && trail.ConnRemoteAddr != nil {
    if ip, _, err := net.SplitHostPort(trail.ConnRemoteAddr.String()); err == nil {
    tagsAndMeta.SetSystemTagOrMeta(metrics.TagIP, ip)
    }
    }
    . Referencing a little functiondynamicTagsCallback to allow for more data extraction and return of an additional list of tags and their value would be awesome.

@joanlopez
Copy link
Contributor

Hi @frittentheke,

I've been thinking about this for some time, and now I'm wondering how feasible this would be, because I had a look at the code involved in setting up user-defined tags, and that's typically done right before running the associated function (the HTTP request, the check function, etc).

So, enabling a mechanism to define tags based on the "output" of such functions, dynamically, wouldn't mean only having to make tags accept some sort of callback instead of static key-value pairs, but also specifically injecting that logic on each operation that accepts tags now.

That would require a much larger change, and I don't think we have the capacity now to focus on that.

However, let me invoke @mstoykov, because he knows the system much better than me, so maybe he has a nice workaround or idea of how to implement such a feature without having to do very big changes.

Thanks! 🙇🏻

@joanlopez joanlopez removed their assignment Oct 3, 2024
@joanlopez joanlopez removed the triage label Oct 3, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants