<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sumit Mishra</title>
    <description>The latest articles on DEV Community by Sumit Mishra (@sumit_mishra_cb22d5be5f02).</description>
    <link>https://dev.to/sumit_mishra_cb22d5be5f02</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4034905%2Fa233fb4c-8e89-45e1-a6a2-4634b86d5366.jpg</url>
      <title>DEV Community: Sumit Mishra</title>
      <link>https://dev.to/sumit_mishra_cb22d5be5f02</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sumit_mishra_cb22d5be5f02"/>
    <language>en</language>
    <item>
      <title>My Headless LinkedIn Bot Fails Silently - Until I Point SigNoz at It</title>
      <dc:creator>Sumit Mishra</dc:creator>
      <pubDate>Sat, 18 Jul 2026 08:05:52 +0000</pubDate>
      <link>https://dev.to/sumit_mishra_cb22d5be5f02/my-headless-linkedin-bot-fails-silently-until-i-point-signoz-at-it-4ce6</link>
      <guid>https://dev.to/sumit_mishra_cb22d5be5f02/my-headless-linkedin-bot-fails-silently-until-i-point-signoz-at-it-4ce6</guid>
      <description>&lt;p&gt;&lt;strong&gt;Subtitle:&lt;/strong&gt; From ChromeDriver 151 vs Brave 150 to correlated traces: what I learn instrumenting a Selenium cron job with OpenTelemetry&lt;/p&gt;




&lt;p&gt;Every morning I open yesterday's log file and scroll, guessing.&lt;/p&gt;

&lt;p&gt;Some days the bot sends zero connection requests. Other days it hits 27 of 30 and stops. The script runs headless on a schedule — no browser window, no Slack alert, just a text file that might not tell the whole story.&lt;/p&gt;

&lt;p&gt;I built this for a SigNoz hackathon. The goal isn't a polished observability tutorial. It's to stop flying blind on a real Python + Selenium bot running on my Mac. Here's what actually breaks, what I instrument, and what shows up in SigNoz when I finally look.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Run
&lt;/h2&gt;

&lt;p&gt;The bot sends LinkedIn connection requests up to a daily cap. I run headless Brave on macOS via &lt;code&gt;launchd&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./schedule/install_daily_schedule.sh &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;span class="c"&gt;# At login + hourly while the Mac is on; at most one successful run per day&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Relevant settings from &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;BROWSER_HEADLESS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true
&lt;/span&gt;&lt;span class="nv"&gt;USE_BROWSER_PROFILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true
&lt;/span&gt;&lt;span class="nv"&gt;USE_NETWORK_GROW&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;          &lt;span class="c"&gt;# fallback when people search is exhausted&lt;/span&gt;
&lt;span class="nv"&gt;AUTO_ACCEPT_INVITATIONS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;   &lt;span class="c"&gt;# accept pending invites on Grow first&lt;/span&gt;
&lt;span class="nv"&gt;LINKEDIN_CONNECTIONS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;30
&lt;span class="nv"&gt;LINKEDIN_DRIVER_AUTO_FIX_ATTEMPTS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2

&lt;span class="nv"&gt;SIGNOZ_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true
&lt;/span&gt;&lt;span class="nv"&gt;OTEL_EXPORTER_OTLP_ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:4317
&lt;span class="nv"&gt;OTEL_SERVICE_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;linkedin-connection-bot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When people search runs out, the bot switches to &lt;strong&gt;My Network → Grow&lt;/strong&gt;, accepts incoming invitations, then sends Connect requests. Resume state lets a crashed run pick up on Grow without redoing search. A wrapper script (&lt;code&gt;schedule/daily_run.sh&lt;/code&gt;) handles lock files, stale process cleanup, and one chromedriver auto-fix retry.&lt;/p&gt;

&lt;p&gt;Simple on paper. Messy in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Failures I Can't See From One Log Line
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ChromeDriver 151 vs Brave 150
&lt;/h3&gt;

&lt;p&gt;On July 17, most hourly &lt;code&gt;launchd&lt;/code&gt; checks looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Critical error: Failed to initialize brave: ...
This version of ChromeDriver only supports Chrome version 151
Current browser version is 150.0.7871.125
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Brave hadn't auto-updated. Selenium had cached ChromeDriver 151. The bot never got past browser launch — zero connections, 784 lines of stack trace.&lt;/p&gt;

&lt;p&gt;I add self-healing in &lt;code&gt;Scripts/browser_utils.py&lt;/code&gt;: detect the mismatch, kill stale chromedriver and Brave processes, re-download the matching driver:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;auto_fix_chromedriver_mismatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;browser_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;log_func&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;cleanup_stale_bot_processes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;browser_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;log_func&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;log_func&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resolve_chromedriver_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;browser_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;force_refresh&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;log_func&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Auto-fix: resolved chromedriver at &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That fixed launches once Brave caught up. But I still want to &lt;em&gt;know&lt;/em&gt; when it happens without grepping logs at 7 a.m.&lt;/p&gt;

&lt;h3&gt;
  
  
  Almost Done: 27/30 and a Hangup
&lt;/h3&gt;

&lt;p&gt;Late on July 17, after auto-fix worked, the bot resumed Grow-only mode and sent connections steadily — until:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Progress: 27/30 connections sent &lt;span class="o"&gt;(&lt;/span&gt;grow&lt;span class="o"&gt;)&lt;/span&gt;
Progress: 28/30 connections sent &lt;span class="o"&gt;(&lt;/span&gt;grow&lt;span class="o"&gt;)&lt;/span&gt;
Cleaning up bot browser processes...
./schedule/daily_run.sh: line 19: 52556 Hangup: 1               &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VENV_PYTHON&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; run_bot.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two short of the cap. Killed by SIGHUP. Without exported counters, the only signal is a progress line buried in a 700-line file.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stale Locks
&lt;/h3&gt;

