Skip to content

fix(azure_sentinel): split batches under the 1MB ingestion cap - #39880

Merged
yucheng-berri merged 10 commits into
litellm_internal_stagingfrom
litellm_lit5899_azure_sentinel_batch_split
Sep 6, 2026
Merged

yucheng-berri merged 10 commits into
litellm_internal_stagingfrom
litellm_lit5899_azure_sentinel_batch_split

Conversation

@yucheng-berri

@yucheng-berri yucheng-berri commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • Azure Sentinel batches over 1MB get a 413 and are dropped whole
  • Any send failure also dropped the whole queue, no retry
  • Records added during an in-flight send were cleared unsent

How it solves it:

  • Split batches under the 1MB ingestion cap before sending
  • On 413, halve and retry, drop only a lone oversize record
  • Keep undelivered records queued for the next flush, retried once per flush interval
  • Take the flush lock for threshold sends too, so overlapping flushes stop double-sending records
  • On a cancelled send, requeue only the pieces the destination never accepted
  • Share the split logic with Datadog instead of copying it

User Flow

Before: a proxy admin who ships request logs to Microsoft Sentinel sees a burst of large prompts vanish from Log Analytics, along with the small requests sent in the same few seconds

  1. They send POST https://litellm-domain/v1/chat/completions six times at once, each with a 240KB prompt, plus one small request a few seconds earlier, and every call returns 200
  2. They open the Log Analytics workspace and run LiteLLM_CL | where TimeGenerated > ago(10m): none of the seven requests is there
  3. The proxy log shows a single line, Azure Sentinel Error sending batch API - Client error '413 Request Entity Too Large', and nothing is retried
  4. The same happens for a short ingestion outage: one 503 Service Unavailable line, and every record queued at that moment is gone for good

After: the same requests all show up in Log Analytics, and a short outage delays them instead of losing them

  1. They send the same POST https://litellm-domain/v1/chat/completions six times at once with 240KB prompts, plus the one small request, and every call returns 200
  2. LiteLLM_CL | where TimeGenerated > ago(10m) lists all seven requests
  3. The proxy log shows no 413. A single request whose own record is over 1MB logs Azure Sentinel API Error - Payload too large for a single record and only that one record is skipped
  4. During an ingestion outage the proxy logs one 503 Service Unavailable line per flush, and once the endpoint is back the queued records land in Log Analytics on the next flush

Relevant issues

Fixes #26450

Linear ticket

Resolves LIT-5899

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • The handful of test files covering my change pass locally, e.g. uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*, make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more
  • My PR passes all required CI/CD checks (e.g., lint, schema.d.ts sync check, etc.)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Screenshots / Proof of Fix

Datadog UI evidence: public dashboard and dashboard screenshot. The dashboard reads six real Datadog Logs events from both the base proxy and fixed head after the intake returned 403 for 30 seconds, then recovered

Azure portal evidence from the dedicated Log Analytics workspace:

Azure reactive 413 readback

Reactive live-413 readback: the forwarder returned 413 above 300KB and relayed smaller requests to the real Azure endpoint; the base delivered 1 of 6 records while the fixed head delivered all 6. Open the full image

Azure proactive split readback

Real Azure proactive-split readback: the workspace contains every final-head large-batch and small-sibling record; the base's burst result varies with flush timing and dropped mixed oversize batches in the recorded run. Open the full image

Azure audit delivery readback

Audit stream parity readback: both base and head delivered the small audit record, confirming the audit callback path remains intact. Open the full image

Azure portal UI steps: open https://portal.azure.com, select Log Analytics workspaces, open lit5899-ws in resource group litellm-lit5899-rg, choose Logs, paste the LiteLLM_CL query below, set the time range to Last hour, and run it. The query shows the final-head LIT5899_R6_S1_HEAD, LIT5899_R6_S3_HEAD, and LIT5899_R6_S4SMALL_HEAD rows. Run the LiteLLMAudit_CL query below for the audit marker

