<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Alloy on ICE-ICE-BEAR-BLOG</title><link>https://ice-ice-bear.github.io/tags/alloy/</link><description>Recent content in Alloy on ICE-ICE-BEAR-BLOG</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Fri, 17 Apr 2026 00:00:00 +0900</lastBuildDate><atom:link href="https://ice-ice-bear.github.io/tags/alloy/index.xml" rel="self" type="application/rss+xml"/><item><title>hybrid-image-search-demo Dev Log #16 — Lens Preset Expansion, Hover Previews, OTLP Telemetry</title><link>https://ice-ice-bear.github.io/posts/2026-04-17-hybrid-search-dev16/</link><pubDate>Fri, 17 Apr 2026 00:00:00 +0900</pubDate><guid>https://ice-ice-bear.github.io/posts/2026-04-17-hybrid-search-dev16/</guid><description>&lt;img src="https://ice-ice-bear.github.io/" alt="Featured image of post hybrid-image-search-demo Dev Log #16 — Lens Preset Expansion, Hover Previews, OTLP Telemetry" /&gt;&lt;h2 id="overview"&gt;Overview
&lt;/h2&gt;&lt;p&gt;Three commits, three themes. Lens presets expanded to 5 general options plus a beauty-specific Briese lighting preset. AnglePicker and LensPicker gained 31 hover-preview thumbnails so users can see what each preset actually produces before clicking. The headline work was wiring the production FastAPI backend&amp;rsquo;s traces, metrics, and logs to a local Grafana Alloy agent over OTLP, which forwards to Grafana Cloud. The same interval saw the telemetry&amp;rsquo;s first real use — debugging a user&amp;rsquo;s missing auto-fill tone image by following a trace through Loki. Two sessions, three commits, 5h 54m total.&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://ice-ice-bear.github.io/posts/2026-04-16-hybrid-search-dev15/" &gt;Previous post: hybrid-image-search-demo Dev Log #15&lt;/a&gt;&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph TD
 A["FastAPI backend (prod)"] --&gt;|"OTLP HTTP :4318"| B["Grafana Alloy (per EC2)"]
 B --&gt; C["Grafana Cloud: Traces (Tempo)"]
 B --&gt; D["Grafana Cloud: Metrics (Prometheus)"]
 B --&gt; E["Grafana Cloud: Logs (Loki)"]
 F["logging.getLogger().error(...)"] --&gt;|"stdlib LogRecord"| A
 G["FastAPI span"] --&gt;|"auto-instrumented"| A
 C -. "trace_id link" .-&gt; E&lt;/pre&gt;&lt;h2 id="lens-presets--5-general--1-beauty"&gt;Lens Presets — 5 General + 1 Beauty
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;c4fb076 feat(gen): expand lens presets to 5 general + beauty w/ Briese lighting&lt;/code&gt; touched &lt;code&gt;backend/src/generation/lens_presets.py&lt;/code&gt;. The previous three lens options weren&amp;rsquo;t enough to cover the generation scenarios our users wanted. This change did two things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Expand to 5 general focal lengths&lt;/strong&gt; — 24mm (wide), 35mm (street/environmental), 50mm (natural), 85mm (portrait), 135mm (tight). A standard photography focal-length ladder.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Add a beauty-specific preset — Briese lighting&lt;/strong&gt;. Briese is the large reflector rig used heavily in advertising and beauty photography. This is the first time we&amp;rsquo;ve injected a &lt;em&gt;lighting&lt;/em&gt; directive alongside focal length. &lt;code&gt;prompt.py&lt;/code&gt;&amp;rsquo;s &lt;code&gt;build_generation_prompt&lt;/code&gt; now combines the lens text with the lighting directive when the category is beauty.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Test coverage: one new unit test in &lt;code&gt;backend/tests/test_lens_presets.py&lt;/code&gt; asserts each preset produces the expected string through the prompt builder.&lt;/p&gt;
&lt;p&gt;Frontend &lt;code&gt;LensPicker.tsx&lt;/code&gt; grew its radio options to five and grouped the beauty preset separately. &lt;code&gt;GeneratedImageDetail.tsx&lt;/code&gt; surfaces the selected lens text in the info panel.&lt;/p&gt;
&lt;h2 id="31-hover-preview-thumbnails"&gt;31 Hover-Preview Thumbnails
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;4b886a9 feat(ui): hover-preview examples for angle/lens pickers&lt;/code&gt; is a 31-file commit. Most of those files are the actual example images under &lt;code&gt;frontend/public/preset-examples/angles/*.jpg&lt;/code&gt; and &lt;code&gt;lens/*.jpg&lt;/code&gt; — bird&amp;rsquo;s-eye-view, close-up-cu, dutch-angle, extreme-close-up-ecu, extreme-long-shot-els, eye-level, high-angle, insert-shot, long-shot-ls, low-angle, master-shot, medium-close-up-mcu, and so on.&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph LR
 A["User hovers an angle/lens option"] --&gt; B["AnglePicker/LensPicker computes preview URL"]
 B --&gt; C["/preset-examples/{kind}/{slug}.jpg"]
 C --&gt; D["Floating tooltip renders the image"]
 D --&gt; E["User picks with visual context"]&lt;/pre&gt;&lt;p&gt;A generator script at &lt;code&gt;backend/scripts/generate_preset_examples.py&lt;/code&gt; batch-produced these thumbnails, calling the same generation pipeline from previous posts on a fixed reference character and dumping outputs into &lt;code&gt;frontend/public/preset-examples/&lt;/code&gt;. &lt;code&gt;.gitignore&lt;/code&gt; was updated to exclude the raw source materials.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;AnglePicker.tsx&lt;/code&gt; and &lt;code&gt;LensPicker.tsx&lt;/code&gt; share a floating tooltip pattern on hover. The UX call here is to stop making users pick by jargon alone — &amp;ldquo;extreme-long-shot (ELS)&amp;rdquo; is opaque if you&amp;rsquo;ve never shot cinema, but a thumbnail communicates it instantly.&lt;/p&gt;
&lt;h2 id="grafana-otlp-telemetry"&gt;Grafana OTLP Telemetry
&lt;/h2&gt;&lt;p&gt;The weight of the interval is &lt;code&gt;7a55e9b feat(telemetry): ship prod logs to Alloy/Grafana Cloud via OTLP&lt;/code&gt;. Only four files changed, but it&amp;rsquo;s a significant operational shift.&lt;/p&gt;
&lt;h3 id="the-brief"&gt;The Brief
&lt;/h3&gt;&lt;p&gt;User brief was precise: &amp;ldquo;I&amp;rsquo;m on the free Grafana tier. I&amp;rsquo;d like at least the API logs, or at minimum any API-level error logs. Confirm it&amp;rsquo;s possible under the free tier.&amp;rdquo; Collect prod only, manage the packages globally through &lt;code&gt;pyproject.toml&lt;/code&gt;, enable prod-only via an &lt;code&gt;.env&lt;/code&gt; variable.&lt;/p&gt;
&lt;h3 id="architecture"&gt;Architecture
&lt;/h3&gt;&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart LR
 A["FastAPI app"] --&gt;|"OTLP HTTP"| B["Alloy (localhost:4318)"]
 B --&gt; C["Grafana Cloud OTLP endpoint"]
 C --&gt; D["Tempo / Loki / Prometheus"]
 E["stdlib logging"] --&gt;|"LoggingHandler"| A&lt;/pre&gt;&lt;p&gt;The FastAPI app emits to a local Alloy agent over OTLP HTTP on port 4318. Alloy forwards to Grafana Cloud&amp;rsquo;s OTLP endpoint. This puts Grafana Cloud credentials in Alloy&amp;rsquo;s config instead of the app&amp;rsquo;s environment — rotating prod images doesn&amp;rsquo;t expose the Grafana token.&lt;/p&gt;
&lt;h3 id="implementation"&gt;Implementation
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;backend/src/telemetry.py&lt;/code&gt;&lt;/strong&gt; — initialization behind a &lt;code&gt;_telemetry_enabled&lt;/code&gt; flag gated on &lt;code&gt;DEPLOYMENT_ENV == &amp;quot;prod&amp;quot;&lt;/code&gt;. Traces via &lt;code&gt;OTLPSpanExporter&lt;/code&gt;, metrics via &lt;code&gt;OTLPMetricExporter&lt;/code&gt;, logs via &lt;code&gt;OTLPLogExporter&lt;/code&gt;. Auto-instrumentation through &lt;code&gt;FastAPIInstrumentor&lt;/code&gt;, &lt;code&gt;SQLAlchemyInstrumentor&lt;/code&gt;, and &lt;code&gt;LoggingInstrumentor&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;stdlib logging → OTLP bridge.&lt;/strong&gt; The critical detail. A root-level &lt;code&gt;LoggingHandler&lt;/code&gt; attaches to the stdlib root logger so every &lt;code&gt;logging.getLogger(...)&lt;/code&gt; call (uvicorn access logs, SQLAlchemy chatter, app &lt;code&gt;logger.error&lt;/code&gt;s) ships as an OTLP log. The handler reads the active span context on emit and attaches &lt;code&gt;trace_id&lt;/code&gt; to each LogRecord — so clicking a log line in Grafana jumps to the trace that produced it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Global &lt;code&gt;pyproject.toml&lt;/code&gt; additions.&lt;/strong&gt; &lt;code&gt;opentelemetry-instrumentation-fastapi&lt;/code&gt;, &lt;code&gt;-sqlalchemy&lt;/code&gt;, &lt;code&gt;-logging&lt;/code&gt;, &lt;code&gt;-exporter-otlp-proto-http&lt;/code&gt;, &lt;code&gt;-exporter-otlp-proto-grpc&lt;/code&gt;, all pinned to &lt;code&gt;&amp;gt;=0.54b0&lt;/code&gt; / &lt;code&gt;&amp;gt;=1.33.0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;infra/alloy/config.alloy&lt;/code&gt;&lt;/strong&gt; — Alloy config. OTLP receiver opens grpc on 4317 and http on 4318, passes through a batch processor, forwards to Grafana Cloud. Short and boring, which is the right shape for infra config.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;infra/alloy/SETUP.md&lt;/code&gt;&lt;/strong&gt; — per-EC2 manual install: &lt;code&gt;sudo apt install grafana-alloy&lt;/code&gt;, drop the config, enable via systemd.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="deploy-and-a-pm2-gotcha"&gt;Deploy and a PM2 Gotcha
&lt;/h3&gt;&lt;p&gt;Deployed dev → prod through the &lt;code&gt;/deploy-diff&lt;/code&gt; workflow. Verified traces arriving in Grafana Cloud&amp;rsquo;s Explore view. One trap documented but not yet fixed:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ecosystem.config.js&lt;/code&gt; sets &lt;code&gt;DEPLOYMENT_ENV: process.env.DEPLOYMENT_ENV || &amp;quot;&amp;quot;&lt;/code&gt; — which depends on the PM2 daemon&amp;rsquo;s shell environment. If prod EC2 reboots or &lt;code&gt;pm2 kill&lt;/code&gt; + resurrect runs outside a login shell, &lt;code&gt;DEPLOYMENT_ENV&lt;/code&gt; comes back empty, &lt;code&gt;_telemetry_enabled&lt;/code&gt; flips to false, and telemetry silently dies in prod. Fix is to set &lt;code&gt;Environment=DEPLOYMENT_ENV=prod&lt;/code&gt; in the systemd unit that launches PM2. Recorded for the next interval.&lt;/p&gt;
&lt;h2 id="first-live-use--debugging-a-user-issue"&gt;First Live Use — Debugging a User Issue
&lt;/h2&gt;&lt;p&gt;Session 4 was telemetry&amp;rsquo;s first real workout. A user (&lt;a class="link" href="mailto:khk@diffs.studio" &gt;khk@diffs.studio&lt;/a&gt;) generated an image with the prompt &amp;ldquo;우주의 신비로운 모습&amp;rdquo; around 4/16 13:20, and the auto-fill tone image didn&amp;rsquo;t render in the detail view. Normally this would start with an SSH into the prod box and &lt;code&gt;grep&lt;/code&gt; through files. This time the first query was in Grafana Loki: &lt;code&gt;{service_name=&amp;quot;hybrid-image-search&amp;quot;} |= &amp;quot;khk@diffs.studio&amp;quot;&lt;/code&gt; — which pulled the relevant generation logs immediately.&lt;/p&gt;
&lt;p&gt;The chase turned up three intertwined issues:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;blob:http://...&lt;/code&gt; URL throwing an &amp;ldquo;insecure connection&amp;rdquo; warning — the EC2 host hadn&amp;rsquo;t moved to HTTPS yet.&lt;/li&gt;
&lt;li&gt;A 502 Bad Gateway on a different request — likely to resolve together once the HTTPS + nginx upstream config lands.&lt;/li&gt;
&lt;li&gt;A 401 on a third server from an expired session token that wasn&amp;rsquo;t being refreshed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The workflow pattern was clean. Follow the trace link in Grafana → see the FastAPI span → jump to the connected log record and read the error. What used to be &amp;ldquo;SSH to prod and tail logs&amp;rdquo; became &amp;ldquo;click the trace in Grafana.&amp;rdquo; Fixes deferred to the next interval; the forensics part of this interval was the point.&lt;/p&gt;
&lt;h2 id="commit-log"&gt;Commit Log
&lt;/h2&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Message&lt;/th&gt;
 &lt;th&gt;Changes&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;feat(gen): expand lens presets to 5 general + beauty w/ Briese lighting&lt;/td&gt;
 &lt;td&gt;5 files&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;feat(ui): hover-preview examples for angle/lens pickers&lt;/td&gt;
 &lt;td&gt;31 files (mostly images)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;feat(telemetry): ship prod logs to Alloy/Grafana Cloud via OTLP&lt;/td&gt;
 &lt;td&gt;4 files&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="insights"&gt;Insights
&lt;/h2&gt;&lt;p&gt;The best signal of this interval was that the telemetry work and the telemetry&amp;rsquo;s first real use overlapped in the same interval. &amp;ldquo;Set this up because it&amp;rsquo;ll be useful eventually&amp;rdquo; usually takes weeks to pay off, but the OTLP + Alloy stack got drafted into a real user debug on deployment day. Two effects. First, the shape of what Grafana captures and what it misses is now concrete — trace_id linking between logs and traces works; browser-side errors are not captured (OTLP covers server-side only). Second, the query &amp;ldquo;who ran what prompt when, with what error&amp;rdquo; now has a one-liner in Loki that takes an email and returns ordered log records — the next support ticket answers itself in 2 seconds. Being able to use a tool on the day it arrives is a signal that the tool landed on a real problem, not on a hypothetical one. This interval landed right.&lt;/p&gt;</description></item></channel></rss>