<?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: Cronpimo</title>
    <description>The latest articles on DEV Community by Cronpimo (cronpimo-com).</description>
    <link>https://dev.to/cronpimo-com</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%2Forganization%2Fprofile_image%2F15454%2F6f3acc7f-9739-4282-915e-e15ecbd5de4e.png</url>
      <title>DEV Community: Cronpimo</title>
      <link>https://dev.to/cronpimo-com</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cronpimo-com"/>
    <language>en</language>
    <item>
      <title>Beyond a completion signal: three useful backup metrics</title>
      <dc:creator>Cronpimo</dc:creator>
      <pubDate>Wed, 11 Nov 2026 08:15:47 +0000</pubDate>
      <link>https://dev.to/cronpimo-com/beyond-a-completion-signal-three-useful-backup-metrics-4m1l</link>
      <guid>https://dev.to/cronpimo-com/beyond-a-completion-signal-three-useful-backup-metrics-4m1l</guid>
      <description>&lt;p&gt;Originally published on Cronpimo: &lt;a href="https://cronpimo.com/index.php?article/10-beyond-a-completion-signal-three-useful-backup-metrics/" rel="noopener noreferrer"&gt;Beyond a completion signal: three useful backup metrics&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The overnight backup finishes without an error and its completion signal arrives. Yet the new archive contains only a fraction of the expected data. Perhaps a source directory was not mounted; perhaps someone intentionally narrowed the selection. A green completion signal cannot distinguish those cases. Three additional measurements can help you decide what to investigate.&lt;/p&gt;

&lt;p&gt;The example below is a fictional daily full export into a compressed archive. Its numbers illustrate the checks. They are neither recommended defaults for arbitrary backups nor results from a customer incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define what each measurement means
&lt;/h2&gt;

&lt;p&gt;Use the same definitions for every run: &lt;code&gt;files&lt;/code&gt; counts backed-up source files in the current manifest, &lt;code&gt;bytes&lt;/code&gt; measures the completed compressed archive, and &lt;code&gt;duration_seconds&lt;/code&gt; covers the backup from its start to confirmed completion at the destination. The HTTP request that delivers the completion signal is not part of that duration.&lt;/p&gt;

&lt;p&gt;Our example job has these deliberately chosen limits:&lt;/p&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;Reported value&lt;/th&gt;
&lt;th&gt;Allowed range&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Files&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;At least 100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Archive size&lt;/td&gt;
&lt;td&gt;50 MiB&lt;/td&gt;
&lt;td&gt;40 to 100 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duration&lt;/td&gt;
&lt;td&gt;180 seconds&lt;/td&gt;
&lt;td&gt;60 to 600 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  1. Count: did the expected selection get backed up?
&lt;/h2&gt;

&lt;p&gt;A file count helps reveal a suddenly smaller selection. If this export reports 3 files instead of 120, inspect source paths, mounted volumes and exclusion rules. Successfully creating an archive does not mean the intended selection went into it.&lt;/p&gt;

&lt;p&gt;Do not simply count files in the destination directory: one archive remains one file regardless of how many source files it contains. Use the result or manifest from this particular backup run. A database backup might need a different meaningful count; its internal backup files may say little about completeness.&lt;/p&gt;

&lt;p&gt;Nor does 120 matching 120 prove identical contents. The limit detects certain differences, not every form of damage. Choose it using known, checked runs and the data structure you actually expect.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Volume: was unusually little or much written?
&lt;/h2&gt;

&lt;p&gt;A 2 MiB archive falls below the example's lower limit. That calls for investigation, but does not establish a cause. Check whether less content was backed up or whether its compressibility changed. Switching compression methods can also alter the comparison.&lt;/p&gt;

&lt;p&gt;An upper limit can reveal unexpectedly large backups: newly included logs, for example, or old archives accidentally added to the selection. Assess full and incremental backups separately. A small amount of new data can be perfectly valid for an incremental run.&lt;/p&gt;

&lt;p&gt;The example reports bytes: 50 MiB is 52,428,800 bytes, with limits of 41,943,040 and 104,857,600 bytes. Keep the unit consistent when collecting, sending and configuring the measurement.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Duration: was the process unusually short or long?
&lt;/h2&gt;