Shared setup, identical for both runs. Real proxy on a fresh Postgres, real OpenAI, real Azure Monitor Logs Ingestion endpoint (a dedicated Log Analytics workspace with a Custom-LiteLLM and a Custom-LiteLLM-Audit stream), one worker

model_list:
  - model_name: gpt-4o-mini
    litellm_params:
      model: openai/gpt-4o-mini
      api_key: os.environ/OPENAI_API_KEY

litellm_settings:
  callbacks: ["azure_sentinel"]
  audit_log_callbacks: ["azure_sentinel"]
  store_audit_logs: true

general_settings:
  master_key: sk-lit5899
export AZURE_SENTINEL_ENDPOINT=... AZURE_SENTINEL_DCR_ID=... AZURE_SENTINEL_TENANT_ID=... AZURE_SENTINEL_CLIENT_ID=... AZURE_SENTINEL_CLIENT_SECRET=...
export DATABASE_URL=postgresql://litellm:litellm@127.0.0.1:15899/lit5899
python litellm/proxy/proxy_cli.py --config config.yaml --port 4000

# payloads: a 216 byte prompt, a 240KB prompt, a 1.1MB prompt, each carrying a marker like LIT5899_R2_S3_<BASE|HEAD>
python -c 'import json,sys; print(json.dumps({"model":"gpt-4o-mini","max_tokens":5,"messages":[{"role":"user","content":sys.argv[1]+" "+"x"*int(sys.argv[2])}]}))' LIT5899_R2_S3_HEAD 240000 > s3.json

# readback, run a minute after the last request
WS=<workspace id>
az monitor log-analytics query -w $WS -o table --analytics-query "LiteLLM_CL | where TimeGenerated > ago(1h) | extend marker = extract('LIT5899_R2_[A-Z0-9]+_(?:BASE|HEAD)', 0, tostring(messages)) | where isnotempty(marker) | summarize records = count(), firstSeen = min(TimeGenerated) by marker | order by marker asc"
az monitor log-analytics query -w $WS -o table --analytics-query "LiteLLMAudit_CL | where TimeGenerated > ago(1h) | extend marker = extract('LIT5899_R2_S5_(?:base|head)', 0, tostring(updated_values)) | where isnotempty(marker) | summarize records = count(), firstSeen = min(TimeGenerated) by marker, action, table_name | order by marker asc"

Before (aea5358)

One small request followed by six concurrent 240KB requests

  1. curl -sS -o /dev/null -w 'status=%{http_code}\n' http://127.0.0.1:4000/v1/chat/completions -H 'Authorization: Bearer sk-lit5899' -H 'Content-Type: application/json' -d @s1.json

    status=200
    
  2. seq 1 6 | xargs -P 6 -I{} curl -sS -o /dev/null -w 'status=%{http_code}\n' http://127.0.0.1:4000/v1/chat/completions -H 'Authorization: Bearer sk-lit5899' -H 'Content-Type: application/json' -d @s3.json | sort | uniq -c

       6 status=200
    
  3. Proxy log at the next flush

    01:17:35 - LiteLLM:ERROR: azure_sentinel.py:372 - Azure Sentinel Error sending batch API - Client error '413 Request Entity Too Large' for url '<url>
    
  4. LiteLLM_CL readback: no rows for LIT5899_R2_S1_BASE or LIT5899_R2_S3_BASE. All seven records were dropped, including the small one that happened to share the batch

One 1.1MB request plus three small ones

  1. curl ... -d @s4big.json & seq 1 3 | xargs -P 3 -I{} curl ... -d @s4small.json; wait

       1 big status=400
       3 small status=200
    

    The 400 is OpenAI rejecting the 1.1MB prompt, which the proxy still logs as a failure record

  2. Proxy log at the next flush

    01:17:40 - LiteLLM:ERROR: azure_sentinel.py:372 - Azure Sentinel Error sending batch API - Client error '413 Request Entity Too Large' for url '<url>
    
  3. LiteLLM_CL readback: no rows for LIT5899_R2_S4SMALL_BASE. The three small records were dropped along with the one oversize record

