
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/">
    <channel>
        <title><![CDATA[ The Cloudflare Blog ]]></title>
        <description><![CDATA[ Get the latest news on how products at Cloudflare are built, technologies used, and join the teams helping to build a better Internet. ]]></description>
        <link>https://blog.cloudflare.com</link>
        <atom:link href="https://blog.cloudflare.com/" rel="self" type="application/rss+xml"/>
        <language>en-us</language>
        <image>
            <url>https://blog.cloudflare.com/favicon.png</url>
            <title>The Cloudflare Blog</title>
            <link>https://blog.cloudflare.com</link>
        </image>
        <lastBuildDate>Tue, 14 Apr 2026 21:24:00 GMT</lastBuildDate>
        <item>
            <title><![CDATA[Adopting OpenTelemetry for our logging pipeline]]></title>
            <link>https://blog.cloudflare.com/adopting-opentelemetry-for-our-logging-pipeline/</link>
            <pubDate>Mon, 03 Jun 2024 13:00:41 GMT</pubDate>
            <description><![CDATA[ Recently, Cloudflare's Observability team undertook an effort to migrate our existing syslog-ng backed logging infrastructure to instead being backed by OpenTelemetry Collectors. In this post, we detail the process that we undertook, and the difficulties we faced along the way ]]></description>
            <content:encoded><![CDATA[ <p></p><p>Cloudflare’s logging pipeline is one of the largest data pipelines that Cloudflare has, serving millions of log events per second globally, from every server we run. Recently, we undertook a project to migrate the underlying systems of our logging pipeline from <a href="https://www.syslog-ng.com/">syslog-ng</a> to <a href="https://github.com/open-telemetry/opentelemetry-collector">OpenTelemetry Collector</a> and in this post we want to share how we managed to swap out such a significant piece of our infrastructure, why we did it, what went well, what went wrong, and how we plan to improve the pipeline even more going forward.</p>
    <div>
      <h2>Background</h2>
      <a href="#background">
        
      </a>
    </div>
    <p>A full breakdown of our existing infrastructure can be found in our previous post <a href="/an-overview-of-cloudflares-logging-pipeline">An overview of Cloudflare's logging pipeline</a>, but to quickly summarize here:</p><ul><li><p>We run a syslog-ng daemon on every server, reading from the local systemd-journald journal, and a set of named pipes.</p></li><li><p>We forward those logs to a set of centralized “log-x receivers”, in one of our core data centers.</p></li><li><p>We have a dead letter queue destination in another core data center, which receives messages that could not be sent to the primary receiver, and which get mirrored across to the primary receivers when possible.</p></li></ul>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/5sjQKVdQN2fHiKpXDoz3KP/403e5d362685125b8c6fc7b8c7e60d7a/image1-16.png" />
            
            </figure><p>The goal of this project was to replace those syslog-ng instances as transparently as possible. That means we needed to implement all these behaviors as precisely as possible, so that we didn’t need to modify any downstream systems.</p><p>There were a few reasons for wanting to make this shift, and enduring the difficulties of overhauling such a large part of our infrastructure:</p><ul><li><p>syslog-ng is written in C, which is not a core competency of our team. While we have made upstream contributions to the project in the past, and the experience was great, having the OpenTelemetry collector in Go allows much more of our team to be able to contribute improvements to the system.</p></li><li><p>Building syslog-ng against our internal Post-Quantum cryptography libraries was difficult, due to having to maintain an often brittle C build chain, whereas our engineering teams have optimized the Go build model to make this as simple as possible.</p></li><li><p>OpenTelemetry Collectors have built in support for Prometheus metrics, which allows us to gather much deeper levels of telemetry data around what the collectors are doing, and surface these insights as “meta-observability” to our engineering teams.</p></li><li><p>We already use OpenTelemetry Collectors for some of our tracing infrastructure, so unifying onto one daemon rather than having separate collectors for all our different types of telemetry reduces the cognitive load on the team.</p></li></ul>
    <div>
      <h2>The Migration Process</h2>
      <a href="#the-migration-process">
        
      </a>
    </div>
    
    <div>
      <h3>What we needed to build</h3>
      <a href="#what-we-needed-to-build">
        
      </a>
    </div>
    <p>While the upstream <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib">contrib repository</a> contains a wealth of useful components, all packaged into its own distribution, it became clear early on that we would need our own internal components. Having our own internal components would require us to build our own distribution, so one of the first things we did was turn to <a href="https://pkg.go.dev/go.opentelemetry.io/collector/cmd/builder">OCB</a> (OpenTelemetry Collector Builder) to provide us a way to build an internal distribution of an OpenTelemetry Collector. We eventually ended up templating our OCB configuration file to automatically include all the internal components we have built, so that we didn’t have to add them manually.</p><p>In total, we built four internal components for our initial version of the collector.</p>
    <div>
      <h4>cfjs1exporter</h4>
      <a href="#cfjs1exporter">
        
      </a>
    </div>
    <p>Internally, our logging pipeline uses a line format we call “cfjs1”. This format describes a JSON encoded log, with two fields: a <code>format</code> field, that decides the type of the log, and a “wrapper” field which contains the log body (which is a structured JSON object in and of itself), with a field name that changes depending on the <code>format</code> field. These two fields decide which Kafka topic our receivers will end up placing the log message in.</p><p>Because we didn’t want to make changes to other parts of the pipeline, we needed to support this format in our collector. To do this, we took inspiration from the contrib repository’s <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/exporter/syslogexporter">syslogexporter</a>, building our cfjs1 format into it.</p><p>Ultimately, we would like to move towards using <a href="https://opentelemetry.io/docs/specs/otel/protocol/">OTLP</a> (OpenTelemetry Protocol) as our line format. This would allow us to remove our custom exporter, and utilize open standards, enabling easier migrations in the future.</p>
    <div>
      <h4>fileexporter</h4>
      <a href="#fileexporter">
        
      </a>
    </div>
    <p>While the upstream contrib repo does have a <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/exporter/fileexporter">file exporter</a> component, it only supports two formats: JSON and Protobuf. We needed to support two other formats, plain text and syslog, so we ended up forking the file exporter internally. Our plain text formatter simply outputs the body of the log message into a file, with newlines as a delimiter. Our syslog format outputs <a href="https://www.rfc-editor.org/rfc/rfc5424.html">RFC 5424</a> formatted syslog messages into a file.</p><p>The other feature we implemented on our internal fork was custom permissions. The upstream file exporter is a bit of a mess, in that it actually has two different modes of operation – a standard mode, not utilizing any of the compression or rotation features, and a more advanced mode which uses those features. Crucially, if you want to use any of the rotation features, you end up using <a href="https://github.com/natefinch/lumberjack">lumberjack</a>, whereas without those features you use a more native file handling. This leads to strange issues where some features of the exporter are supported in one mode, but not the other. In the case of permissions, the community <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/31459">seems open to the idea</a> in the native handling, but <a href="https://github.com/natefinch/lumberjack/issues/164">lumberjack seems against the idea</a>. This dichotomy is what led us to implement it ourselves internally.</p><p>Ultimately, we would love to upstream these improvements should the community be open to them. Having support for custom marshallers (<a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/30331">https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/30331</a>) would have made this a bit easier, however it’s not clear how that would work with OCB. Either that, or we could open source them in the <a href="https://github.com/cloudflare">Cloudflare organization</a>, but we would love to remove the need to maintain our own fork in the future.</p>
    <div>
      <h4>externaljsonprocessor</h4>
      <a href="#externaljsonprocessor">
        
      </a>
    </div>
    <p>We want to set the value of an attribute/field that comes from external sources: either from an HTTP endpoint or an output from running a specific command. In syslog-ng, we have a sidecar service that generates a syslog-ng configuration to achieve this. In replacing syslog-ng with our OpenTelemetry Collector, we thought it would be easier to implement this feature as a custom component of our collector instead.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/WG45quu9J9Mpm1SxOCQR8/0f2761748bb68af101ed2880cb5e2fbb/image2-14.png" />
            
            </figure><p>To that end, we implemented an “external JSON processor”, which is able to periodically query external data sources and add those fields to all the logs that flow through the processor. Cloudflare has many internal tools and APIs, and we use this processor to fetch data like the status of a data center, or the status of a systemd unit. This enables our engineers to have more filtering options, such as to exclude logs from data centers that are not supposed to receive customer traffic, or servers that are disabled for maintenance. Crucially, this allows us to update these values much faster than the standard three-hour cadence of other configuration updates through salt, allowing more rapid updates to these fields that may change quickly as we operate our network.</p>
    <div>
      <h4>ratelimit processor</h4>
      <a href="#ratelimit-processor">
        
      </a>
    </div>
    <p>The last component we needed to implement was a replacement for the syslog-ng <a href="https://www.syslog-ng.com/technical-documents/doc/syslog-ng-open-source-edition/3.36/administration-guide/69">ratelimit filter</a>, also contributed by us upstream. The ratelimit filter allows applying rate limits based on a specific field of a log message, dropping messages that exceed some limit (with an optional burst limit). In our case, we apply rate limits over the <code>service</code> field, ensuring that no individual service can degrade the log collection for any other.</p><p>While there has been some upstream discussion of <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/6908">similar components</a>, we couldn’t find anything that explicitly fit our needs. This was especially true when you consider that in our case the data loss during the rate limiting process is intentional, something that might be hard to sell when trying to build something more generally applicable.</p>
    <div>
      <h3>How we migrated</h3>
      <a href="#how-we-migrated">
        
      </a>
    </div>
    <p>Once we had an OpenTelemetry Collector binary, we had to deploy it. Our deployment process took two forks: Deploying to our core data centers, and deploying to our edge data centers. For those unfamiliar, Cloudflare’s core data centers contain a small number of servers with a very diverse set of workloads, from Postgresql, to ElasticSearch, to Kubernetes, and everything in between. Our edge data centers, on the other hand, are much more homogenous. They contain a much larger number of servers, each one running the same set of services.</p><p>Both edge and core use <a href="https://saltproject.io/index.html">salt</a> to configure the services running on their servers. This meant that the first step was to write salt states that would install the OpenTelemetry collector, and write the appropriate configurations to disk. Once we had those in place, we also needed to write some temporary migration pieces that would disable syslog-ng and start the OpenTelemetry collector, as well as the inverse in the case of a roll back.</p><p>For the edge data centers, once we had a set of configurations written, it mostly came down to rolling the changes out gradually across the edge servers. Because edge servers run the same set of services, once we had gained confidence in our set of configurations, it became a matter of rolling out the changes slowly and monitoring the logging pipelines along the way. We did have a few false starts here, and needed to instrument our cfjs1exporter a bit more to work around issues surrounding some of our more niche services and general Internet badness which we’ll detail below in our lessons learned.</p><p>The core data centers required a more hands-on approach. Many of our services in core have custom syslog-ng configurations. For example, our Postgresql servers have custom handling for their audit logs, and our Kubernetes servers have custom handling for <a href="https://projectcontour.io/">contour</a> ingress and error logs. This meant that each role with a custom config had to be manually onboarded, with extensive testing on the designated canary nodes of each role to validate the configurations.</p>
    <div>
      <h2>Lessons Learned</h2>
      <a href="#lessons-learned">
        
      </a>
    </div>
    
    <div>
      <h3>Failover</h3>
      <a href="#failover">
        
      </a>
    </div>
    <p>At Cloudflare, we regularly schedule chaos testing on our core data centers which contain our centralized log receivers. During one of these chaos tests, our cfjs1 exporter did not notice that it could not send to the primary central logging server. This caused our collector to not failover to the secondary central logging server and its log buffer to fill up, which resulted in the collector failing to consume logs from its receivers. This is not a problem with journal receivers since logs are buffered by journald before they get consumed by the collector, but it is a different case with named pipe receivers. Due to this bug, our collectors stopped consuming logs from named pipes, and services writing to these named pipes started blocking threads waiting to write to them. Our syslog-ng deployment solved this issue using a <a href="https://mmonit.com/">monit script</a> to periodically kill the connections between syslog-ng and the central receivers, however we opted to solve this more explicitly in our exporter by building in much tighter timeouts, and modifying the upstream <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/connector/failoverconnector">failover receiver</a> to better respond to these partial failures.</p>
    <div>
      <h3>Cutover delays</h3>
      <a href="#cutover-delays">
        
      </a>
    </div>
    <p>As we’ve <a href="/how-we-use-hashicorp-nomad">previously blogged about</a>, at Cloudflare, we use <a href="https://www.nomadproject.io/">Nomad</a> for running dynamic tasks in our edge data centers. We use a custom driver to run containers and this custom driver handles the shipping of logs from the container to a named pipe.</p><p>We did the migration from syslog-ng to OpenTelemetry Collectors while servers were live and running production services. During the migration, there was a gap when syslog-ng was stopped by our configuration management and our OpenTelemetry collector was started on the server. This gap caused the logs in the named pipe to not get consumed and similar to the previous named pipe, the services writing to the named pipe receiver in blocking mode got affected. Similar to NGINX and Postgresql, Cloudflare’s driver for Nomad also writes logs to the named pipe driver in blocking mode. Because of this delay, the driver timed out sending logs and rescheduled the containers.</p><p>We ultimately caught this pretty early on in testing, and changed our approach to the rollout. Instead of using Salt to separately stop syslog-ng and start the collector, we instead used salt to schedule a systemd “one shot” service that simultaneously stopped syslog-ng and started the collector, minimizing the downtime between the two.</p>
    <div>
      <h2>What’s next?</h2>
      <a href="#whats-next">
        
      </a>
    </div>
    <p>Migrating such a critical part of our infrastructure is never easy, especially when it has remained largely untouched for nearly half a decade. Even with the issues we hit during our rollout, migrating to an OpenTelemetry Collector unlocks so many more improvements to our logging pipeline going forward. With the initial deployment complete, there are a number of changes we’re excited to work on next, including:</p><ul><li><p>Better handling for log sampling, including tail sampling</p></li><li><p>Better insights for our engineering teams on their telemetry production</p></li><li><p>Migration to OTLP as our line protocol</p></li><li><p>Upstreaming of some of our custom components</p></li></ul><p>If that sounds interesting to you, <a href="https://boards.greenhouse.io/cloudflare/jobs/5563753?gh_jid=5563753">we’re hiring engineers to come work on our logging pipeline</a>, so please reach out!</p> ]]></content:encoded>
            <category><![CDATA[Observability]]></category>
            <category><![CDATA[Engineering]]></category>
            <guid isPermaLink="false">1Ykpgu09F0QGyb0bptVin4</guid>
            <dc:creator>Colin Douch</dc:creator>
            <dc:creator>Jayson Cena</dc:creator>
        </item>
        <item>
            <title><![CDATA[Reclaiming CPU for free with Go's Profile Guided Optimization]]></title>
            <link>https://blog.cloudflare.com/reclaiming-cpu-for-free-with-pgo/</link>
            <pubDate>Tue, 14 May 2024 13:00:32 GMT</pubDate>
            <description><![CDATA[ Golang 1.20 introduced support for Profile Guided Optimization (PGO) to the go compiler. This post covers the process we created for experimenting with PGO at Cloudflare, and measuring the CPU savings ]]></description>
            <content:encoded><![CDATA[ <p></p><p><a href="https://tip.golang.org/doc/go1.20">Golang 1.20</a> introduced support for Profile Guided Optimization (PGO) to the go compiler. This allows guiding the compiler to introduce optimizations based on the real world behaviour of your system. In the Observability Team at Cloudflare, we maintain a few Go-based services that use thousands of cores worldwide, so even the 2-7% savings advertised would drastically reduce our CPU footprint, effectively for free. This would reduce the CPU usage for our internal services, freeing up those resources to serve customer requests, providing measurable improvements to our customer experience. In this post, I will cover the process we created for experimenting with PGO – collecting representative profiles across our production infrastructure and then deploying new PGO binaries and measuring the CPU savings.</p>
    <div>
      <h3>How does PGO work?</h3>
      <a href="#how-does-pgo-work">
        
      </a>
    </div>
    <p>PGO itself is not a Go-specific tool, although it is relatively new. PGO allows you to take CPU profiles from a program running in production and use that to optimise the generated assembly for that program. This includes a bunch of different optimisations such as inlining heavily used functions more aggressively, reworking branch prediction to favour the more common branches, and rearranging the generated code to lump hot paths together to save on CPU cache swapping.</p><p>The general flow for using PGO is to compile a non-PGO binary and deploy it to production, collect CPU profiles from the binary in production, and then compile a <i>second</i> binary using that CPU profile. CPU Profiles contain samples of what the CPU was spending the most time on when executing a program, which provides valuable context to the compiler when it’s making decisions about optimising a program. For example, the compiler may choose to inline a function that is called many times to reduce the function call overhead, or it might choose to unroll a particularly jump-heavy loop. Crucially, using a profile from production can guide the compiler much more efficiently than any upfront heuristics.</p>
    <div>
      <h3>A practical example</h3>
      <a href="#a-practical-example">
        
      </a>
    </div>
    <p>In the Observability team, we operate a system we call “wshim”. Wshim is a service that runs on every one of our edge servers, providing a push gateway for telemetry sourced from our internal Cloudflare Workers. Because this service runs on every server, and is called every time an internal worker is called, wshim requires a lot of CPU time to run. In order to track exactly how much, we put wshim into its own <a href="https://en.wikipedia.org/wiki/Cgroups">cgroup</a>, and use <a href="https://github.com/google/cadvisor">cadvisor</a> to expose Prometheus metrics pertaining to the resources that it uses.</p><p>Before deploying PGO, wshim was using over 3000 cores globally:</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/1YefuIqASjynzrsnMuKMYk/9d5ef360c79393dac4771b21f7cdc1a3/image1-3.png" />
            
            </figure><p><code>container_cpu_time_seconds</code> is our internal metric that tracks the amount of time a CPU has spent running wshim across the world. Even a 2% saving would return 60 cores to our customers, making the Cloudflare network even more efficient.</p><p>The first step in deploying PGO was to collect representative profiles from our servers worldwide. The first problem we run into is that we run thousands of servers, each with different usage patterns at given points in time – a datacenter serving lots of requests during daytime hours will have a different usage pattern than a different data center that locally is in the middle of the night. As such, selecting exactly which servers to profile is paramount to collecting good profiles for PGO to use.</p><p>In the end, we decided that the best samples would be from those datacenters experiencing heavy load – those are the ones where the slowest parts of wshim would be most obvious. Even further, we will only collect profiles from our Tier 1 data centers. These are data centers that serve our most heavily populated regions, are generally our largest, and are generally under very heavy loads during peak hours.</p><p>Concretely, we can get a list of high CPU servers by querying our <a href="https://thanos.io/">Thanos</a> infrastructure:</p>
            <pre><code>num_profiles="1000"

# Fetch the top n CPU users for wshim across the edge using Thanos.
cloudflared access curl "https://thanos/api/v1/query?query=topk%28${num_profiles}%2Cinstance%3Acontainer_cpu_time_seconds_total%3Arate2m%7Bapp_name%3D%22wshim.service%22%7D%29&amp;dedup=true&amp;partial_response=true" --compressed | jq '.data.result[].metric.instance' -r &gt; "${instances_file}"</code></pre>
            <p>Go makes actually fetching CPU profiles trivial with <a href="https://pkg.go.dev/net/http/pprof">pprof</a>. In order for our engineers to debug their systems in production, we provide a method to easily retrieve production profiles that we can use here. Wshim provides a pprof interface that we can use to retrieve profiles, and we can collect these again with bash:</p>
            <pre><code># For every instance, attempt to pull a CPU profile. Note that due to the transient nature of some data centers
# a certain percentage of these will fail, which is fine, as long as we get enough nodes to form a representative sample.
while read instance; do fetch-pprof $instance –port 8976 –seconds 30' &gt; "${working_dir}/${instance}.pprof" &amp; done &lt; "${instances_file}"

wait $(jobs -p)</code></pre>
            <p>And then merge all the gathered profiles into one, with go tool:</p>
            <pre><code># Merge the fetched profiles into one.
go tool pprof -proto "${working_dir}/"*.pprof &gt; profile.pprof</code></pre>
            <p>It’s this merged profile that we will use to compile our pprof binary. As such, we commit it to our repo so that it lives alongside all the other deployment components of wshim:</p>
            <pre><code>~/cf-repos/wshim ± master
23/01/2024 10:49:08 AEDT❯ tree pgo
pgo
├── README.md
├── fetch-profiles.sh
└── profile.pprof</code></pre>
            <p>And update our Makefile to pass in the <code>-pgo</code> flag to the <code>go build</code> command:</p>
            <pre><code>build:
       go build -pgo ./pgo/profile.pprof -o /tmp/wshim ./cmd/wshim</code></pre>
            <p>After that, we can build and deploy our new PGO optimized version of wshim, like any other version.</p>
    <div>
      <h3>Results</h3>
      <a href="#results">
        
      </a>
    </div>
    <p>Once our new version is deployed, we can review our CPU metrics to see if we have any meaningful savings. Resource usages are notoriously hard to compare. Because wshim’s CPU usage scales with the amount of traffic that any given server is receiving, it has a lot of potentially confounding variables, including the time of day, day of the year, and whether there are any active attacks affecting the datacenter. That being said, we can take a couple of numbers that might give us a good indication of any potential savings.</p><p>Firstly, we can look at the CPU usage of wshim immediately before and after the deployment. This may be confounded by the time difference between the sets, but it shows a decent improvement. Because our release takes just under two hours to roll to every tier 1 datacenter, we can use PromQLs `offset` operator to measure the difference:</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3enVHBJjLVGDCSBBRqrLAn/157a65a91619f9252ad8819600241921/image3-1.png" />
            
            </figure>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/I7piaGicvlQbFmzpiHxUT/65553d182d296b4b1d663b69eb34cb41/image4-1.png" />
            
            </figure><p>This indicates that following the release, we’re using ~97 cores fewer than before the release, a ~3.5% reduction. This seems to be inline with the <a href="https://go.dev/doc/pgo#overview">upstream documentation</a> that gives numbers between 2% and 14%.</p><p>The second number we can look at is the usage at the same time of day on different days of the week. The average usage for the 7 days prior to the release was 3067.83 cores, whereas the 7 days <i>after</i> the release were 2996.78, a savings of 71 CPUs. Not quite as good as our 97 CPU savings, but still pretty substantial!</p><p>This seems to prove the benefits of PGO – without changing the code at all, we managed to save ourselves several servers worth of CPU time.</p>
    <div>
      <h3>Future work</h3>
      <a href="#future-work">
        
      </a>
    </div>
    <p>Looking at these initial results certainly seems to prove the case for PGO – saving multiple servers worth of CPU without any code changes is a big win for freeing up resources to better serve customer requests. However, there is definitely more work to be done here. In particular:</p><ul><li><p>Automating the collection of profiles, perhaps using <a href="https://www.cncf.io/blog/2022/05/31/what-is-continuous-profiling/">continuous profiling</a></p></li><li><p>Refining the deployment process to handle the new “two-step deployment”, deploying a non PGO binary, and then a PGO one</p></li><li><p>Refining our techniques to derive representative profiling samples</p></li><li><p>Implementing further improvements with <a href="https://github.com/facebookarchive/BOLT">BOLT</a>, or other Link Time Optimization (LTO) techniques</p></li></ul><p><i>If that sounds interesting to you, we’re hiring in both the </i><a href="https://boards.greenhouse.io/cloudflare/jobs/5563753?gh_jid=5563753"><i>USA</i></a><i> and </i><a href="https://boards.greenhouse.io/cloudflare/jobs/5443710?gh_jid=5443710"><i>EMEA</i></a><i>!</i></p> ]]></content:encoded>
            <category><![CDATA[Observability]]></category>
            <category><![CDATA[Performance]]></category>
            <guid isPermaLink="false">7By5SCb5dDWXN5ymX5WfgD</guid>
            <dc:creator>Colin Douch</dc:creator>
        </item>
        <item>
            <title><![CDATA[An overview of Cloudflare's logging pipeline]]></title>
            <link>https://blog.cloudflare.com/an-overview-of-cloudflares-logging-pipeline/</link>
            <pubDate>Mon, 08 Jan 2024 14:00:21 GMT</pubDate>
            <description><![CDATA[ In this post, we’re going to go over what that looks like, how we achieve high availability, and how we meet our Service Level Objectives (SLOs) while shipping close to a million log lines per second ]]></description>
            <content:encoded><![CDATA[ <p></p><p>One of the roles of Cloudflare's Observability Platform team is managing the operation, improvement, and maintenance of our internal logging pipelines. These pipelines are used to ship debugging logs from every service across Cloudflare’s infrastructure into a centralised location, allowing our engineers to operate and debug their services in near real time. In this post, we’re going to go over what that looks like, how we achieve high availability, and how we meet our Service Level Objectives (SLOs) while shipping close to a million log lines per second.</p><p>Logging itself is a simple concept. Virtually every programmer has written a <code>Hello, World!</code> program at some point. Printing something to the console like that is logging, whether intentional or not.</p><p>Logging <i>pipelines</i> have been around since the beginning of computing itself. Starting with putting string lines in a file, or simply in memory, our industry quickly outgrew the concept of each machine in the network having its own logs. To centralise logging, and to provide scaling beyond a single machine, we used protocols such as the <a href="https://www.ietf.org/rfc/rfc3164.txt">BSD Syslog Protocol</a> to provide a method for individual machines to send logs over the network to a collector, providing a single pane of glass for logs over an entire set of machines.</p><p>Our logging infrastructure at Cloudflare is a bit more complicated, but still builds on these foundational principles.</p>
    <div>
      <h3>The beginning</h3>
      <a href="#the-beginning">
        
      </a>
    </div>
    <p>Logs at Cloudflare start the same as any other, with a <code>println</code>. Generally systems don’t call println directly however, they outsource that logic to a logging library. Systems at Cloudflare use various logging libraries such as Go’s <a href="https://github.com/rs/zerolog">zerolog</a>, C++’s <a href="https://github.com/capnproto/capnproto/blob/7fc185568b4649d1fae97b0791a8f7aa110bfa5d/kjdoc/tour.md">KJ_LOG</a>, or Rusts <a href="https://docs.rs/log/latest/log/">log</a>, however anything that is able to print lines to a program's stdout/stderr streams is compatible with our pipeline. This offers our engineers the greatest flexibility in choosing tools that work for them and their teams.</p><p>Because we use systemd for most of our service management, these stdout/stderr streams are generally piped into systemd-journald which handles the local machine logs. With its <code>RateLimitBurst</code> and <code>RateLimitInterval</code> configurations, this gives us a simple knob to control the output of any given service on a machine. This has given our logging pipeline the colloquial name of the “journal pipeline”, however as we will see, our pipeline has expanded far beyond just journald logs.</p>
    <div>
      <h3>Syslog-NG</h3>
      <a href="#syslog-ng">
        
      </a>
    </div>
    <p>While journald provides us a method to collect logs on every machine, logging onto each machine individually is impractical for debugging large scale services. To this end, the next step of our pipeline is <a href="https://github.com/syslog-ng/syslog-ng">syslog-ng</a>. Syslog-ng is a daemon that implements the aforementioned BSD syslog protocol. In our case, it reads logs from journald, and applies another layer of rate limiting. It then applies rewriting rules to add common fields, such as the name of the machine that emitted the log, the name of the data center the machine is in, and the state of the data center that the machine is in. It then wraps the log in a JSON wrapper and forwards it to our Core data centers.</p><p>journald itself has an interesting feature that makes it difficult for some of our use cases - it guarantees a global ordering of every log on a machine. While this is convenient for the single node case, it imposes the limitation that journald is <a href="https://github.com/systemd/systemd/issues/15677">single-threaded</a>. This means that for our heavier workloads, where every millisecond of delay counts, we provide a more direct path into our pipeline. In particular, we offer a Unix Domain Socket that syslog-ng listens on. This socket operates as a separate source of logs into the same pipeline that the journald logs follow, but allows greater throughput by eschewing the need for a global ordering that journald enforces. Logging in this manner is a bit more involved than outputting logs to the stdout streams, as services have to have a pipe created for them and then manually open that socket to write to. As such, this is generally reserved for services that need it, and don’t mind the management overhead it requires.</p>
    <div>
      <h3>log-x</h3>
      <a href="#log-x">
        
      </a>
    </div>
    <p>Our logging pipeline is a critical service at Cloudflare. Any potential delays or missing data can cause downstream effects that may hinder or even prevent the resolving of customer facing incidents. Because of this strict requirement, we have to offer redundancy in our pipeline. This is where the operation we call “log-x” comes into play.</p><p>We operate two main core data centers. One in the United States, and one in Europe. From each machine, we ship logs to both of these data centers. We call these endpoints log-a, and log-b. The log-a and log-b receivers will insert the logs into a Kafka topic for later consumption. By duplicating the data to two different locations, we achieve a level of redundancy that can handle the failure of either data center.</p><p>The next problem we encounter is that we have many <a href="https://www.cloudflare.com/learning/cdn/glossary/data-center/">data centers</a> all around the world, which at any time due to changing Internet conditions may become disconnected from one, or both core data centers. If the data center is disconnected for long enough we may end up in a situation where we drop logs to either the log-a or log-b receivers. This would result in an incomplete view of logs from one data center and is unacceptable; Log-x was designed to alleviate this problem. In the event that syslog-ng fails to send logs to either log-a or log-b, it will actually send the log <i>twice</i> to the available receiver. This second copy will be marked as actually destined for the other log-x receiver. When a log-x receiver receives such a log, it will insert it into a <i>different</i> Kafka queue, known as the Dead Letter Queue (DLQ). We then use <a href="https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=27846330">Kafka Mirror Maker</a> to sync this DLQ across to the data center that was inaccessible. With this logic log-x allows us to maintain a full copy of all the logs in each core data center, regardless of any transient failures from any of our data centers.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/1PLoVSIH48LYsFxCwUK9vf/85598f497674f9509fced29decaf7e58/Blog-1945_Kafka.png" />
            
            </figure>
    <div>
      <h3>Kafka</h3>
      <a href="#kafka">
        
      </a>
    </div>
    <p>When logs arrive in the core data centers, we buffer them in a Kafka queue. This provides a few benefits. Firstly, it means that any consumers of the logs can be added without any changes - they only need to register with Kafka as a consumer group on the logs topic. Secondly, it allows us to tolerate transient failures of the consumers without losing any data. Because the Kafka clusters in the core data centers are much larger than any single machine, Kafka allows us to tolerate up to eight hours of total outage for our consumers without losing any data. This has proven to be enough to recover without data loss from all but the largest of incidents.</p><p>When it comes to partitioning our Kafka data, we have an interesting dilemma. Rather arbitrarily, the syslog protocol only supports <a href="https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.3.1">timestamps up to microseconds</a>. For our faster log emitters, this means that the syslog protocol cannot guarantee ordering with timestamps alone. To work around this limitation, we partition our logs using a key made up of both the host, and the service name. Because Kafka guarantees ordering within a partition, this means that any logs from a service on a machine are guaranteed to be ordered between themselves. Unfortunately, because logs from a service can have vastly different rates between different machines, this can result in unbalanced Kafka partitions. We have an ongoing project to move towards <a href="https://opentelemetry.io/docs/specs/otel/protocol/">Open Telemetry Logs</a> to combat this.</p>
    <div>
      <h3>Onward to storage</h3>
      <a href="#onward-to-storage">
        
      </a>
    </div>
    <p>With the logs in Kafka, we can proceed to insert them into a more long term storage. For storage, we operate two backends. An ElasticSearch/Logstash/Kibana (ELK) stack, and a Clickhouse cluster.</p><p>For ElasticSearch, we split our cluster of 90 nodes into a few types. The first being “master” nodes. These nodes act as the ElasticSearch masters, and coordinate insertions into the cluster. We then have “data” nodes that handle the actual insertion and storage. Finally, we have the “HTTP” nodes that handle HTTP queries. Traditionally in an ElasticSearch cluster, all the data nodes will also handle HTTP queries, however because of the size of our cluster and shards we have found that designating only a few nodes to handle <a href="https://www.cloudflare.com/learning/ddos/glossary/hypertext-transfer-protocol-http/">HTTP requests</a> greatly reduces our query times by allowing us to take advantage of aggressive caching.</p><p>On the Clickhouse side, we operate a ten node Clickhouse cluster that stores our service logs. We are in the process of migrating this to be our primary storage, but at the moment it provides an alternative interface into the same logs that ElasticSearch provides, allowing our Engineers to use either Lucene through the ELK stack, or SQL and Bash scripts through the Clickhouse interface.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/7iTClO1Axdg7wI0UVLK0oL/13e1a46890d207290f8ee0a230af6e03/Blog-1945_What-s-next_.png" />
            
            </figure>
    <div>
      <h2>What’s next?</h2>
      <a href="#whats-next">
        
      </a>
    </div>
    <p>As Cloudflare continues to grow, our demands on our Observability systems, and our logging pipeline in particular continue to grow with it. This means that we’re always thinking ahead to what will allow us to scale and improve the experience for our engineers. On the horizon, we have a number of projects to further that goal including:</p><ul><li><p>Increasing our multi-tenancy capabilities with better resource insights for our engineers</p></li><li><p>Migrating our syslog-ng pipeline towards Open Telemetry</p></li><li><p>Tail sampling rather than our probabilistic head sampling we have at the moment</p></li><li><p>Better balancing for our Kafka clusters</p></li></ul><p>If you’re interested in working with logging at Cloudflare, then reach out - <a href="https://www.cloudflare.com/en-gb/careers/jobs/?department=Production+Engineering">we’re hiring</a>!</p> ]]></content:encoded>
            <category><![CDATA[Observability]]></category>
            <category><![CDATA[Logs]]></category>
            <guid isPermaLink="false">2oaeQB5iiazhLNCnVk7Cn6</guid>
            <dc:creator>Colin Douch</dc:creator>
        </item>
    </channel>
</rss>