&lt;p&gt;A much shorter run may fit the missing-selection hypothesis. A longer one may reflect more data, slower storage or a stalled transfer. None of these measurements identifies a cause automatically. Compare duration with the count and volume from the same run.&lt;/p&gt;

&lt;p&gt;A lower duration limit is useful only when a faster run would genuinely be suspicious. An optimization or storage change may require revising it. Here, the upper limit of 600 seconds means a completed run reporting 601 seconds violates the metric rule.&lt;/p&gt;

&lt;p&gt;A duration reported after completion cannot detect a process that never finishes. Monitor missing or late signals as well. The &lt;a href="https://cronpimo.com/service/index.php?cronjob-monitoring/&amp;amp;l=2" rel="noopener noreferrer"&gt;cron job monitoring guide&lt;/a&gt; distinguishes starting a process, completing it and checking its result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Send measurements with the completion signal
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://cronpimo.com/service/index.php?heartbeat-monitoring/&amp;amp;l=2" rel="noopener noreferrer"&gt;heartbeat guide&lt;/a&gt; covers setup. For a completion-only signal, the illustrative payload is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"event_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"backup-example-001-end"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"metrics"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;"files"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"bytes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;52428800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"duration_seconds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace those numbers with measurements from the run that just finished, and use a unique event identifier for each signal. If you also send a start signal, both start and completion need the same &lt;code&gt;run_key&lt;/code&gt;, unique to that run.&lt;/p&gt;

&lt;p&gt;Cronpimo's minimum and maximum limits are inclusive: 100 files, 40 MiB, and either 60 or 600 seconds are inside these rules. Each configured metric must be present in a success signal. If &lt;code&gt;duration_seconds&lt;/code&gt; is missing, the heartbeat is classified as failed despite &lt;code&gt;kind: success&lt;/code&gt;. An unavailable measurement is not the same as zero seconds. Send numbers as JSON numbers; do not replace missing measurements with invented values.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn an unusual result into a specific check
&lt;/h2&gt;

&lt;p&gt;First inspect the affected run and its measurement source. Are the manifest and archive really from today? Was the selection changed? Adjust limits after explaining the difference. Cronpimo does not learn these limits automatically from earlier backups. If your workflow instead exposes a readable status endpoint, the &lt;a href="https://cronpimo.com/service/index.php?api-result-monitoring/&amp;amp;l=2" rel="noopener noreferrer"&gt;result-checking guide&lt;/a&gt; shows how to evaluate the result and its freshness separately.&lt;/p&gt;

&lt;p&gt;Continue to schedule restoration tests. Even specialized integrity checkers do not replace them; the &lt;a href="https://www.postgresql.org/docs/18/app-pgverifybackup.html" rel="noopener noreferrer"&gt;PostgreSQL documentation for pg_verifybackup&lt;/a&gt; makes that distinction explicitly. A complete set of plausible measurements is a useful operational signal. Restoring the required data is a separate test.&lt;/p&gt;




&lt;p&gt;Disclosure: This article and its cover illustration were created autonomously with AI for Cronpimo. Its technical claims were checked against the current implementation and the referenced documentation. The heartbeat examples and metric boundaries were validated with isolated tests against the current code.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>monitoring</category>
      <category>backup</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Every morning at eight: choosing schedules and time zones</title>
      <dc:creator>Cronpimo</dc:creator>
      <pubDate>Wed, 28 Oct 2026 08:16:40 +0000</pubDate>
      <link>https://dev.to/cronpimo-com/every-morning-at-eight-choosing-schedules-and-time-zones-59l8</link>
      <guid>https://dev.to/cronpimo-com/every-morning-at-eight-choosing-schedules-and-time-zones-59l8</guid>
      <description>&lt;p&gt;Your import runs every day, but since the clocks changed, its report no longer arrives at the expected local time. The schedule might be doing exactly what it was configured to do: “every 24 hours” instead of “every morning at eight.” Those instructions coincide on many days. A change in the local UTC offset exposes the difference.&lt;/p&gt;

&lt;p&gt;A dependable schedule starts with three decisions: Is the requirement a wall-clock time or an elapsed interval? Which time zone belongs to the task? What should happen when a local time does not exist or occurs twice?&lt;/p&gt;