Team creation audit record

  1. curl -sS -o /dev/null -w 'status=%{http_code}\n' http://127.0.0.1:4000/team/new -H 'Authorization: Bearer sk-lit5899' -H 'Content-Type: application/json' -d '{"team_alias":"LIT5899_R2_S5_base"}'

    status=200
    
  2. LiteLLMAudit_CL readback

    TableName      Action    FirstSeen                     Marker              Records    Table_name
    PrimaryResult  created   2026-09-05T08:19:31.2674302Z  LIT5899_R2_S5_base  1          LiteLLM_TeamTable
    

    Small audit batches were never affected, this case is the parity check

Ingestion endpoint answers 503 for 25 seconds, then recovers

  1. Point AZURE_SENTINEL_ENDPOINT at a local forwarder that answers 503 while /tmp/fault.flag exists and otherwise relays the request unchanged to the real endpoint. touch /tmp/fault.flag, then send three chat completions with marker LIT5899_REC_base and one POST /team/new with alias LIT5899_REC_S5_base

    base chat 1 status=200
    base chat 2 status=200
    base chat 3 status=200
    base team/new status=200
    
  2. Forwarder log after 22 seconds, then rm /tmp/fault.flag and 15 more seconds

       1 FAULT 503 bytes=35800 path=.../streams/Custom-LiteLLM?api-version=2023-01-01
       1 FAULT 503 bytes=614 path=.../streams/Custom-LiteLLM-Audit?api-version=2023-01-01
    

    One attempt per stream, nothing after the fault is lifted

  3. Proxy log

    01:14:36 - LiteLLM:ERROR: azure_sentinel.py:372 - Azure Sentinel Error sending batch API - Server error '503 Service Unavailable' for url '<url>
    01:14:36 - LiteLLM:ERROR: azure_sentinel.py:372 - Azure Sentinel Error sending batch API - Server error '503 Service Unavailable' for url '<url>
    
  4. Readback with the LIT5899_REC_ markers: no rows in LiteLLM_CL, no rows in LiteLLMAudit_CL

After (934a0d1)

One small request followed by six concurrent 240KB requests

  1. curl -sS -o /dev/null -w 'status=%{http_code}\n' http://127.0.0.1:4000/v1/chat/completions -H 'Authorization: Bearer sk-lit5899' -H 'Content-Type: application/json' -d @s1.json

    status=200
    
  2. seq 1 6 | xargs -P 6 -I{} curl -sS -o /dev/null -w 'status=%{http_code}\n' http://127.0.0.1:4000/v1/chat/completions -H 'Authorization: Bearer sk-lit5899' -H 'Content-Type: application/json' -d @s3.json | sort | uniq -c

       6 status=200
    
  3. Proxy log at the next flush: no error line

  4. LiteLLM_CL readback

    TableName      FirstSeen                     Marker                   Records
    PrimaryResult  2026-09-05T08:18:10.9170946Z  LIT5899_R2_S1_HEAD       1
    PrimaryResult  2026-09-05T08:18:21.5206178Z  LIT5899_R2_S3_HEAD       6
    

One 1.1MB request plus three small ones

  1. curl ... -d @s4big.json & seq 1 3 | xargs -P 3 -I{} curl ... -d @s4small.json; wait

       1 big status=400
       3 small status=200
    
  2. Proxy log at the next flush

    01:18:37 - LiteLLM:ERROR: batch_utils.py:22 - Azure Sentinel API Error - Payload too large for a single record
    
  3. LiteLLM_CL readback

    TableName      FirstSeen                     Marker                   Records
    PrimaryResult  2026-09-05T08:18:36.8969782Z  LIT5899_R2_S4SMALL_HEAD  3
    

    The three small records land. Only the single record that is over 1MB on its own is skipped, and the log names it