&lt;p&gt;While one process is still running, the next hourly check logs: &lt;code&gt;Another bot run is already in progress (pid 51947). Skipping.&lt;/code&gt; Earlier that evening I saw: &lt;code&gt;Removing stale lock file.&lt;/code&gt; The lock logic is correct. Interrupted runs still leave footguns. Metrics like &lt;code&gt;bot.driver_errors&lt;/code&gt; and &lt;code&gt;bot.session_crashes&lt;/code&gt; surface this on a dashboard instead of me tailing files.&lt;/p&gt;

&lt;h2&gt;
  
  
  One Module, Three Signals
&lt;/h2&gt;

&lt;p&gt;Everything lives in &lt;code&gt;Scripts/telemetry.py&lt;/code&gt; — OTLP gRPC to port 4317, gated by &lt;code&gt;SIGNOZ_ENABLED&lt;/code&gt;. Each run gets a 12-character &lt;code&gt;bot.run_id&lt;/code&gt; on traces, metrics, and logs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spans:&lt;/strong&gt; &lt;code&gt;bot.run&lt;/code&gt;, &lt;code&gt;bot.configure_browser&lt;/code&gt;, &lt;code&gt;bot.login&lt;/code&gt;, &lt;code&gt;bot.search_page&lt;/code&gt;, &lt;code&gt;bot.accept_invitations&lt;/code&gt;, &lt;code&gt;bot.connect_from_grow&lt;/code&gt;, &lt;code&gt;bot.recover_session&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Metrics:&lt;/strong&gt; &lt;code&gt;bot.connections_sent&lt;/code&gt; (label &lt;code&gt;source=search|grow&lt;/code&gt;), &lt;code&gt;bot.invitations_accepted&lt;/code&gt;, &lt;code&gt;bot.driver_errors&lt;/code&gt; (&lt;code&gt;error_type=version_mismatch&lt;/code&gt;), &lt;code&gt;bot.session_crashes&lt;/code&gt;, &lt;code&gt;bot.run_success&lt;/code&gt; / &lt;code&gt;bot.run_failure&lt;/code&gt;, &lt;code&gt;bot.run_duration_seconds&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Main loop wiring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;tel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init_telemetry&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;tel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bot run started&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;phase&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bot.run&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;phase&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;configurations&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;webpage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;tel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record_run_outcome&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;run_failed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;connections_sent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;connections_sent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;tel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record_run_duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;run_started_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On driver mismatch: &lt;code&gt;tel.record_driver_error(error_type="version_mismatch")&lt;/code&gt; before auto-fix runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Logs&lt;/strong&gt; append &lt;code&gt;run_id&lt;/code&gt; and &lt;code&gt;phase&lt;/code&gt; for trace correlation in SigNoz Logs Explorer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Verify telemetry smoke test | run_id=87626c6dc93a phase=verify
trace_id=7b69827901b867b9f0658ed774ff684d  span_id=0e08651f63956788
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One lesson I keep relearning: logs need explicit OTLP export. &lt;code&gt;telemetry.py&lt;/code&gt; wires a &lt;code&gt;LoggingHandler&lt;/code&gt; plus &lt;code&gt;OTLPLogExporter&lt;/code&gt; — stdout files alone aren't enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three-Pillar Correlation — The Part I Care About Most
&lt;/h2&gt;

