Skip to content

feat(configuration): Mark 4xx calls as erroneous spans and adapt current instrumentations. - #900

Open
CagriYonca wants to merge 1 commit into
mainfrom
fix-4xx-code
Open

feat(configuration): Mark 4xx calls as erroneous spans and adapt current instrumentations.#900
CagriYonca wants to merge 1 commit into
mainfrom
fix-4xx-code

Conversation

@CagriYonca

@CagriYonca CagriYonca commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

feat: HTTP 4xx exit span error classification

Implements opt-in 4xx error classification for HTTP exit spans per the
tracing specification. Entry spans are never affected.

What changed

Configuration

  • Two new opt-in settings under tracing.http.exit:
    • classify-all-4xx-as-errors (boolean) — marks all 4xx responses as errors on exit spans
    • classify-as-errors (list of integers, 400–499) — marks only the listed codes as errors
  • classify-as-errors takes full precedence over classify-all-4xx-as-errors when both are set
  • Three configuration sources supported (in priority order):
    1. INSTANA_TRACING_HTTP_EXIT_CLASSIFY_AS_ERRORS=401,403
    2. INSTANA_TRACING_HTTP_EXIT_CLASSIFY_ALL_4XX_AS_ERRORS=true
    3. Agent config / INSTANA_CONFIG_PATH YAML
  • Invalid env var values are ignored with a warning; unrecognised values for CLASSIFY_ALL keep the default (false)

http.error format

  • http.error attribute now uses "<code> <reason>" format (e.g. "404 Not Found") via a new http_error_message() helper, consistent across all four clients: aiohttp, httpx, tornado, urllib3

Tests

  • tests/clients/test_4xx_classification.py — end-to-end classification tests
  • tests/test_options.py — configuration precedence, invalid value handling, YAML fallback
  • tests/util/test_http_utils.pyhttp_error_message() and should_mark_http_exit_as_error() unit tests

@CagriYonca CagriYonca self-assigned this Sep 1, 2026
@CagriYonca
CagriYonca force-pushed the fix-4xx-code branch 2 times, most recently from b4a4852 to ef94674 Compare September 1, 2026 15:29
@CagriYonca
CagriYonca marked this pull request as ready for review September 1, 2026 16:06
@CagriYonca
CagriYonca requested a review from a team as a code owner September 1, 2026 16:06
@CagriYonca
CagriYonca force-pushed the fix-4xx-code branch 2 times, most recently from a0dc787 to 067a804 Compare September 2, 2026 13:10
…ent instrumentations.

Signed-off-by: Cagri Yonca <cagri@ibm.com>
Comment thread src/instana/options.py
Comment on lines +128 to +167
env_classify_as_errors = os.environ.get(
"INSTANA_TRACING_HTTP_EXIT_CLASSIFY_AS_ERRORS", None
)
if env_classify_as_errors is not None:
codes = []
for part in env_classify_as_errors.split(","):
part = part.strip()
if part.isdigit():
code = int(part)
if 400 <= code <= 499:
codes.append(code)
else:
logger.warning(
"Ignoring out-of-range value in"
" INSTANA_TRACING_HTTP_EXIT_CLASSIFY_AS_ERRORS:"
f" {code}, must be 400-499"
)
elif part:
logger.warning(
"Ignoring non-integer value in"
f" INSTANA_TRACING_HTTP_EXIT_CLASSIFY_AS_ERRORS: {part}"
)
if codes:
self.http_exit_classify_as_errors = codes
elif "INSTANA_TRACING_HTTP_EXIT_CLASSIFY_ALL_4XX_AS_ERRORS" in os.environ:
raw = os.environ["INSTANA_TRACING_HTTP_EXIT_CLASSIFY_ALL_4XX_AS_ERRORS"]
if raw.lower() in ("true", "false", "1"):
self.http_exit_classify_all_4xx_as_errors = is_truthy(raw)
else:
logger.warning(
"Ignoring unrecognised value for"
" INSTANA_TRACING_HTTP_EXIT_CLASSIFY_ALL_4XX_AS_ERRORS:"
f" '{raw}', expected true or false"
)
elif "INSTANA_CONFIG_PATH" in os.environ:
classify_all, codes = get_http_exit_classification_from_yaml()
if codes:
self.http_exit_classify_as_errors = codes
elif classify_all:
self.http_exit_classify_all_4xx_as_errors = True

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this to a specific method like the set_disable_trace_configurations() and others.
It's too long and hard to understand that this is related to setting up the list of 4xx status codes to use as error codes.

It was even difficult to select all those lines to provide this comment.