Team creation audit record

  1. curl -sS -o /dev/null -w 'status=%{http_code}\n' http://127.0.0.1:4000/team/new -H 'Authorization: Bearer sk-lit5899' -H 'Content-Type: application/json' -d '{"team_alias":"LIT5899_R2_S5_head"}'

    status=200
    
  2. LiteLLMAudit_CL readback

    TableName      Action    FirstSeen                     Marker              Records    Table_name
    PrimaryResult  created   2026-09-05T08:19:32.441537Z   LIT5899_R2_S5_head  1          LiteLLM_TeamTable
    

Ingestion endpoint answers 503 for 25 seconds, then recovers

  1. Same forwarder, touch /tmp/fault.flag, three chat completions with marker LIT5899_REC_head and one POST /team/new with alias LIT5899_REC_S5_head

    head chat 1 status=200
    head chat 2 status=200
    head chat 3 status=200
    head team/new status=200
    
  2. Forwarder log after 22 seconds, then rm /tmp/fault.flag and 15 more seconds

       5 FAULT 503 bytes=35799 path=.../streams/Custom-LiteLLM?api-version=2023-01-01
       5 FAULT 503 bytes=614 path=.../streams/Custom-LiteLLM-Audit?api-version=2023-01-01
       1 FORWARD status=204 bytes=35799 path=.../streams/Custom-LiteLLM?api-version=2023-01-01
       1 FORWARD status=204 bytes=614 path=.../streams/Custom-LiteLLM-Audit?api-version=2023-01-01
    

    One retry per flush interval while the fault is on, then both batches delivered with 204 as soon as it lifts

  3. Proxy log

    01:14:36 - LiteLLM:ERROR: batch_utils.py:55 - Azure Sentinel Error sending batch API - Server error '503 Service Unavailable' for url '<url>
    01:14:41 - LiteLLM:ERROR: batch_utils.py:55 - Azure Sentinel Error sending batch API - Server error '503 Service Unavailable' for url '<url>
    01:14:46 - LiteLLM:ERROR: batch_utils.py:55 - Azure Sentinel Error sending batch API - Server error '503 Service Unavailable' for url '<url>
    01:14:51 - LiteLLM:ERROR: batch_utils.py:55 - Azure Sentinel Error sending batch API - Server error '503 Service Unavailable' for url '<url>
    01:14:56 - LiteLLM:ERROR: batch_utils.py:55 - Azure Sentinel Error sending batch API - Server error '503 Service Unavailable' for url '<url>
    

    (one pair per flush, the audit stream's twin lines omitted for brevity)

  4. Readback with the LIT5899_REC_ markers

    TableName      FirstSeen                     Marker            Records
    PrimaryResult  2026-09-05T08:15:01.8701628Z  LIT5899_REC_head  3
    
    TableName      FirstSeen                     Marker               Records
    PrimaryResult  2026-09-05T08:15:02.0582746Z  LIT5899_REC_S5_head  1
    

Retry cadence and concurrent threshold sends with DEFAULT_BATCH_SIZE=4 (396f0ee)

Same forwarder, with DEFAULT_BATCH_SIZE=4 exported on all proxies. The new leg sent concurrent requests while the endpoint returned 503, then read back the real workspace

  1. Twelve concurrent requests per side, one worker, 22 seconds under fault, then 15 seconds after recovery

    Forwarder 503 attempts: base 9, prev 9, head 5
    Log Analytics records:  base 0, prev 12, head 12
    
  2. Forty-eight concurrent requests per side, four workers, 22 seconds under fault, then 15 seconds after recovery

    Forwarder 503 attempts: base 32, prev 45, head 21
    Log Analytics records:  base 0, prev 48, head 48
    Workers started:       4 per proxy
    

    The previous head let concurrent threshold waiters resend the growing queue after the first failure. The new head rechecks the retry flag and threshold under the flush lock, so one failed threshold attempt is followed by timer retries only, and recovery delivers every record exactly once

  3. The earlier sequential leg remains covered: base indexed 17 records for 9 requests under overlapping flushes, while the fixed head indexed exactly 9; during a 22-second outage, the fixed head retried once per 5-second flush interval and delivered all standard and audit records after recovery

Reactive halving on a live 413 (4b78699)

Azure's cap equals litellm's 1MB estimate, so the real destination never exercises the halve-on-413 branch. For this leg the forwarder answered 413 for any body over 300KB and relayed smaller bodies to the real ingestion endpoint. Six concurrent 240KB requests per side, DEFAULT_BATCH_SIZE=8

Forwarder, base:  413 bytes=1261033 (five records, dropped), 204 bytes=252237 (the straggler that flushed alone)
Forwarder, head:  413 bytes=756608, 413 bytes=756624, 413 bytes=504411, 413 bytes=504430, then six 204 forwards of ~252KB each

Log Analytics:    LIT5899_R7D_S3_BASE 1 record, LIT5899_R7D_S3_HEAD 6 records

The head split the six records into two 756KB halves, each 413 was halved again, and every single-record body was accepted; no error line was logged. The base dropped its five-record batch on the first 413

Type

🐛 Bug Fix

Caveats (if any)

Medium

  • A single record over 1MB on its own is still dropped, with an error line naming it
  • Records held across a long outage are capped at max_queue_size (default 50000), oldest dropped first

Low

  • While records are waiting for a retry, hitting batch_size no longer triggers a send; the periodic flush is the only retry path, so a failed backlog is attempted once per flush_interval (default 5s) instead of once per new event
  • Other CustomBatchLogger subclasses still decide their batch-size sends outside the flush lock, and this PR only changes Azure Sentinel, so that stays for a follow-up
  • A record that cannot be serialized (a mixed-type set in the payload, for example) is dropped on its own with an error line; before this PR it raised out of the flush and stopped the periodic flush task
  • Datadog now logs Datadog delivered N records and Datadog API error: status_code=
    • the previous texts were Datadog: delivered N events and Datadog: unexpected response status_code=
    • anything alerting on the old literal text needs the new pattern
  • Permanent 4xx responses other than 413, including OAuth token failures, are dropped instead of retried forever for Sentinel; Datadog keeps its existing requeue behavior for every non-413 HTTP failure, including permanent 4xx responses
  • The shared helper defaults to requeueing on a non-413 failure, so a future integration that adopts it keeps its records unless it asks for dropping; Sentinel passes undelivered_after_http_error explicitly
  • Sentinel recovery requests are capped at batch_size records, and cancellation requeues only the unsent suffix

Final exhaustive live matrix (4b78699)

The final head was re-driven base-versus-head across every remaining Azure Sentinel and Datadog branch against real Azure Monitor and Datadog destinations. HTTP faults were injected only at destination or OAuth boundaries

  • Sentinel: permanent 401 and 403 drops; transient 408, 429, 503, malformed-token, whole-send cancellation, and mid-split cancellation recovery; independent standard and audit queues; zero-sized retry cap; unserializable-record isolation; proactive and reactive 413 splitting; fresh clean-marker real-Azure delivery
  • Datadog: terminal 413; non-413 429 and 503 compatibility; reactive and proactive splitting; whole-send and mid-split cancellation; unserializable-record isolation; real Logs API readback
  • Readback: head recovered four distinct records from each transient OAuth leg, all four whole-cancel Sentinel records, all four mid-split Sentinel records once, four standard plus one audit record after a shared 503, all 1 + 6 + 3 clean-marker Azure records, six reactive-split Datadog records per side, all four and six Datadog cancellation records, and exactly three good poison-sibling events
  • Base controls reproduced the expected loss or poison wedge; Datadog response and split ordering stayed compatible. No changed live path remains untested and no regression or backward-incompatible risk was found

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

Note

Medium Risk
Changes core logging delivery semantics for Azure Sentinel (retry vs drop on 4xx, queue behavior under failure) and refactors Datadog’s batch send path; misconfiguration could now drop permanent 4xx batches in Sentinel where Datadog still requeues.

Overview
Fixes Azure Sentinel (and hardens Datadog) so proxy logs are not lost when batches exceed Azure Monitor’s ~1MB limit or when ingestion/OAuth fails transiently.

Shared batch_utils: New send_batch_with_413_split proactively splits oversized batches, halves on 413, drops only a single record that still 413s, isolates unserializable records, and on cancel requeues only the suffix that was never accepted. HTTP handling distinguishes retryable errors (5xx, 408, 429) from permanent 4xx via undelivered_after_http_error (Sentinel) vs Datadog’s existing “requeue all non-413” behavior.

Azure Sentinel: Sends through the shared helper with AZURE_SENTINEL_MAX_PAYLOAD_SIZE_BYTES (1MB) and per-request batch_size caps. Queues are detached before send and merged back with undelivered items (with max_queue_size trimming). logs_awaiting_retry / audit_logs_awaiting_retry block batch-size triggers while a backlog is pending so retries run on the periodic flush only. Threshold sends use flush_lock with rechecks to avoid duplicate sends and ordering bugs under concurrency.

Datadog: Replaces inline 413/split loop with the same helper; adds tests for serialization failures and mid-split cancellation.

Reviewed by Cursor Bugbot for commit 4b78699. Bugbot is set up for automated code reviews on this repo. Configure here.

ran /live-pr-risk and found no regressions/backward incompatible risks

…ep undelivered records queued

Azure Monitor rejects any Logs Ingestion body over 1MB with a 413. The Sentinel logger
posted the whole queue as one body and cleared it in a finally block, so an oversize
batch, a transient 5xx, or a failed token call dropped every queued record, and records
logged while a send was in flight were cleared with it. Both the standard and the audit
queue share the sender.

Move Datadog's proactive size split and 413 halving into a shared helper,
litellm/integrations/batch_utils.send_batch_with_413_split, and route Sentinel through it
with a 1MB size check. A lone record that still 413s is dropped, everything a transient
failure leaves undelivered goes back to the front of its queue, and the retry queue is
capped at max_queue_size so an unreachable workspace cannot grow memory without bound
@yucheng-berri
yucheng-berri requested a review from a team September 5, 2026 08:39
@codspeed-hq

codspeed-hq Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_lit5899_azure_sentinel_batch_split (4b78699) with litellm_internal_staging (0ad361a)

Open in CodSpeed

@greptile-apps

greptile-apps Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR makes Azure Sentinel delivery resilient to oversized payloads, transient failures, concurrent flushes, and cancellation while sharing the 413-splitting implementation with Datadog. The latest changes make requeueing the safe shared default while Azure Sentinel explicitly retains its policy of dropping permanent non-413 client rejections.

  • Splits batches under destination limits and recursively handles unexpected 413 responses
  • Preserves undelivered records without duplicating already accepted chunks
  • Serializes threshold and timer flushes to prevent overlapping sends
  • Keeps Datadog's prior behavior of requeueing every non-413 HTTP failure
  • Adds focused coverage for splitting, retries, cancellation, serialization failures, and concurrent flushes

Confidence Score: 5/5

The PR appears safe to merge because current callers explicitly select their intended non-success behavior and no outstanding failure remains

All previous findings are resolved, and the latest default change does not alter either production integration because Datadog explicitly requeues while Azure Sentinel explicitly drops permanent non-413 client failures

Important Files Changed

Filename Overview
litellm/integrations/batch_utils.py Adds shared batch splitting and cancellation-aware delivery, with requeueing as the default non-success policy
litellm/integrations/azure_sentinel/azure_sentinel.py Adds bounded retry queues, serialized flushing, payload splitting, and an explicit permanent-4xx drop policy
litellm/integrations/datadog/datadog.py Adopts the shared split helper while explicitly preserving Datadog's existing requeue behavior
tests/test_litellm/integrations/test_azure_sentinel.py Covers standard and audit delivery across size limits, failures, retries, cancellation, and concurrency
tests/test_litellm/integrations/datadog/test_datadog_logger_batching.py Covers Datadog cancellation, serialization isolation, and requeue compatibility across HTTP failures

Reviews (9): Last reviewed commit: "fix(batch_utils): requeue by default and..." | Re-trigger Greptile

greptile-apps[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

self._drop_oldest_over_max_queue_size(self.audit_log_queue, "audit logs")

def _drop_oldest_over_max_queue_size(self, queue: list[_QueuedPayload], log_type: str) -> None:
overflow: Final = len(queue) - self.max_queue_size

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Low: Retry queue can exhaust proxy memory

The retry queue is bounded by record count, although standard log records contain user-controlled messages and can vary significantly in size. While Azure ingestion is returning transient errors, an authenticated user can submit large requests until retained payloads exhaust proxy memory; each subsequent threshold flush also repeatedly serializes the retained queue. Add a serialized-byte limit for retained records, or use a substantially smaller integration-specific retry budget, and drop oldest records until both limits are satisfied.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

max_queue_size is the CustomBatchLogger contract every batch logger shares, and operators can lower it. Repeated serialization is gone, retries wait for the timer

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for the detail. I can't automatically confirm this is safe to dismiss, so I'm leaving the thread open for a maintainer to make the call.

@veria-ai

veria-ai Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

PR overview

This pull request updates the Azure Sentinel integration to split ingestion batches so each stays below Azure's 1 MB limit.

Two queue-management issues remain open in the Azure Sentinel and Datadog integrations. Under failing or rejected ingestion requests, an authenticated user could cause retained log data to grow until the proxy exhausts memory, resulting in a denial of service. No reported issues have yet been addressed.

Open issues (2)

Fixed/addressed: 0 · PR risk: 5/10

@codecov

codecov Bot commented Sep 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.35099% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...ellm/integrations/azure_sentinel/azure_sentinel.py 95.83% 3 Missing ⚠️
litellm/integrations/batch_utils.py 98.57% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

Requeued records made every later event cross the batch_size threshold, so a
down ingestion endpoint got one full-queue resend per request. Threshold sends
now go through flush_queue, so they take the flush lock instead of racing the
timer, and they stand down while records are awaiting retry.

A record that cannot be serialized raised out of the size probe and killed the
periodic flush task. The probe now runs inside the failure handling, so the
batch is split and only the record that cannot be serialized is dropped.
@yucheng-berri

Copy link
Copy Markdown
Contributor Author

@greptileai please review the current head ef94b7d

@yucheng-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

devin-ai-integration[bot]

This comment was marked as resolved.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Concurrent callbacks all read logs_awaiting_retry before the first send
finished, so each one resent the whole queue once that send failed. The
flag and the batch_size threshold are now rechecked while holding the
flush lock, and each queue sends only itself instead of going through
flush_queue, which was retrying the other queue too.
@yucheng-berri

Copy link
Copy Markdown
Contributor Author

@greptileai please review the current head 396f0ee

@yucheng-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

@yucheng-berri

Copy link
Copy Markdown
Contributor Author

@greptileai please review the current head add9ef4

@yucheng-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

devin-ai-integration[bot]

This comment was marked as resolved.

greptile-apps[bot]

This comment was marked as resolved.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

… split

A batch over the ingestion cap goes out in pieces, so a cancellation partway
through requeued pieces the destination had already accepted and sent them a
second time on the next flush

The split helper now raises a cancellation carrying the records it never
delivered, and Azure Sentinel requeues those instead of the whole batch
@yucheng-berri

Copy link
Copy Markdown
Contributor Author

@greptileai please review the current head 577ff5c

@yucheng-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

…cept

A non-413 4xx from the ingestion endpoint or from the OAuth token call means the request
will fail the same way on every retry, so requeueing it held the batch, and every record
logged behind it, until the queue cap dropped them. Retryable statuses (5xx, 408, 429)
still keep the whole batch, and a shared classifier gives Datadog the same rule

The serialization probe now catches any exception, not just TypeError and ValueError,
because safe_dumps hands pydantic models to model_dump and can raise anything. It also
splits on record count, so a recovery flush sends batch_size records per request instead
of serializing the whole requeued queue to measure it

Both integrations re-raise a cancelled send as exactly asyncio.CancelledError. Python
3.12's asyncio.wait_for only translates the exact class into TimeoutError, so the
BatchSendCancelled subclass escaped the logging worker as an unhandled error

The awaiting-retry flag now follows the queue that survived the max_queue_size trim, so
a deployment with the cap at zero is not left waiting for a timer flush with nothing
queued to retry
@yucheng-berri

Copy link
Copy Markdown
Contributor Author

@greptileai please review the current head 0b0e470

@yucheng-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

devin-ai-integration[bot]

This comment was marked as resolved.

Comment thread litellm/integrations/batch_utils.py
Annotate the queue detach and requeue constructions required by the logger's appendable queue contract so the type-discipline budget stays clean
@yucheng-berri

Copy link
Copy Markdown
Contributor Author

@greptileai please review the current head 16e2e80

@yucheng-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Keep Datadog's existing contract of requeuing every non-413 HTTP failure while Azure Sentinel applies its permanent-client-error policy through the shared splitter
@yucheng-berri

Copy link
Copy Markdown
Contributor Author

@greptileai please review the current head 154ad28

@yucheng-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

success_status_codes=frozenset({202}),
integration_name="Datadog",
drop_error_message=DD_ERRORS.DATADOG_413_ERROR.value,
non_success_handler=requeue_after_http_error,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Low: Permanent failures grow the queue without a bound

This handler requeues records for permanent responses such as 400 and 403. DataDogLogger.flush_queue() bypasses the base class's max_queue_size enforcement, so an authenticated user can continually submit requests while the intake rejects them and grow log_queue until the proxy exhausts memory. Either drop non-retryable responses or enforce the queue limit when records are requeued.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Live base and head 403 legs both requeued records and recovered them after fault removal; queue bounding predates this PR and remains a follow-up

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for the detail. I can't automatically confirm this is safe to dismiss, so I'm leaving the thread open for a maintainer to make the call.

The shared splitter's default non-success handler is now requeue_after_http_error, the behavior Datadog had before the extraction, so a caller that omits the argument keeps its records. Azure Sentinel passes undelivered_after_http_error explicitly to drop permanent 4xx rejections

Also drops an explicit return None the strict ruff gate flags in the test helper
@yucheng-berri

Copy link
Copy Markdown
Contributor Author

@greptileai please review the current head 4b78699, which makes requeue the shared helper default and has Sentinel opt into dropping explicitly

@yucheng-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 4b78699. Configure here.

@yucheng-berri
yucheng-berri merged commit d515a28 into litellm_internal_staging Sep 6, 2026
189 of 192 checks passed
@yucheng-berri
yucheng-berri deleted the litellm_lit5899_azure_sentinel_batch_split branch September 6, 2026 00:15
yucheng-berri added a commit that referenced this pull request Sep 8, 2026
…current events (#40320)

* test(azure_sentinel): pin batch_size as a per-request bound under concurrent events

Adds a regression test to the mapped Azure Sentinel test file for the concurrency scenario from LIT-6920: 40 records logged concurrently at batch_size=5 while each ingestion request is still in flight. Asserts no request carries more than batch_size records, every record arrives exactly once in order, and the queue is empty afterwards. Runs for both the standard log queue and the audit log queue.

The test fails on the tree before #39880 (whole shared queue serialized per threshold send, then cleared) and passes on current staging. It is independent of the size-split coverage that #39880 added for LIT-5899.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(azure_sentinel): gate the first send on events so later records provably arrive while it is in flight

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: yucheng <yucheng@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
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.

[Bug]: Azure Sentinel logging fails due to Azure limits

2 participants