&lt;h2&gt;
  
  
  Wall-clock time or elapsed interval?
&lt;/h2&gt;

&lt;p&gt;A daily report for a team in Vienna belongs to a local time. In a five-field cron expression, &lt;code&gt;0 8 * * *&lt;/code&gt; means minute zero, hour eight, every day. With the time zone &lt;code&gt;Europe/Vienna&lt;/code&gt;, the scheduled occurrence stays at 08:00 local time.&lt;/p&gt;

&lt;p&gt;A recurring technical check often has a different requirement: scheduled occurrences should be 300 seconds apart, for example. An interval fits that requirement. Cronpimo calculates interval occurrences from a fixed starting anchor. It does not add the previous request’s runtime as an extra delay between occurrences.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;86,400 seconds is exactly 24 hours, but not always one local calendar day.&lt;/strong&gt; When Vienna moves to daylight saving time, the affected day has 23 hours; when it moves back, the day has 25. A fixed interval in seconds does not follow that change.&lt;/p&gt;

&lt;h2&gt;
  
  
  A concrete example from October 2026
&lt;/h2&gt;

&lt;p&gt;The following comparison shows calculated occurrences in &lt;code&gt;Europe/Vienna&lt;/code&gt;. The interval starts on 24 October at 08:00 and repeats every 86,400 seconds. The calendar schedule uses &lt;code&gt;0 8 * * *&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Calendar schedule&lt;/th&gt;
&lt;th&gt;24-hour interval&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;24 October&lt;/td&gt;
&lt;td&gt;08:00 (UTC+02:00)&lt;/td&gt;
&lt;td&gt;08:00 (UTC+02:00)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25 October&lt;/td&gt;
&lt;td&gt;08:00 (UTC+01:00)&lt;/td&gt;
&lt;td&gt;07:00 (UTC+01:00)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;26 October&lt;/td&gt;
&lt;td&gt;08:00 (UTC+01:00)&lt;/td&gt;
&lt;td&gt;07:00 (UTC+01:00)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The calendar schedule has one 25-hour gap across this transition. The interval preserves its 24-hour gap and lands earlier on the local clock. A morning report usually calls for the calendar schedule; evenly spaced technical checks call for the interval. These are scheduled occurrences, not a promise that execution starts without any delay or has completed by that time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose a named time zone
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Europe/Vienna&lt;/code&gt; represents local time rules. &lt;code&gt;UTC+01:00&lt;/code&gt; represents only a fixed offset and contains no daylight saving rule. The &lt;a href="https://www.iana.org/time-zones" rel="noopener noreferrer"&gt;IANA time zone database&lt;/a&gt; is updated when those rules change. Choose the zone of the business operation: Vienna for an Austrian end-of-day process, for example, or UTC for an exchange explicitly defined in UTC.&lt;/p&gt;

&lt;p&gt;The executing agent’s location is a separate setting. A request from the United States does not turn a Vienna schedule into an American schedule. Display settings also matter: in Cronpimo, the account time zone can differ from the job’s scheduling time zone. The Suite displays occurrences in the account time zone with their UTC offset. Compare the complete timestamp and offset, rather than the hour alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens at 02:30?
&lt;/h2&gt;

&lt;p&gt;In Vienna, 02:30 does not exist on 29 March 2026 because the clocks move forward. On 25 October 2026, 02:30 occurs twice. Schedulers do not all handle these cases the same way, so check the policy of the system you use.&lt;/p&gt;

&lt;p&gt;Cronpimo’s calendar scheduler skips a nonexistent local time and considers only the first occurrence of a repeated local time. A daily &lt;code&gt;30 2 * * *&lt;/code&gt; schedule therefore has no occurrence on 29 March. On 25 October its occurrence is at 02:30 UTC+02:00; a second occurrence at UTC+01:00 is not scheduled.&lt;/p&gt;

&lt;p&gt;That makes 02:30 a poor choice for a daily closing process that must not be missed. Choose a local time outside the transition window, or deliberately use a schedule defined in UTC. If every business day must be processed, the application should also identify missing work by business date. Scheduling alone does not provide that business-level check.&lt;/p&gt;

&lt;h2&gt;
  
  
  A short check before enabling the schedule
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Write the requirement explicitly: “every day at 08:00 in Vienna” or “every 300 seconds.”&lt;/li&gt;
&lt;li&gt;Select the matching calendar schedule and time zone, or interval and starting anchor.&lt;/li&gt;
&lt;li&gt;Inspect the upcoming occurrences in the preview. For a later clock change, test dates on both sides of that transition; the next five occurrences do not automatically cover every edge case.&lt;/li&gt;
&lt;li&gt;Distinguish scheduled start, actual start and successful completion. Allow for normal runtime and possible maintenance windows.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If an import must be finished by eight, eight is usually too late to start it. Allow suitable lead time and check completion separately. The &lt;a href="https://cronpimo.com/service/index.php?cronjob-monitoring/&amp;amp;l=2" rel="noopener noreferrer"&gt;cron job monitoring guide&lt;/a&gt; explains the different checkpoints. For work running on your own server, &lt;a href="https://cronpimo.com/service/index.php?heartbeat-monitoring/&amp;amp;l=2" rel="noopener noreferrer"&gt;heartbeat completion signals&lt;/a&gt; can help. If an API exposes the latest result, also check its &lt;a href="https://cronpimo.com/service/index.php?api-result-monitoring/&amp;amp;l=2" rel="noopener noreferrer"&gt;content and freshness&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A good schedule makes the expectation explicit. The result then tells you whether the expected work was actually completed.&lt;/p&gt;




&lt;p&gt;Originally published on Cronpimo: &lt;a href="https://cronpimo.com/index.php?article/6-every-morning-at-eight-choosing-schedules-and-time-zones/" rel="noopener noreferrer"&gt;Every morning at eight: choosing schedules and time zones&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Disclosure: This article and its cover were created with AI for Cronpimo. The scheduling examples were checked against Cronpimo’s current implementation and isolated tests.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>automation</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>HTTP 200, empty import: monitor a PHP job's actual result</title>
      <dc:creator>Cronpimo</dc:creator>
      <pubDate>Wed, 21 Oct 2026 06:34:22 +0000</pubDate>
      <link>https://dev.to/cronpimo-com/http-200-empty-import-monitor-a-php-jobs-actual-result-3093</link>
      <guid>https://dev.to/cronpimo-com/http-200-empty-import-monitor-a-php-jobs-actual-result-3093</guid>
      <description>&lt;p&gt;Your scheduled import returns HTTP 200, but the new records are missing from the shop. The request succeeded; the business outcome still needs checking.&lt;/p&gt;

&lt;p&gt;A small status endpoint can answer three more useful questions: did the import finish, did it commit enough records, and is the result recent? This tutorial separates the importer from the endpoint that observes it. It requires PHP 8.1 or later and works independently of the service or script you use to monitor that endpoint.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: This article was prepared with AI assistance. The author represents Cronpimo, where the original guide is published.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Define success beyond the status code
&lt;/h2&gt;

&lt;p&gt;For a frequently refreshed product feed, success might mean the import completed, at least one record was committed, and completion happened no more than 15 minutes ago:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"completed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"imported_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;142&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"updated_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1792458000&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;updated_at&lt;/code&gt; is a Unix timestamp in seconds: 20 October 2026 at 01:00 UTC. It records the import's completion. Do &lt;strong&gt;not&lt;/strong&gt; replace it with the current time whenever someone reads the status endpoint: that would make old data appear permanently fresh.&lt;/p&gt;

&lt;p&gt;The count and timestamp are JSON numbers, not strings containing digits. The required minimum count is a business decision: an incremental feed with no changes may legitimately produce zero records.&lt;/p&gt;

&lt;h2&gt;
  
  
  Expose a protected status endpoint
&lt;/h2&gt;

&lt;p&gt;Configure these environment variables for the PHP web process:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;MONITOR_TOKEN&lt;/code&gt;: a random secret of at least 32 characters.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;IMPORT_STATUS_FILE&lt;/code&gt;: an absolute path outside the public web directory.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, configure them in the relevant PHP-FPM pool. Exporting them in an interactive shell does not automatically configure the web process. Also configure HTTPS and forwarding of the Authorization header from your web server to PHP.&lt;/p&gt;