Comment thread src/instana/options.py
Comment on lines +153 to +160
raw = os.environ["INSTANA_TRACING_HTTP_EXIT_CLASSIFY_ALL_4XX_AS_ERRORS"]
if raw.lower() in ("true", "false", "1"):
self.http_exit_classify_all_4xx_as_errors = is_truthy(raw)
else:
logger.warning(
"Ignoring unrecognised value for"
" INSTANA_TRACING_HTTP_EXIT_CLASSIFY_ALL_4XX_AS_ERRORS:"
f" '{raw}', expected true or false"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to process all of this information. You have already set http_exit_classify_all_4xx_as_errors = False as the default value, so let's only accept a "truthy" value here.

Suggested change
raw = os.environ["INSTANA_TRACING_HTTP_EXIT_CLASSIFY_ALL_4XX_AS_ERRORS"]
if raw.lower() in ("true", "false", "1"):
self.http_exit_classify_all_4xx_as_errors = is_truthy(raw)
else:
logger.warning(
"Ignoring unrecognised value for"
" INSTANA_TRACING_HTTP_EXIT_CLASSIFY_ALL_4XX_AS_ERRORS:"
f" '{raw}', expected true or false"
self.http_exit_classify_all_4xx_as_errors = is_truthy(
os.environ["INSTANA_TRACING_HTTP_EXIT_CLASSIFY_ALL_4XX_AS_ERRORS"]
)
logger.debug(
"INSTANA_TRACING_HTTP_EXIT_CLASSIFY_ALL_4XX_AS_ERRORS = True"
)

Comment thread src/instana/options.py
Comment on lines +162 to +167
elif "INSTANA_CONFIG_PATH" in os.environ:
classify_all, codes = get_http_exit_classification_from_yaml()
if codes:
self.http_exit_classify_as_errors = codes
elif classify_all:
self.http_exit_classify_all_4xx_as_errors = True

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's ensure the self.http_exit_classify_as_errors and self.http_exit_classify_all_4xx_as_errors are correct:

Suggested change
elif "INSTANA_CONFIG_PATH" in os.environ:
classify_all, codes = get_http_exit_classification_from_yaml()
if codes:
self.http_exit_classify_as_errors = codes
elif classify_all:
self.http_exit_classify_all_4xx_as_errors = True
elif "INSTANA_CONFIG_PATH" in os.environ:
classify_all, codes = get_http_exit_classification_from_yaml()
if codes:
self.http_exit_classify_as_errors = codes
self.http_exit_classify_all_4xx_as_errors = False
elif classify_all:
self.http_exit_classify_all_4xx_as_errors = True
self.http_exit_classify_as_errors = []

Actually, if you really guarantee the return of get_http_exit_classification_from_yaml() will be always correct, you can make it simpler:

Suggested change
elif "INSTANA_CONFIG_PATH" in os.environ:
classify_all, codes = get_http_exit_classification_from_yaml()
if codes:
self.http_exit_classify_as_errors = codes
elif classify_all:
self.http_exit_classify_all_4xx_as_errors = True
elif "INSTANA_CONFIG_PATH" in os.environ:
self.http_exit_classify_all_4xx_as_errors, self.http_exit_classify_as_errors = get_http_exit_classification_from_yaml()

if params.response.status >= 500:
span.mark_as_errored({"http.error": params.response.reason})
if should_mark_http_exit_as_error(params.response.status, agent.options):
span.mark_as_errored({"http.error": http_error_message(params.response.status)})

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of having a method to set the expected value with the status code and reason. However, I don't think that's totally reusable since not all frameworks work with the same encoding or return pattern - you can see your Twisted change; it doesn't fit. In addition, users can customize the reason depending on the behavior of their systems, so not all messages will provide the correct reason.

I suggest you remove the http_error_message() method and use the f-string format for that.

Suggested change
span.mark_as_errored({"http.error": http_error_message(params.response.status)})
error_msg = f"{params.response.status} {params.response.reason}"
span.mark_as_errored({"http.error": error_msg)


extract_custom_headers(span, response.headers)
if should_mark_http_exit_as_error(response.code, agent.options):
span.mark_as_errored({"http.error": http_error_message(response.code)})

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like here:

Suggested change
span.mark_as_errored({"http.error": http_error_message(response.code)})
error_msg = f"{response.code} {response.body}"
span.mark_as_errored({"http.error": error_msg)

span = instance.request._instana
tracer = get_tracer()
tracer.inject(span.context, Format.HTTP_HEADERS, instance._headers)
return wrapped(*argv, **kwargs)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants