0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

provider_connection_error を upstream 障害と設定ミスで切り分けるログにした

0
Posted at

はじめに

LLM API gateway を本番寄りに触っていると、たまに provider_connection_errorupstream connection failed 系のエラーに当たります。

私が最初にハマったのは、これを見た瞬間に「自分の API key か model 指定が悪いのでは」と寄せすぎたことでした。実際には、TCP 接続の失敗、provider 側の一時障害、router 側の設定、model route の指定ミスが同じような見た目になることがあります。

この記事では local fixture で connection error を再現し、request_idroutemodelprovider_statusretry_count を 1 セットで残す形にしました。実在 provider の障害を断定する記事ではありません。

3行まとめ

  • provider_statusnull なら、provider から HTTP response を受け取る前に落ちた可能性を見る。
  • provider_status4014044295xx なら、接続ではなく status ごとの切り分けに移る。
  • request_idroutemodelprovider_statusretry_count を同じログ行に置くと、upstream 起因と設定ミスを混ぜにくいです。

再現環境

今回の fixture は Python 標準ライブラリだけで作りました。

python3 agents/qiita-growth-agent/results/qiita_issue_321_provider_connection_fixture.py

やっていることは単純です。

  1. 使われていない localhost port に POST して connection refused を起こす。
  2. gateway が返す想定の provider_connection_error body とログ行を作る。
  3. 同じ route/model で mock upstream を立て直し、200 が返る fixed path を確認する。

実 provider には接続していないので、これは outage の証明ではなく、ログ設計の再現 fixture です。

エラー全文

local fixture で残したエラー body はこれです。

{
  "error": {
    "message": "provider_connection_error: upstream connection failed before an HTTP response was received",
    "type": "provider_connection_error",
    "code": "upstream_connection_failed",
    "param": null
  },
  "request_id": "req_321_bad_upstream"
}

この時点では provider_status がありません。HTTP response を受け取れていないので、502503 と同じ表で雑に扱うと少し危ないと思います。

ログで見る順番

私が最初に見るログは、エラー文そのものよりこの 1 行です。

{"client_request_id":"client_321","duration_ms":24,"error_code":"upstream_connection_failed","error_type":"provider_connection_error","event":"llm_gateway.upstream_connection_failed","level":"error","model":"gpt-5.4-nano","provider":"local-fixture-upstream","provider_status":null,"request_id":"req_321_bad_upstream","retry_count":2,"route":"chat.completions.primary","timestamp":"2026-07-17T00:30:00Z","upstream_host":"127.0.0.1"}

見る順番はだいたい次のようにしています。

見る項目 見方
request_id ユーザー報告、gateway log、provider log をつなぐ軸にする
route どの routing rule に入ったかを見る
model model 名の typo、非対応 model、別 provider 向け model ではないかを見る
provider_status null なら HTTP 前、数字なら status table に移る
retry_count 1 回だけの失敗か、retry 後も同じ失敗かを見る

ここで provider_status: nullretry_count: 2 なら、私はまず DNS、network、TLS、provider endpoint 到達性、proxy 設定を見ます。逆に provider_status: 401 なら API key や provider account、404 なら endpoint/model route、429 なら quota と rate limit に寄せます。

upstream 寄りに見るケース

provider_statusnull のときは、まだ provider から HTTP として怒られていない状態です。ここでいきなり model 名を直したり key を差し替えたりすると、たまたま直ったように見えて原因が残ることがあります。

私はこの順番で見ます。

  • 同じ provider の別 model も失敗しているか。
  • 同じ route の fallback 先も失敗しているか。
  • 特定の runtime、region、container だけで失敗していないか。
  • proxy や egress firewall の変更が直近にないか。
  • provider の status page や社内 incident channel に同時刻の情報がないか。

特に Connection refused、名前解決失敗、TLS handshake 失敗、timeout は、アプリの payload より前で落ちています。もちろん gateway の upstream URL 設定ミスでも同じ症状になるので、ここでは provider 障害と断定せず、「HTTP response 前の失敗」とだけ置くのがよさそうです。