&lt;p&gt;SigNoz's real payoff for me isn't three separate tabs. It's jumping between logs, traces, and metrics without rebuilding queries. &lt;a href="https://signoz.io/docs/what-is-signoz/" rel="noopener noreferrer"&gt;Unified observability&lt;/a&gt; means one &lt;code&gt;run_id&lt;/code&gt; on every signal, and SigNoz UI links that carry filters and time range when I drill down (&lt;a href="https://signoz.io/docs/dashboards/interactivity/" rel="noopener noreferrer"&gt;dashboard interactivity&lt;/a&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  How my bot wires it
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pillar&lt;/th&gt;
&lt;th&gt;What I emit&lt;/th&gt;
&lt;th&gt;Correlation hook&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Traces&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Spans like &lt;code&gt;bot.run&lt;/code&gt;, &lt;code&gt;bot.login&lt;/code&gt;, &lt;code&gt;bot.connect_from_grow&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;bot.run_id&lt;/code&gt; span attribute; root span wraps the whole cron run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Metrics&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;bot.connections_sent&lt;/code&gt;, &lt;code&gt;bot.run_success&lt;/code&gt;, etc.&lt;/td&gt;
&lt;td&gt;Same &lt;code&gt;bot.run_id&lt;/code&gt; label on every counter/histogram sample&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Logs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;log_event()&lt;/code&gt; lines with &lt;code&gt;run_id=&lt;/code&gt; and &lt;code&gt;phase=&lt;/code&gt; in the body&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;LoggingInstrumentor&lt;/code&gt; injects &lt;code&gt;trace_id&lt;/code&gt; / &lt;code&gt;span_id&lt;/code&gt; when the log fires inside an active span (&lt;a href="https://signoz.io/docs/traces-management/guides/correlate-traces-and-logs/" rel="noopener noreferrer"&gt;official guide&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Logs emitted &lt;strong&gt;inside&lt;/strong&gt; a span carry both domain context (&lt;code&gt;run_id=87626c6dc93a phase=verify&lt;/code&gt;) and OTel trace context (&lt;code&gt;trace_id=… span_id=…&lt;/code&gt;). That second pair is what powers SigNoz's &lt;strong&gt;View trace&lt;/strong&gt; button on a log line. Metrics don't get trace IDs on every sample — I correlate them through the shared &lt;code&gt;bot.run_id&lt;/code&gt; label instead. SigNoz also derives APM/RED metrics from span data automatically (&lt;a href="https://signoz.io/docs/traces-management/guides/apm-metrics/" rel="noopener noreferrer"&gt;APM metrics from traces&lt;/a&gt;), which gives me operation-level rates alongside my custom &lt;code&gt;bot.*&lt;/code&gt; counters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Walkthrough: one smoke test, three pillars
&lt;/h3&gt;

&lt;p&gt;After &lt;code&gt;./venv/bin/python Scripts/verify_telemetry.py&lt;/code&gt; prints &lt;code&gt;run_id=87626c6dc93a&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Logs → trace.&lt;/strong&gt; I open &lt;strong&gt;Logs Explorer&lt;/strong&gt;, filter &lt;code&gt;service.name = linkedin-connection-bot&lt;/code&gt;, and find &lt;code&gt;Verify telemetry smoke test | run_id=87626c6dc93a phase=verify&lt;/code&gt;. The row shows &lt;code&gt;trace_id&lt;/code&gt; and &lt;code&gt;span_id&lt;/code&gt;. I click &lt;strong&gt;View trace&lt;/strong&gt; (or the trace ID link). SigNoz opens the &lt;code&gt;bot.verify_telemetry&lt;/code&gt; span — the same run I just emitted.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Trace → logs.&lt;/strong&gt; On that trace's span detail panel I open the &lt;strong&gt;Logs&lt;/strong&gt; tab (&lt;a href="https://signoz.io/docs/userguide/span-details/" rel="noopener noreferrer"&gt;span details&lt;/a&gt;). SigNoz shows the correlated log line from step 1 without me re-filtering by hand. I can also click &lt;strong&gt;Go to related logs&lt;/strong&gt; from the trace list view.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Trace → metrics.&lt;/strong&gt; Back in &lt;strong&gt;Metrics Explorer&lt;/strong&gt; I query &lt;code&gt;bot.connections_sent&lt;/code&gt; with aggregation &lt;strong&gt;Sum&lt;/strong&gt;, group by &lt;code&gt;bot.run_id&lt;/code&gt;, and filter &lt;code&gt;bot.run_id = 87626c6dc93a&lt;/code&gt;. I see &lt;code&gt;source = verify&lt;/code&gt; with value 1 — the counter incremented inside the same span that produced the log. For a demo run with twelve simulated runs (&lt;code&gt;signoz_demo_data.py&lt;/code&gt;), I group by &lt;code&gt;source&lt;/code&gt; and spot the grow spike that matches a &lt;code&gt;bot.connect_from_grow&lt;/code&gt; span in Traces Explorer.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's the July 17 hangup story in reverse: instead of grepping 700 lines for &lt;code&gt;Progress: 27/30&lt;/code&gt;, I click a log from the failed run, jump to &lt;code&gt;bot.run&lt;/code&gt;, and check whether &lt;code&gt;bot.connections_sent&lt;/code&gt; stopped climbing for that &lt;code&gt;run_id&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify correlation in the SigNoz UI (5 minutes)
&lt;/h3&gt;

&lt;p&gt;Run the smoke test, wait ~10 seconds for export, then check each pillar:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;th&gt;What to confirm&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Logs Explorer&lt;/strong&gt; → filter &lt;code&gt;service.name = linkedin-connection-bot&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Body contains &lt;code&gt;run_id=&lt;/code&gt;; log attributes or body include &lt;code&gt;trace_id&lt;/code&gt; and &lt;code&gt;span_id&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Click a log row → &lt;strong&gt;View trace&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Trace opens with span &lt;code&gt;bot.verify_telemetry&lt;/code&gt; and &lt;code&gt;bot.run_id&lt;/code&gt; attribute&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Trace detail → &lt;strong&gt;Logs&lt;/strong&gt; tab&lt;/td&gt;
&lt;td&gt;Same smoke-test log appears in the correlated panel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Metrics Explorer&lt;/strong&gt; → &lt;code&gt;bot.connections_sent&lt;/code&gt;, Sum, group by &lt;code&gt;bot.run_id&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Sample for your smoke-test &lt;code&gt;run_id&lt;/code&gt; with &lt;code&gt;source = verify&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Traces Explorer&lt;/strong&gt; → &lt;code&gt;service.name = linkedin-connection-bot&lt;/code&gt;, span name &lt;code&gt;bot.verify_telemetry&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Span attributes include &lt;code&gt;bot.run_id&lt;/code&gt; matching the log and metric label&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If logs show &lt;code&gt;run_id=&lt;/code&gt; but no &lt;code&gt;trace_id&lt;/code&gt;, the log probably fired outside an active span — I make sure &lt;code&gt;tel.log_event()&lt;/code&gt; calls sit inside &lt;code&gt;with tel.span(...)&lt;/code&gt;. If &lt;strong&gt;View trace&lt;/strong&gt; is missing, I confirm &lt;code&gt;LoggingInstrumentor&lt;/code&gt; ran (SDK OTLP export path per &lt;a href="https://signoz.io/docs/logs-management/send-logs/collection-methods/" rel="noopener noreferrer"&gt;log collection methods&lt;/a&gt;) and that I'm on current &lt;code&gt;telemetry.py&lt;/code&gt; with OTLP log export enabled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Standing Up SigNoz and Verifying
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nt"&gt;-f&lt;/span&gt; pours/deployment/compose.yaml up &lt;span class="nt"&gt;-d&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://localhost:8080/api/v1/health   &lt;span class="c"&gt;# {"status":"ok"}&lt;/span&gt;
nc &lt;span class="nt"&gt;-z&lt;/span&gt; localhost 4317 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"OTLP gRPC reachable"&lt;/span&gt;

./venv/bin/python Scripts/verify_telemetry.py
&lt;span class="c"&gt;# run_id=87626c6dc93a&lt;/span&gt;

&lt;span class="c"&gt;# Optional: realistic demo runs for dashboard screenshots (no LinkedIn traffic)&lt;/span&gt;
./venv/bin/python Scripts/signoz_demo_data.py &lt;span class="nt"&gt;--runs&lt;/span&gt; 12 &lt;span class="nt"&gt;--delay&lt;/span&gt; 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A ClickHouse verification script confirmed logs, traces, and metric samples after smoke tests. For blog screenshots I also run &lt;code&gt;signoz_demo_data.py&lt;/code&gt;, which emits a dozen simulated runs with varied &lt;code&gt;run_id&lt;/code&gt;s, &lt;code&gt;source=search|grow|verify&lt;/code&gt; on &lt;code&gt;bot.connections_sent&lt;/code&gt;, histogram samples on &lt;code&gt;bot.run_duration_seconds&lt;/code&gt;, and a mix of &lt;code&gt;bot.run_success&lt;/code&gt; / &lt;code&gt;bot.run_failure&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; SigNoz Metrics Explorer defaults counters like &lt;code&gt;bot.run_success&lt;/code&gt; to &lt;strong&gt;Rate&lt;/strong&gt; over &lt;strong&gt;Last 30 minutes&lt;/strong&gt;. Sparse cron jobs look empty even when data exists. Switch to &lt;strong&gt;Sum&lt;/strong&gt; over &lt;strong&gt;Last 24 hours&lt;/strong&gt; (or run demo data recently) to see cumulative run counts. Spans like &lt;code&gt;bot.login&lt;/code&gt; and &lt;code&gt;bot.connect_from_grow&lt;/code&gt; still need a full &lt;code&gt;run_bot.py&lt;/code&gt; run — I skip that during verification to avoid live LinkedIn side effects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dashboards and Alerts (Designed, Not Yet Fired)
&lt;/h2&gt;

&lt;p&gt;From my dashboard notes, the panels I build first: run health (&lt;code&gt;bot.run_success&lt;/code&gt; vs &lt;code&gt;bot.run_failure&lt;/code&gt;), throughput by source, driver errors plus session crashes, run duration p95, and recent &lt;code&gt;bot.run&lt;/code&gt; traces.&lt;/p&gt;

&lt;p&gt;Alert targets for my setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;bot.run_success&lt;/code&gt; sum over 26h = 0&lt;/strong&gt; — no successful daily run (26h allows &lt;code&gt;launchd&lt;/code&gt; drift)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;bot.driver_errors&lt;/code&gt; where &lt;code&gt;version_mismatch&lt;/code&gt;, sum over 1h &amp;gt; 0&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;bot.session_crashes&lt;/code&gt; sum over 15m &amp;gt; 0&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No &lt;code&gt;bot.*&lt;/code&gt; metric data for 26h&lt;/strong&gt; — telemetry pipeline dead&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I haven't fired these in production yet. They're the guardrails I wish I'd had before July 17.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Do Differently Now
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Instrument before the first scheduled run.&lt;/strong&gt; A &lt;code&gt;verify_telemetry.py&lt;/code&gt; smoke test on day one beats log archaeology.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Partial runs need explicit failure signals.&lt;/strong&gt; Stopping at 27/30 should increment &lt;code&gt;bot.run_failure&lt;/code&gt; with &lt;code&gt;connections_sent=27&lt;/code&gt;, not look like quiet success.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Put &lt;code&gt;OTEL_*&lt;/code&gt; in &lt;code&gt;.env&lt;/code&gt;, not just your shell.&lt;/strong&gt; &lt;code&gt;run_bot.py&lt;/code&gt; loads &lt;code&gt;.env&lt;/code&gt; with override. My interactive shell had stale exports that confused early tests.&lt;/p&gt;

&lt;p&gt;Headless Selenium observability is about phase spans and batch counters, not HTTP latency. But the pattern — one &lt;code&gt;run_id&lt;/code&gt; on every signal — transfers to any cron-driven browser job.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bot Isn't Mysterious — It's Unobserved
&lt;/h2&gt;

&lt;p&gt;ChromeDriver mismatches, stale locks, and a SIGHUP at 28/30 all leave evidence in flat files. Nothing aggregates it. OpenTelemetry plus SigNoz gives me one place to ask: did today's run succeed? How many connections? Which phase failed? Was it the driver again?&lt;/p&gt;

&lt;p&gt;This works for my setup: local SigNoz via Docker, OTLP gRPC on 4317, macOS &lt;code&gt;launchd&lt;/code&gt;, headless Brave. Your mileage will vary.&lt;/p&gt;

&lt;p&gt;The full project — bot, telemetry module, SigNoz compose stack, verification scripts, and dashboard notes — is on GitHub:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/Sumit884-byte/linkedin_connection_bot" rel="noopener noreferrer"&gt;https://github.com/Sumit884-byte/linkedin_connection_bot&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you're running a headless browser job on a schedule and only checking log files afterward, start with a smoke test and three signals. You'll thank yourself the first time ChromeDriver and Brave disagree.&lt;/p&gt;




&lt;h2&gt;
  
  
  Screenshots
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmhk8nlz8zmjirebm0i2k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmhk8nlz8zmjirebm0i2k.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
ui-logs-linkedin-bot.png &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk2p07ar3pvb7peb8oxih.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk2p07ar3pvb7peb8oxih.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
ui-traces-bot-run.png&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg7hcepe0qndxg41bv4b6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg7hcepe0qndxg41bv4b6.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
ui-metrics-connections-sent.png&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwx4krrcjl55tp4tkbyl4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwx4krrcjl55tp4tkbyl4.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
ui-metrics-run-success.png&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3blq6tfjcqbduv62tenj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3blq6tfjcqbduv62tenj.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
ui-services-overview.png&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Use in section&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ui-logs-linkedin-bot.png&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Instrumentation / SigNoz verification — Logs Explorer with &lt;code&gt;run_id&lt;/code&gt;, &lt;code&gt;phase&lt;/code&gt;, trace correlation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ui-traces-bot-run.png&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SigNoz verification — &lt;code&gt;bot.verify_telemetry&lt;/code&gt; spans&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ui-metrics-connections-sent.png&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SigNoz verification — &lt;code&gt;bot.connections_sent&lt;/code&gt; by source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ui-metrics-run-success.png&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SigNoz verification — &lt;code&gt;bot.run_success&lt;/code&gt; over 24h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ui-services-overview.png&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SigNoz verification — Services page with &lt;code&gt;linkedin-connection-bot&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




</description>
      <category>signoz</category>
      <category>opentelemetry</category>
      <category>opensource</category>
      <category>python</category>
    </item>
    <item>
      <title>My Headless LinkedIn Bot Fails Silently - Until I Point SigNoz at It</title>
      <dc:creator>Sumit Mishra</dc:creator>
      <pubDate>Sat, 18 Jul 2026 07:47:09 +0000</pubDate>
      <link>https://dev.to/sumit_mishra_cb22d5be5f02/mcp-bbk</link>
      <guid>https://dev.to/sumit_mishra_cb22d5be5f02/mcp-bbk</guid>
      <description>&lt;p&gt;&lt;strong&gt;Subtitle:&lt;/strong&gt; From ChromeDriver 151 vs Brave 150 to correlated traces: what I learn instrumenting a Selenium cron job with OpenTelemetry&lt;/p&gt;




&lt;p&gt;Every morning I open yesterday's log file and scroll, guessing.&lt;/p&gt;

&lt;p&gt;Some days the bot sends zero connection requests. Other days it hits 27 of 30 and stops. The script runs headless on a schedule — no browser window, no Slack alert, just a text file that might not tell the whole story.&lt;/p&gt;

&lt;p&gt;I built this for a SigNoz hackathon. The goal isn't a polished observability tutorial. It's to stop flying blind on a real Python + Selenium bot running on my Mac. Here's what actually breaks, what I instrument, and what shows up in SigNoz when I finally look.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Run
&lt;/h2&gt;

&lt;p&gt;The bot sends LinkedIn connection requests up to a daily cap. I run headless Brave on macOS via &lt;code&gt;launchd&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./schedule/install_daily_schedule.sh &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;span class="c"&gt;# At login + hourly while the Mac is on; at most one successful run per day&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Relevant settings from &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;BROWSER_HEADLESS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true
&lt;/span&gt;&lt;span class="nv"&gt;USE_BROWSER_PROFILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true
&lt;/span&gt;&lt;span class="nv"&gt;USE_NETWORK_GROW&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;          &lt;span class="c"&gt;# fallback when people search is exhausted&lt;/span&gt;
&lt;span class="nv"&gt;AUTO_ACCEPT_INVITATIONS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;   &lt;span class="c"&gt;# accept pending invites on Grow first&lt;/span&gt;
&lt;span class="nv"&gt;LINKEDIN_CONNECTIONS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;30
&lt;span class="nv"&gt;LINKEDIN_DRIVER_AUTO_FIX_ATTEMPTS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2

&lt;span class="nv"&gt;SIGNOZ_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true
&lt;/span&gt;&lt;span class="nv"&gt;OTEL_EXPORTER_OTLP_ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:4317
&lt;span class="nv"&gt;OTEL_SERVICE_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;linkedin-connection-bot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When people search runs out, the bot switches to &lt;strong&gt;My Network → Grow&lt;/strong&gt;, accepts incoming invitations, then sends Connect requests. Resume state lets a crashed run pick up on Grow without redoing search. A wrapper script (&lt;code&gt;schedule/daily_run.sh&lt;/code&gt;) handles lock files, stale process cleanup, and one chromedriver auto-fix retry.&lt;/p&gt;

&lt;p&gt;Simple on paper. Messy in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Failures I Can't See From One Log Line
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ChromeDriver 151 vs Brave 150
&lt;/h3&gt;

&lt;p&gt;On July 17, most hourly &lt;code&gt;launchd&lt;/code&gt; checks looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Critical error: Failed to initialize brave: ...
This version of ChromeDriver only supports Chrome version 151
Current browser version is 150.0.7871.125
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Brave hadn't auto-updated. Selenium had cached ChromeDriver 151. The bot never got past browser launch — zero connections, 784 lines of stack trace.&lt;/p&gt;

&lt;p&gt;I add self-healing in &lt;code&gt;Scripts/browser_utils.py&lt;/code&gt;: detect the mismatch, kill stale chromedriver and Brave processes, re-download the matching driver:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;auto_fix_chromedriver_mismatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;browser_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;log_func&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;cleanup_stale_bot_processes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;browser_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;log_func&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;log_func&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resolve_chromedriver_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;browser_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;force_refresh&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;log_func&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Auto-fix: resolved chromedriver at &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That fixed launches once Brave caught up. But I still want to &lt;em&gt;know&lt;/em&gt; when it happens without grepping logs at 7 a.m.&lt;/p&gt;

&lt;h3&gt;
  
  
  Almost Done: 27/30 and a Hangup
&lt;/h3&gt;

&lt;p&gt;Late on July 17, after auto-fix worked, the bot resumed Grow-only mode and sent connections steadily — until:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Progress: 27/30 connections sent &lt;span class="o"&gt;(&lt;/span&gt;grow&lt;span class="o"&gt;)&lt;/span&gt;
Progress: 28/30 connections sent &lt;span class="o"&gt;(&lt;/span&gt;grow&lt;span class="o"&gt;)&lt;/span&gt;
Cleaning up bot browser processes...
./schedule/daily_run.sh: line 19: 52556 Hangup: 1               &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VENV_PYTHON&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; run_bot.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two short of the cap. Killed by SIGHUP. Without exported counters, the only signal is a progress line buried in a 700-line file.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stale Locks
&lt;/h3&gt;

&lt;p&gt;While one process is still running, the next hourly check logs: &lt;code&gt;Another bot run is already in progress (pid 51947). Skipping.&lt;/code&gt; Earlier that evening I saw: &lt;code&gt;Removing stale lock file.&lt;/code&gt; The lock logic is correct. Interrupted runs still leave footguns. Metrics like &lt;code&gt;bot.driver_errors&lt;/code&gt; and &lt;code&gt;bot.session_crashes&lt;/code&gt; surface this on a dashboard instead of me tailing files.&lt;/p&gt;

&lt;h2&gt;
  
  
  One Module, Three Signals
&lt;/h2&gt;

&lt;p&gt;Everything lives in &lt;code&gt;Scripts/telemetry.py&lt;/code&gt; — OTLP gRPC to port 4317, gated by &lt;code&gt;SIGNOZ_ENABLED&lt;/code&gt;. Each run gets a 12-character &lt;code&gt;bot.run_id&lt;/code&gt; on traces, metrics, and logs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spans:&lt;/strong&gt; &lt;code&gt;bot.run&lt;/code&gt;, &lt;code&gt;bot.configure_browser&lt;/code&gt;, &lt;code&gt;bot.login&lt;/code&gt;, &lt;code&gt;bot.search_page&lt;/code&gt;, &lt;code&gt;bot.accept_invitations&lt;/code&gt;, &lt;code&gt;bot.connect_from_grow&lt;/code&gt;, &lt;code&gt;bot.recover_session&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Metrics:&lt;/strong&gt; &lt;code&gt;bot.connections_sent&lt;/code&gt; (label &lt;code&gt;source=search|grow&lt;/code&gt;), &lt;code&gt;bot.invitations_accepted&lt;/code&gt;, &lt;code&gt;bot.driver_errors&lt;/code&gt; (&lt;code&gt;error_type=version_mismatch&lt;/code&gt;), &lt;code&gt;bot.session_crashes&lt;/code&gt;, &lt;code&gt;bot.run_success&lt;/code&gt; / &lt;code&gt;bot.run_failure&lt;/code&gt;, &lt;code&gt;bot.run_duration_seconds&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Main loop wiring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;tel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init_telemetry&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;tel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bot run started&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;phase&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bot.run&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;phase&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;configurations&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;webpage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;tel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record_run_outcome&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;run_failed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;connections_sent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;connections_sent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;tel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record_run_duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;run_started_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On driver mismatch: &lt;code&gt;tel.record_driver_error(error_type="version_mismatch")&lt;/code&gt; before auto-fix runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Logs&lt;/strong&gt; append &lt;code&gt;run_id&lt;/code&gt; and &lt;code&gt;phase&lt;/code&gt; for trace correlation in SigNoz Logs Explorer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Verify telemetry smoke test | run_id=87626c6dc93a phase=verify
trace_id=7b69827901b867b9f0658ed774ff684d  span_id=0e08651f63956788
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One lesson I keep relearning: logs need explicit OTLP export. &lt;code&gt;telemetry.py&lt;/code&gt; wires a &lt;code&gt;LoggingHandler&lt;/code&gt; plus &lt;code&gt;OTLPLogExporter&lt;/code&gt; — stdout files alone aren't enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three-Pillar Correlation — The Part I Care About Most
&lt;/h2&gt;

&lt;p&gt;SigNoz's real payoff for me isn't three separate tabs. It's jumping between logs, traces, and metrics without rebuilding queries. &lt;a href="https://signoz.io/docs/what-is-signoz/" rel="noopener noreferrer"&gt;Unified observability&lt;/a&gt; means one &lt;code&gt;run_id&lt;/code&gt; on every signal, and SigNoz UI links that carry filters and time range when I drill down (&lt;a href="https://signoz.io/docs/dashboards/interactivity/" rel="noopener noreferrer"&gt;dashboard interactivity&lt;/a&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  How my bot wires it
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pillar&lt;/th&gt;
&lt;th&gt;What I emit&lt;/th&gt;
&lt;th&gt;Correlation hook&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Traces&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Spans like &lt;code&gt;bot.run&lt;/code&gt;, &lt;code&gt;bot.login&lt;/code&gt;, &lt;code&gt;bot.connect_from_grow&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;bot.run_id&lt;/code&gt; span attribute; root span wraps the whole cron run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Metrics&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;bot.connections_sent&lt;/code&gt;, &lt;code&gt;bot.run_success&lt;/code&gt;, etc.&lt;/td&gt;
&lt;td&gt;Same &lt;code&gt;bot.run_id&lt;/code&gt; label on every counter/histogram sample&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Logs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;log_event()&lt;/code&gt; lines with &lt;code&gt;run_id=&lt;/code&gt; and &lt;code&gt;phase=&lt;/code&gt; in the body&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;LoggingInstrumentor&lt;/code&gt; injects &lt;code&gt;trace_id&lt;/code&gt; / &lt;code&gt;span_id&lt;/code&gt; when the log fires inside an active span (&lt;a href="https://signoz.io/docs/traces-management/guides/correlate-traces-and-logs/" rel="noopener noreferrer"&gt;official guide&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Logs emitted &lt;strong&gt;inside&lt;/strong&gt; a span carry both domain context (&lt;code&gt;run_id=87626c6dc93a phase=verify&lt;/code&gt;) and OTel trace context (&lt;code&gt;trace_id=… span_id=…&lt;/code&gt;). That second pair is what powers SigNoz's &lt;strong&gt;View trace&lt;/strong&gt; button on a log line. Metrics don't get trace IDs on every sample — I correlate them through the shared &lt;code&gt;bot.run_id&lt;/code&gt; label instead. SigNoz also derives APM/RED metrics from span data automatically (&lt;a href="https://signoz.io/docs/traces-management/guides/apm-metrics/" rel="noopener noreferrer"&gt;APM metrics from traces&lt;/a&gt;), which gives me operation-level rates alongside my custom &lt;code&gt;bot.*&lt;/code&gt; counters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Walkthrough: one smoke test, three pillars
&lt;/h3&gt;

&lt;p&gt;After &lt;code&gt;./venv/bin/python Scripts/verify_telemetry.py&lt;/code&gt; prints &lt;code&gt;run_id=87626c6dc93a&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Logs → trace.&lt;/strong&gt; I open &lt;strong&gt;Logs Explorer&lt;/strong&gt;, filter &lt;code&gt;service.name = linkedin-connection-bot&lt;/code&gt;, and find &lt;code&gt;Verify telemetry smoke test | run_id=87626c6dc93a phase=verify&lt;/code&gt;. The row shows &lt;code&gt;trace_id&lt;/code&gt; and &lt;code&gt;span_id&lt;/code&gt;. I click &lt;strong&gt;View trace&lt;/strong&gt; (or the trace ID link). SigNoz opens the &lt;code&gt;bot.verify_telemetry&lt;/code&gt; span — the same run I just emitted.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Trace → logs.&lt;/strong&gt; On that trace's span detail panel I open the &lt;strong&gt;Logs&lt;/strong&gt; tab (&lt;a href="https://signoz.io/docs/userguide/span-details/" rel="noopener noreferrer"&gt;span details&lt;/a&gt;). SigNoz shows the correlated log line from step 1 without me re-filtering by hand. I can also click &lt;strong&gt;Go to related logs&lt;/strong&gt; from the trace list view.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Trace → metrics.&lt;/strong&gt; Back in &lt;strong&gt;Metrics Explorer&lt;/strong&gt; I query &lt;code&gt;bot.connections_sent&lt;/code&gt; with aggregation &lt;strong&gt;Sum&lt;/strong&gt;, group by &lt;code&gt;bot.run_id&lt;/code&gt;, and filter &lt;code&gt;bot.run_id = 87626c6dc93a&lt;/code&gt;. I see &lt;code&gt;source = verify&lt;/code&gt; with value 1 — the counter incremented inside the same span that produced the log. For a demo run with twelve simulated runs (&lt;code&gt;signoz_demo_data.py&lt;/code&gt;), I group by &lt;code&gt;source&lt;/code&gt; and spot the grow spike that matches a &lt;code&gt;bot.connect_from_grow&lt;/code&gt; span in Traces Explorer.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's the July 17 hangup story in reverse: instead of grepping 700 lines for &lt;code&gt;Progress: 27/30&lt;/code&gt;, I click a log from the failed run, jump to &lt;code&gt;bot.run&lt;/code&gt;, and check whether &lt;code&gt;bot.connections_sent&lt;/code&gt; stopped climbing for that &lt;code&gt;run_id&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify correlation in the SigNoz UI (5 minutes)
&lt;/h3&gt;

&lt;p&gt;Run the smoke test, wait ~10 seconds for export, then check each pillar:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;th&gt;What to confirm&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Logs Explorer&lt;/strong&gt; → filter &lt;code&gt;service.name = linkedin-connection-bot&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Body contains &lt;code&gt;run_id=&lt;/code&gt;; log attributes or body include &lt;code&gt;trace_id&lt;/code&gt; and &lt;code&gt;span_id&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Click a log row → &lt;strong&gt;View trace&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Trace opens with span &lt;code&gt;bot.verify_telemetry&lt;/code&gt; and &lt;code&gt;bot.run_id&lt;/code&gt; attribute&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Trace detail → &lt;strong&gt;Logs&lt;/strong&gt; tab&lt;/td&gt;
&lt;td&gt;Same smoke-test log appears in the correlated panel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Metrics Explorer&lt;/strong&gt; → &lt;code&gt;bot.connections_sent&lt;/code&gt;, Sum, group by &lt;code&gt;bot.run_id&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Sample for your smoke-test &lt;code&gt;run_id&lt;/code&gt; with &lt;code&gt;source = verify&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Traces Explorer&lt;/strong&gt; → &lt;code&gt;service.name = linkedin-connection-bot&lt;/code&gt;, span name &lt;code&gt;bot.verify_telemetry&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Span attributes include &lt;code&gt;bot.run_id&lt;/code&gt; matching the log and metric label&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If logs show &lt;code&gt;run_id=&lt;/code&gt; but no &lt;code&gt;trace_id&lt;/code&gt;, the log probably fired outside an active span — I make sure &lt;code&gt;tel.log_event()&lt;/code&gt; calls sit inside &lt;code&gt;with tel.span(...)&lt;/code&gt;. If &lt;strong&gt;View trace&lt;/strong&gt; is missing, I confirm &lt;code&gt;LoggingInstrumentor&lt;/code&gt; ran (SDK OTLP export path per &lt;a href="https://signoz.io/docs/logs-management/send-logs/collection-methods/" rel="noopener noreferrer"&gt;log collection methods&lt;/a&gt;) and that I'm on current &lt;code&gt;telemetry.py&lt;/code&gt; with OTLP log export enabled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Standing Up SigNoz and Verifying
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nt"&gt;-f&lt;/span&gt; pours/deployment/compose.yaml up &lt;span class="nt"&gt;-d&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://localhost:8080/api/v1/health   &lt;span class="c"&gt;# {"status":"ok"}&lt;/span&gt;
nc &lt;span class="nt"&gt;-z&lt;/span&gt; localhost 4317 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"OTLP gRPC reachable"&lt;/span&gt;

./venv/bin/python Scripts/verify_telemetry.py
&lt;span class="c"&gt;# run_id=87626c6dc93a&lt;/span&gt;

&lt;span class="c"&gt;# Optional: realistic demo runs for dashboard screenshots (no LinkedIn traffic)&lt;/span&gt;
./venv/bin/python Scripts/signoz_demo_data.py &lt;span class="nt"&gt;--runs&lt;/span&gt; 12 &lt;span class="nt"&gt;--delay&lt;/span&gt; 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A ClickHouse verification script confirmed logs, traces, and metric samples after smoke tests. For blog screenshots I also run &lt;code&gt;signoz_demo_data.py&lt;/code&gt;, which emits a dozen simulated runs with varied &lt;code&gt;run_id&lt;/code&gt;s, &lt;code&gt;source=search|grow|verify&lt;/code&gt; on &lt;code&gt;bot.connections_sent&lt;/code&gt;, histogram samples on &lt;code&gt;bot.run_duration_seconds&lt;/code&gt;, and a mix of &lt;code&gt;bot.run_success&lt;/code&gt; / &lt;code&gt;bot.run_failure&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; SigNoz Metrics Explorer defaults counters like &lt;code&gt;bot.run_success&lt;/code&gt; to &lt;strong&gt;Rate&lt;/strong&gt; over &lt;strong&gt;Last 30 minutes&lt;/strong&gt;. Sparse cron jobs look empty even when data exists. Switch to &lt;strong&gt;Sum&lt;/strong&gt; over &lt;strong&gt;Last 24 hours&lt;/strong&gt; (or run demo data recently) to see cumulative run counts. Spans like &lt;code&gt;bot.login&lt;/code&gt; and &lt;code&gt;bot.connect_from_grow&lt;/code&gt; still need a full &lt;code&gt;run_bot.py&lt;/code&gt; run — I skip that during verification to avoid live LinkedIn side effects.&lt;/p&gt;

&lt;p&gt;[IMAGE 1: Upload ui-logs-linkedin-bot.png — caption: Logs Explorer showing run_id and trace correlation for linkedin-connection-bot]&lt;/p&gt;

&lt;p&gt;[IMAGE 2: Upload ui-traces-bot-run.png — caption: Trace list with bot.verify_telemetry spans and service filter]&lt;/p&gt;

&lt;p&gt;[IMAGE 3: Upload ui-metrics-connections-sent.png — caption: bot.connections_sent by source (Sum over 24h)]&lt;/p&gt;

&lt;p&gt;[IMAGE 4: Upload ui-metrics-run-success.png — caption: bot.run_success over 24h]&lt;/p&gt;

&lt;p&gt;[IMAGE 5: Upload ui-services-overview.png — caption: Services overview with linkedin-connection-bot]&lt;/p&gt;

&lt;p&gt;These are live SigNoz UI captures from &lt;code&gt;http://localhost:8080&lt;/code&gt;. I keep a ClickHouse-backed HTML audit trail in the same folder as a secondary check — useful for debugging, not required for the story.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dashboards and Alerts (Designed, Not Yet Fired)
&lt;/h2&gt;

&lt;p&gt;From my dashboard notes, the panels I build first: run health (&lt;code&gt;bot.run_success&lt;/code&gt; vs &lt;code&gt;bot.run_failure&lt;/code&gt;), throughput by source, driver errors plus session crashes, run duration p95, and recent &lt;code&gt;bot.run&lt;/code&gt; traces.&lt;/p&gt;

&lt;p&gt;Alert targets for my setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;bot.run_success&lt;/code&gt; sum over 26h = 0&lt;/strong&gt; — no successful daily run (26h allows &lt;code&gt;launchd&lt;/code&gt; drift)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;bot.driver_errors&lt;/code&gt; where &lt;code&gt;version_mismatch&lt;/code&gt;, sum over 1h &amp;gt; 0&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;bot.session_crashes&lt;/code&gt; sum over 15m &amp;gt; 0&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No &lt;code&gt;bot.*&lt;/code&gt; metric data for 26h&lt;/strong&gt; — telemetry pipeline dead&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I haven't fired these in production yet. They're the guardrails I wish I'd had before July 17.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Do Differently Now
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Instrument before the first scheduled run.&lt;/strong&gt; A &lt;code&gt;verify_telemetry.py&lt;/code&gt; smoke test on day one beats log archaeology.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Partial runs need explicit failure signals.&lt;/strong&gt; Stopping at 27/30 should increment &lt;code&gt;bot.run_failure&lt;/code&gt; with &lt;code&gt;connections_sent=27&lt;/code&gt;, not look like quiet success.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Put &lt;code&gt;OTEL_*&lt;/code&gt; in &lt;code&gt;.env&lt;/code&gt;, not just your shell.&lt;/strong&gt; &lt;code&gt;run_bot.py&lt;/code&gt; loads &lt;code&gt;.env&lt;/code&gt; with override. My interactive shell had stale exports that confused early tests.&lt;/p&gt;

&lt;p&gt;Headless Selenium observability is about phase spans and batch counters, not HTTP latency. But the pattern — one &lt;code&gt;run_id&lt;/code&gt; on every signal — transfers to any cron-driven browser job.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bot Isn't Mysterious — It's Unobserved
&lt;/h2&gt;

&lt;p&gt;ChromeDriver mismatches, stale locks, and a SIGHUP at 28/30 all leave evidence in flat files. Nothing aggregates it. OpenTelemetry plus SigNoz gives me one place to ask: did today's run succeed? How many connections? Which phase failed? Was it the driver again?&lt;/p&gt;

&lt;p&gt;This works for my setup: local SigNoz via Docker, OTLP gRPC on 4317, macOS &lt;code&gt;launchd&lt;/code&gt;, headless Brave. Your mileage will vary.&lt;/p&gt;

&lt;p&gt;The full project — bot, telemetry module, SigNoz compose stack, verification scripts, and dashboard notes — is on GitHub:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/Sumit884-byte/linkedin_connection_bot" rel="noopener noreferrer"&gt;https://github.com/Sumit884-byte/linkedin_connection_bot&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you're running a headless browser job on a schedule and only checking log files afterward, start with a smoke test and three signals. You'll thank yourself the first time ChromeDriver and Brave disagree.&lt;/p&gt;




&lt;h2&gt;
  
  
  Screenshots
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmhk8nlz8zmjirebm0i2k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmhk8nlz8zmjirebm0i2k.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
ui-logs-linkedin-bot.png &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk2p07ar3pvb7peb8oxih.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk2p07ar3pvb7peb8oxih.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
ui-traces-bot-run.png&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg7hcepe0qndxg41bv4b6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg7hcepe0qndxg41bv4b6.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
ui-metrics-connections-sent.png&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwx4krrcjl55tp4tkbyl4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwx4krrcjl55tp4tkbyl4.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
ui-metrics-run-success.png&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3blq6tfjcqbduv62tenj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3blq6tfjcqbduv62tenj.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
ui-services-overview.png&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Use in section&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ui-logs-linkedin-bot.png&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Instrumentation / SigNoz verification — Logs Explorer with &lt;code&gt;run_id&lt;/code&gt;, &lt;code&gt;phase&lt;/code&gt;, trace correlation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ui-traces-bot-run.png&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SigNoz verification — &lt;code&gt;bot.verify_telemetry&lt;/code&gt; spans&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ui-metrics-connections-sent.png&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SigNoz verification — &lt;code&gt;bot.connections_sent&lt;/code&gt; by source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ui-metrics-run-success.png&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SigNoz verification — &lt;code&gt;bot.run_success&lt;/code&gt; over 24h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ui-services-overview.png&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SigNoz verification — Services page with &lt;code&gt;linkedin-connection-bot&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




</description>
      <category>opentelemetry</category>
      <category>signoz</category>
      <category>selenium</category>
      <category>observability</category>
    </item>
  </channel>
</rss>
