feat(observability): 失败响应与网关信封进链路,手工埋点尊重 suppressed
失败此前在 trace 里近乎不可见:异常处理器把异常吃掉换成信封响应,自动 instrumentation 只看到一个 HTTP 状态码,而 AppError 默认 400、信封里 success=false,跟正常返回分不出来。 - api.py:四个异常处理器(对外失败的唯一出口)各记一次 span;兜底处理器额外 record_error——对外只回一句无信息量的错误文案,异常类型与栈只在本地日志里 - telemetry.py:新增 record_envelope / record_parse_failure / span_unless_suppressed;record_error 补 error.message / retryable / status_code;snapshot 支持 extra 带上「这份 HTML 是哪来的」 - worker/client.py:_request 自建 span,活到解信封之后。httpx 那个 CLIENT span 在 request() 返回时就结束,此时信封还没解——success=false code=6002(租约 无效)在它看来是完成的 200 请求 - span_unless_suppressed:suppress_instrumentation 只被 instrumentation 库尊重, 手工 span 不看它,lease 空转长轮询会从这个口子把孤立 trace 放回来 - 两个 scraping client 的解析失败分支收拢到 record_parse_failure;shop_items 显式标 stage=delegate 且不落快照(它自己不抓页面,按 html 推断只会得出 「fetch 失败」的错误结论) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -37,7 +37,7 @@ from app.scraping.utils.rakuma_urls import (
|
||||
split_item_url,
|
||||
split_shop_url,
|
||||
)
|
||||
from app.shared.telemetry import snapshot
|
||||
from app.shared.telemetry import record_parse_failure
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
tracer = trace.get_tracer(__name__)
|
||||
@@ -77,10 +77,11 @@ class RakumaClient:
|
||||
url, len(result.items), result.total_count,
|
||||
)
|
||||
return result
|
||||
except Exception:
|
||||
span.record_exception()
|
||||
span.set_attribute("parse.fail_reason", "parse_error")
|
||||
snapshot(span, "parse.failed_html", html, self._settings.otel_snapshot_max_bytes)
|
||||
except Exception as exc:
|
||||
record_parse_failure(
|
||||
span, exc, html=html,
|
||||
max_bytes=self._settings.otel_snapshot_max_bytes, url=url,
|
||||
)
|
||||
raise
|
||||
|
||||
async def categories(self, payload: RakumaCategoryRequest) -> RakumaCategoryData:
|
||||
@@ -110,10 +111,11 @@ class RakumaClient:
|
||||
data.category_id, data.name, len(data.children), data.total_count,
|
||||
)
|
||||
return data
|
||||
except Exception:
|
||||
span.record_exception()
|
||||
span.set_attribute("parse.fail_reason", "parse_error")
|
||||
snapshot(span, "parse.failed_html", html, self._settings.otel_snapshot_max_bytes)
|
||||
except Exception as exc:
|
||||
record_parse_failure(
|
||||
span, exc, html=html,
|
||||
max_bytes=self._settings.otel_snapshot_max_bytes, url=url,
|
||||
)
|
||||
raise
|
||||
|
||||
async def item_detail(self, payload: RakumaItemDetailRequest) -> RakumaItemDetailData:
|
||||
@@ -139,10 +141,11 @@ class RakumaClient:
|
||||
url, detail.item_name[:40], detail.price, detail.is_sold_out,
|
||||
)
|
||||
return detail
|
||||
except Exception:
|
||||
span.record_exception()
|
||||
span.set_attribute("parse.fail_reason", "parse_error")
|
||||
snapshot(span, "parse.failed_html", html, self._settings.otel_snapshot_max_bytes)
|
||||
except Exception as exc:
|
||||
record_parse_failure(
|
||||
span, exc, html=html,
|
||||
max_bytes=self._settings.otel_snapshot_max_bytes, url=url,
|
||||
)
|
||||
raise
|
||||
|
||||
async def shop_detail(self, payload: RakumaShopDetailRequest) -> RakumaShopDetailData:
|
||||
@@ -177,10 +180,11 @@ class RakumaClient:
|
||||
shop_id, detail.shop_name, detail.item_count, detail.review_count,
|
||||
)
|
||||
return detail
|
||||
except Exception:
|
||||
span.record_exception()
|
||||
span.set_attribute("parse.fail_reason", "parse_error")
|
||||
snapshot(span, "parse.failed_html", html, self._settings.otel_snapshot_max_bytes)
|
||||
except Exception as exc:
|
||||
record_parse_failure(
|
||||
span, exc, html=html,
|
||||
max_bytes=self._settings.otel_snapshot_max_bytes, url=url,
|
||||
)
|
||||
raise
|
||||
|
||||
async def shop_items(self, payload: RakumaShopItemsRequest) -> RakumaShopItemsData:
|
||||
@@ -205,10 +209,11 @@ class RakumaClient:
|
||||
shop_id, len(result.items), result.total_count,
|
||||
)
|
||||
return result
|
||||
except Exception:
|
||||
span.record_exception()
|
||||
span.set_attribute("parse.fail_reason", "parse_error")
|
||||
snapshot(span, "parse.failed_html", html, self._settings.otel_snapshot_max_bytes)
|
||||
except Exception as exc:
|
||||
record_parse_failure(
|
||||
span, exc, html=html,
|
||||
max_bytes=self._settings.otel_snapshot_max_bytes, url=url,
|
||||
)
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -45,7 +45,7 @@ from app.scraping.utils.urls import (
|
||||
split_item_url,
|
||||
split_shop_url,
|
||||
)
|
||||
from app.shared.telemetry import snapshot
|
||||
from app.shared.telemetry import record_parse_failure
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
tracer = trace.get_tracer(__name__)
|
||||
@@ -87,10 +87,11 @@ class RakutenClient:
|
||||
url, len(result.items), result.ad_count, result.total_count,
|
||||
)
|
||||
return result
|
||||
except Exception:
|
||||
span.record_exception()
|
||||
span.set_attribute("parse.fail_reason", "parse_error")
|
||||
snapshot(span, "parse.failed_html", html, self._settings.otel_snapshot_max_bytes)
|
||||
except Exception as exc:
|
||||
record_parse_failure(
|
||||
span, exc, html=html,
|
||||
max_bytes=self._settings.otel_snapshot_max_bytes, url=url,
|
||||
)
|
||||
raise
|
||||
|
||||
async def genres(self, payload: GenreRequest) -> GenreData:
|
||||
@@ -113,10 +114,11 @@ class RakutenClient:
|
||||
result.genre_id or "root", result.name, len(result.children),
|
||||
)
|
||||
return result
|
||||
except Exception:
|
||||
span.record_exception()
|
||||
span.set_attribute("parse.fail_reason", "parse_error")
|
||||
snapshot(span, "parse.failed_html", html, self._settings.otel_snapshot_max_bytes)
|
||||
except Exception as exc:
|
||||
record_parse_failure(
|
||||
span, exc, html=html,
|
||||
max_bytes=self._settings.otel_snapshot_max_bytes, url=url,
|
||||
)
|
||||
raise
|
||||
|
||||
async def shop_detail(self, payload: ShopDetailRequest) -> ShopDetailData:
|
||||
@@ -142,10 +144,11 @@ class RakutenClient:
|
||||
result.shop_code, result.shop_id, result.shop_name, result.review_count,
|
||||
)
|
||||
return result
|
||||
except Exception:
|
||||
span.record_exception()
|
||||
span.set_attribute("parse.fail_reason", "parse_error")
|
||||
snapshot(span, "parse.failed_html", html, self._settings.otel_snapshot_max_bytes)
|
||||
except Exception as exc:
|
||||
record_parse_failure(
|
||||
span, exc, html=html,
|
||||
max_bytes=self._settings.otel_snapshot_max_bytes, url=url,
|
||||
)
|
||||
raise
|
||||
|
||||
async def shop_items(self, payload: ShopItemsRequest) -> SearchResultData:
|
||||
@@ -170,9 +173,11 @@ class RakutenClient:
|
||||
span.set_attribute("parse.items", len(result.items))
|
||||
span.set_attribute("parse.total", result.total_count)
|
||||
return result
|
||||
except Exception:
|
||||
span.record_exception()
|
||||
span.set_attribute("parse.fail_reason", "parse_error")
|
||||
except Exception as exc:
|
||||
# 本方法自己不抓页面,失败一定发生在它转调的 shop_detail / search
|
||||
# 里(那两个 span 已各自记了自己的失败页面),所以显式标 delegate、
|
||||
# 不落快照:按 html 推断只会得出「fetch 失败」的错误结论。
|
||||
record_parse_failure(span, exc, stage="delegate")
|
||||
raise
|
||||
|
||||
async def item_detail(self, payload: ItemDetailRequest) -> ItemDetailData:
|
||||
@@ -226,8 +231,9 @@ class RakutenClient:
|
||||
url, detail.source, detail.item_name[:40], detail.price, detail.sku.variant_count,
|
||||
)
|
||||
return detail
|
||||
except Exception:
|
||||
span.record_exception()
|
||||
span.set_attribute("parse.fail_reason", "parse_error")
|
||||
snapshot(span, "parse.failed_html", html, self._settings.otel_snapshot_max_bytes)
|
||||
except Exception as exc:
|
||||
record_parse_failure(
|
||||
span, exc, html=html,
|
||||
max_bytes=self._settings.otel_snapshot_max_bytes, url=url,
|
||||
)
|
||||
raise
|
||||
|
||||
Reference in New Issue
Block a user