設定ミス寄りに見るケース

一方で、見た目が connection 系でも provider_status に数字が入っているなら、私は普通の HTTP 切り分けに戻します。

401 なら key、header、provider account の権限を見ます。404 なら endpoint path、model 名、OpenAI 互換 endpoint と provider 固有 endpoint の取り違えを見ます。429 なら rate limit、quota、billing、同時実行数を見ます。5xx は upstream 側の一時失敗かもしれませんが、自分の request が大きすぎる、streaming 設定が provider と合っていない、特定 route だけ古い upstream URL を向いている、ということもあります。

このへんを全部エラー名だけで見るとつらいので、routemodel を同じ行に残しておくのが効きました。provider_connection_error と書いてあっても、どの route に入り、どの provider に行こうとして、どこまで HTTP として進んだかを分けるだけで、調査の初手がかなり変わります。

解決した状態

mock upstream を立てた fixed path では、同じ route/model で 200 が返りました。

{"client_request_id":"client_321","duration_ms":0,"error_code":null,"error_type":null,"event":"llm_gateway.upstream_request_succeeded","level":"info","model":"gpt-5.4-nano","provider":"local-fixture-upstream","provider_status":200,"request_id":"req_321_fixed_path","retry_count":0,"route":"chat.completions.primary","timestamp":"2026-07-17T00:30:00Z","upstream_host":"127.0.0.1"}

この fixture では、設定の考え方としては「同じ route と model のまま、upstream endpoint が到達可能になれば成功する」を確認できました。実環境ならここに provider の status page、region、proxy、egress firewall、router の upstream URL、model route の fallback 設定を重ねて見ます。

実装するときの最小ログ

本文用に少し短くすると、私はこのくらいを allowlist にします。

SAFE_LLM_LOG_FIELDS = {
    "request_id",
    "client_request_id",
    "route",
    "model",
    "provider",
    "provider_status",
    "retry_count",
    "duration_ms",
    "error_type",
    "error_code",
}

def llm_error_log(event: dict) -> dict:
    return {key: event.get(key) for key in SAFE_LLM_LOG_FIELDS}

messagespromptrequest_bodyresponse_bodyauthorizationapi_key は入れません。調査したい気持ちはあるのですが、あとから消すのがつらいログを増やすより、まず operational field をそろえる方が現実的だと思います。

Flatkey AI ではどこを見るか

Flatkey AI のような OpenAI 互換 gateway を挟む場合、私はアプリ側のログと gateway 側の visibility を分けて見ます。

アプリ側では request_idclient_request_id を出し、どの user action から来た呼び出しかを追えるようにします。gateway 側では route、model、usage、provider status、retry の見え方を確認します。Flatkey AI は複数 provider account の routing と load balancing、key、usage、billing を dashboard で見る前提なので、connection error のときも「自分の app config」と「upstream 到達性」を分ける確認材料になります。

ただし、dashboard に出ている失敗だけで provider 障害と断定しない方がよいです。同じ時間帯に同じ provider だけ失敗しているのか、特定 route だけなのか、特定 model だけなのかを request_id 単位で見るのが大事だと思います。

まとめ

provider_connection_error を見たとき、私はまず自分の設定ミスを疑いがちでした。今は次の順番にしています。

  1. request_id で対象 request を固定する。
  2. routemodel で routing/model 指定を確認する。
  3. provider_statusnull か数字かを見る。
  4. retry_count で一時失敗か継続失敗かを見る。
  5. null なら DNS/network/TLS/proxy/upstream endpoint、数字なら HTTP status ごとの切り分けに移る。

エラー名だけだと upstream 由来か設定ミスかを混ぜやすいので、ログの 1 セットを先に決めておくのが一番効いた気がします。

間違いあったらコメントください。よろしくお願いします。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?