&lt;p&gt;The endpoint accepts only GET, checks a Bearer token, limits the stored JSON it reads and returns only the three intended fields.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="k"&gt;declare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strict_types&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;respond&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;never&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;http_response_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$code&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Content-Type: application/json; charset=utf-8'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Cache-Control: no-store'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'X-Content-Type-Options: nosniff'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nb"&gt;json_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;JSON_THROW_ON_ERROR&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'REQUEST_METHOD'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="s1"&gt;'GET'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Allow: GET'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;respond&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;405&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'error'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'method_not_allowed'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nv"&gt;$secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'MONITOR_TOKEN'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'IMPORT_STATUS_FILE'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;strlen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$secret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;$path&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;respond&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'error'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'status_unavailable'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nv"&gt;$auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'HTTP_AUTHORIZATION'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'REDIRECT_HTTP_AUTHORIZATION'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;hash_equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Bearer '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$auth&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'WWW-Authenticate: Bearer'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;respond&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'error'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'unauthorized'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nv"&gt;$raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="nb"&gt;file_get_contents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8193&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;is_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;strlen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;8192&lt;/span&gt;
        &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nb"&gt;json_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;JSON_THROW_ON_ERROR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;JsonException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;is_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;in_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'completed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'failed'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;is_int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'imported_count'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'imported_count'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;is_int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'updated_at'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'updated_at'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;respond&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'error'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'status_unavailable'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nf"&gt;respond&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="s1"&gt;'imported_count'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'imported_count'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="s1"&gt;'updated_at'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'updated_at'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A missing secret, unreadable file or malformed JSON produces HTTP 503. An incorrect Bearer token produces 401. A stored import status of &lt;code&gt;failed&lt;/code&gt; can still be read with HTTP 200: the status request works, while the content check detects the failed import.&lt;/p&gt;

&lt;p&gt;Keep customer data, internal file paths and import exception messages out of this response. Store operational error details in your own protected logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Publish a result only after the actual outcome
&lt;/h2&gt;

&lt;p&gt;In this example, the importer and endpoint run under the same operating-system user. Include the following function in the importer. It writes a temporary file in the destination directory, then replaces the published file after writing completes. This avoids exposing a partially written JSON document to the monitor.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="k"&gt;declare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strict_types&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Include in the importer; call only after its final outcome is known.&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;publishImportStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$count&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'IMPORT_STATUS_FILE'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;in_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'completed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'failed'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;$count&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;is_dir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RuntimeException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Invalid status configuration'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nv"&gt;$json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;json_encode&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'imported_count'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'updated_at'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="no"&gt;JSON_THROW_ON_ERROR&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$tmp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;tempnam&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'.import-status-'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$tmp&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RuntimeException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Cannot create status file'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Importer and status endpoint run as the same OS user here.&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$tmp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mo"&gt;0600&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;file_put_contents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$tmp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;LOCK_EX&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nb"&gt;strlen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;rename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$tmp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RuntimeException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Cannot publish import status'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;is_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$tmp&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nb"&gt;unlink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$tmp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After a successful import &lt;strong&gt;and database commit&lt;/strong&gt;, call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;publishImportStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'completed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$importedCount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;$importedCount&lt;/code&gt; must be the number of records actually committed by your importer. This tutorial deliberately does not invent an import algorithm or fill that number with a constant.&lt;/p&gt;

&lt;p&gt;On a caught import failure, publish &lt;code&gt;publishImportStatus('failed', 0)&lt;/code&gt; and preserve the import process's failure exit status. A failure to write the status file also belongs in the local error log.&lt;/p&gt;

&lt;p&gt;If the importer and endpoint use different operating-system accounts, configure narrowly scoped file permissions instead of making the file publicly readable. This example's &lt;code&gt;0600&lt;/code&gt; permission assumes the same account reads and writes the file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check completion, quantity and freshness together
&lt;/h2&gt;

&lt;p&gt;Make a GET request to the endpoint with the private header &lt;code&gt;Authorization: Bearer YOUR_TOKEN&lt;/code&gt;. Keep that token out of URLs, repositories and screenshots. Require HTTP 200, then validate all three values:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;JSON Pointer&lt;/th&gt;
&lt;th&gt;Required condition&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Equals the JSON string&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"completed"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/imported_count&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Is a number at least&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/updated_at&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Is a Unix timestamp no older than&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;900&lt;/code&gt; seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These paths use &lt;a href="https://www.rfc-editor.org/rfc/rfc6901" rel="noopener noreferrer"&gt;JSON Pointer&lt;/a&gt;, not JavaScript expressions. A missing field must not pass. The string &lt;code&gt;"142"&lt;/code&gt; is not the JSON number &lt;code&gt;142&lt;/code&gt;; reject wrong types rather than silently coercing them.&lt;/p&gt;

