<?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: Hirofumi Tsuda</title>
    <description>The latest articles on DEV Community by Hirofumi Tsuda (@hirofumi_tsuda).</description>
    <link>https://dev.to/hirofumi_tsuda</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%2F4056812%2Fba8b4e5c-f4d6-42e3-a3fd-c5dcf5ac8e1d.png</url>
      <title>DEV Community: Hirofumi Tsuda</title>
      <link>https://dev.to/hirofumi_tsuda</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hirofumi_tsuda"/>
    <language>en</language>
    <item>
      <title>A Prometheus exporter for Dagster that polls GraphQL instead of pushing to Pushgateway</title>
      <dc:creator>Hirofumi Tsuda</dc:creator>
      <pubDate>Wed, 12 Aug 2026 12:39:22 +0000</pubDate>
      <link>https://dev.to/hirofumi_tsuda/a-prometheus-exporter-for-dagster-that-polls-graphql-instead-of-pushing-to-pushgateway-39pg</link>
      <guid>https://dev.to/hirofumi_tsuda/a-prometheus-exporter-for-dagster-that-polls-graphql-instead-of-pushing-to-pushgateway-39pg</guid>
      <description>&lt;p&gt;Dagster doesn't expose a &lt;code&gt;/metrics&lt;/code&gt; endpoint out of the box. The officially documented way to get Dagster metrics into Prometheus is the &lt;a href="https://docs.dagster.io/integrations/libraries/prometheus" rel="noopener noreferrer"&gt;&lt;code&gt;dagster-prometheus&lt;/code&gt;&lt;/a&gt; resource, which pushes metrics to a &lt;a href="https://github.com/prometheus/pushgateway" rel="noopener noreferrer"&gt;Pushgateway&lt;/a&gt; from inside a run.&lt;/p&gt;

&lt;p&gt;That works, but it has a structural blind spot: the push only happens if code inside the run gets to call it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A run that's OOM-killed, or crashes before reaching the push call, never reports anything. The failure is silent from Prometheus's point of view.&lt;/li&gt;
&lt;li&gt;A run sitting in &lt;code&gt;QUEUED&lt;/code&gt; because of a run-queue concurrency limit hasn't started user code yet, so it can't push either — you can't see queue backlog forming.&lt;/li&gt;
&lt;li&gt;Prometheus's own docs are explicit that Pushgateway &lt;a href="https://prometheus.io/docs/practices/pushing/" rel="noopener noreferrer"&gt;isn't meant to be a general substitute for pull-based scraping&lt;/a&gt;, only for short-lived batch jobs that genuinely can't be scraped.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I built &lt;a href="https://github.com/HirofumiTsuda/dagster-prometheus-exporter" rel="noopener noreferrer"&gt;&lt;strong&gt;dagster-prometheus-exporter&lt;/strong&gt;&lt;/a&gt;: a small standalone Go binary that polls Dagster's GraphQL API on an interval and derives metrics from whatever state Dagster itself already has — including runs that never got a chance to push anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Dagster (GraphQL) &amp;lt;--poll-- exporter --scrape--&amp;gt; Prometheus --&amp;gt; Grafana
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exporter keeps in-memory state, not an external store. Scraping (writing state) and serving &lt;code&gt;/metrics&lt;/code&gt; (reading state) are decoupled, so if a GraphQL call is slow or fails, &lt;code&gt;/metrics&lt;/code&gt; still serves the last-known state instead of breaking. Completed runs are fetched incrementally (watermark-based, not a full rescan every time), so the cost doesn't grow with total run history.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it currently covers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;What it answers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dagster_active_runs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;How many runs are queued/starting/started, per job&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dagster_active_run_duration_seconds&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;How long the oldest active run in a job has been stuck there — useful for spotting stalls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;dagster_completed_runs_total&lt;/code&gt; / &lt;code&gt;dagster_last_run_info&lt;/code&gt; / &lt;code&gt;dagster_last_run_duration_seconds&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Success/failure counts and timing for completed runs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dagster_run_queue_concurrency_key_backlog&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Queue backlog per &lt;code&gt;dagster/concurrency_key&lt;/code&gt; tag — this is the one that needed the most digging, since &lt;code&gt;instance.concurrencyLimits&lt;/code&gt; in the GraphQL schema &lt;em&gt;looks&lt;/em&gt; like the answer but is actually a separate op-pool concurrency mechanism and reports 0 regardless of run-queue backlog. Ended up reading each queued run's own tags instead.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dagster_code_location_load_error&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Whether a code location is currently failing to load (e.g. broken import) — independent of job-level metrics, since a broken location can't be inferred from run counts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;dagster_schedule_status&lt;/code&gt; / &lt;code&gt;dagster_schedule_last_tick_status&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Whether a schedule is running, and its last tick outcome&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;dagster_sensor_status&lt;/code&gt; / &lt;code&gt;dagster_sensor_last_tick_status&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Same, for sensors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dagster_exporter_build_info&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Which version/commit is actually running — handy once you have more than one exporter pod&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Full label reference is in the &lt;a href="https://github.com/HirofumiTsuda/dagster-prometheus-exporter#metrics" rel="noopener noreferrer"&gt;README&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trying it out
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/HirofumiTsuda/dagster-prometheus-exporter.git
&lt;span class="nb"&gt;cd &lt;/span&gt;dagster-prometheus-exporter
docker compose up &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;brings up Dagster + the exporter + Prometheus + a pre-provisioned Grafana dashboard together. For an existing Dagster deployment, there's a published image and a Helm chart:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 9101:9101 &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;DAGSTER_GRAPHQL_ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://dagster:3000/graphql &lt;span class="se"&gt;\&lt;/span&gt;
  ghcr.io/hirofumitsuda/dagster-prometheus-exporter:0.2.0

helm &lt;span class="nb"&gt;install &lt;/span&gt;my-dagster-exporter oci://ghcr.io/hirofumitsuda/charts/dagster-prometheus-exporter &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--version&lt;/span&gt; 0.1.3 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; env.DAGSTER_GRAPHQL_ENDPOINT&lt;span class="o"&gt;=&lt;/span&gt;http://dagster-webserver.dagster.svc.cluster.local/graphql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What's not covered yet
&lt;/h2&gt;

&lt;p&gt;Asset materialization status. I've looked into the GraphQL shape for it (&lt;code&gt;assetNodes&lt;/code&gt; + &lt;code&gt;assetsLatestInfo&lt;/code&gt;) and the same kind of gotcha as the concurrency backlog shows up: &lt;code&gt;AssetNode.assetMaterializations&lt;/code&gt; only records &lt;em&gt;successful&lt;/em&gt; materializations, so detecting a failed one means cross-referencing &lt;code&gt;assetsLatestInfo.latestRun.status&lt;/code&gt; instead. Tracked in &lt;a href="https://github.com/HirofumiTsuda/dagster-prometheus-exporter/issues/56" rel="noopener noreferrer"&gt;#56&lt;/a&gt; if anyone wants to compare notes.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/HirofumiTsuda/dagster-prometheus-exporter" rel="noopener noreferrer"&gt;https://github.com/HirofumiTsuda/dagster-prometheus-exporter&lt;/a&gt; — issues and PRs welcome.&lt;/p&gt;

</description>
      <category>dagster</category>
      <category>prometheus</category>
      <category>grafana</category>
      <category>go</category>
    </item>
  </channel>
</rss>