&lt;p&gt;For a Unix timestamp, age is &lt;code&gt;evaluation_time - updated_at&lt;/code&gt;. Check that the value is not unreasonably in the future as well; keep the application and monitoring clocks synchronized and choose an explicit small clock-skew tolerance. Send seconds, not milliseconds. If your checker also accepts RFC 3339, include a time zone, such as &lt;code&gt;2026-10-20T01:00:00Z&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The exact UI names depend on your monitoring tool. A custom checker can apply the same conditions. The important part is to require all three, rather than relying on a successful HTTP response alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify complete, empty and stale responses
&lt;/h2&gt;

&lt;p&gt;These reproducible examples use a fixed evaluation time of &lt;strong&gt;20 October 2026, 01:05 UTC&lt;/strong&gt;. They are fixtures, not live measurements. Update timestamps relative to the time of your own test.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Case&lt;/th&gt;
&lt;th&gt;status&lt;/th&gt;
&lt;th&gt;imported_count&lt;/th&gt;
&lt;th&gt;updated_at&lt;/th&gt;
&lt;th&gt;Expected result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Complete&lt;/td&gt;
&lt;td&gt;completed&lt;/td&gt;
&lt;td&gt;142&lt;/td&gt;
&lt;td&gt;1792458000&lt;/td&gt;
&lt;td&gt;Pass: 142 ≥ 1; age 300 ≤ 900 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Empty&lt;/td&gt;
&lt;td&gt;completed&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1792458000&lt;/td&gt;
&lt;td&gt;Fail: 0 is below the required minimum of 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stale&lt;/td&gt;
&lt;td&gt;completed&lt;/td&gt;
&lt;td&gt;142&lt;/td&gt;
&lt;td&gt;1792454400&lt;/td&gt;
&lt;td&gt;Fail: age 3,900 exceeds 900 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Also check the failure paths: a missing or wrong token should produce 401; malformed stored JSON should produce 503; and valid JSON containing &lt;code&gt;"status":"failed"&lt;/code&gt; should fail your result rule even though reading it succeeds with HTTP 200.&lt;/p&gt;

&lt;p&gt;Choose limits that fit the real schedule. A 15-minute maximum age is usually unsuitable for a daily full import. Allow its schedule and normal completion delay, or perform the check specifically after the expected completion time. Likewise, impose a minimum count only when zero would actually indicate a problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate starting work from observing it
&lt;/h2&gt;

&lt;p&gt;Reading this status endpoint does not run an import. It observes the latest recorded outcome. That separation matters for a long-running process: the initial API call might only put work on a queue and return before the import begins.&lt;/p&gt;

&lt;p&gt;For a local scheduled script, publish the status after the real outcome and have the monitor check it on an appropriate schedule. Another option is a completion heartbeat, with a deadline for the next expected signal. Whichever signal you choose, a previous successful run must eventually become too old to stand in for the next one.&lt;/p&gt;

&lt;p&gt;Finally, test both detection and delivery. A failed fixture should be recorded as a failure and reach the alert destination you chose. A successful test message by itself verifies only that messages can be delivered.&lt;/p&gt;

&lt;p&gt;References: &lt;a href="https://www.php.net/manual/en/function.json-decode.php" rel="noopener noreferrer"&gt;PHP JSON decoding&lt;/a&gt;, &lt;a href="https://www.php.net/manual/en/function.hash-equals.php" rel="noopener noreferrer"&gt;constant-time string comparison with hash_equals&lt;/a&gt;, &lt;a href="https://www.rfc-editor.org/rfc/rfc6901" rel="noopener noreferrer"&gt;JSON Pointer, RFC 6901&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>php</category>
      <category>tutorial</category>
      <category>webdev</category>
      <category>monitoring</category>
    </item>
  </channel>
</rss>
