<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Elastic Observability Labs - Articles by Philipp Kahr</title>
        <link>https://www.elastic.co/observability-labs</link>
        <description>Trusted security news &amp; research from the team at Elastic.</description>
        <lastBuildDate>Thu, 04 Jun 2026 17:54:35 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Elastic Observability Labs - Articles by Philipp Kahr</title>
            <url>https://www.elastic.co/observability-labs/assets/observability-labs-thumbnail.png</url>
            <link>https://www.elastic.co/observability-labs</link>
        </image>
        <copyright>© 2026. Elasticsearch B.V. All Rights Reserved</copyright>
        <item>
            <title><![CDATA[Monitoring service performance: An overview of SLA calculation for Elastic Observability]]></title>
            <link>https://www.elastic.co/observability-labs/blog/observability-sla-calculations-transforms</link>
            <guid isPermaLink="false">observability-sla-calculations-transforms</guid>
            <pubDate>Mon, 24 Apr 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[Elastic Stack provides many valuable insights for different users, such as reports on service performance and if the service level agreement (SLA) is met. In this post, we’ll provide an overview of calculating an SLA for Elastic Observability.]]></description>
            <content:encoded><![CDATA[<p>Elastic Stack provides many valuable insights for different users. Developers are interested in low-level metrics and debugging information. <a href="https://www.elastic.co/blog/elastic-observability-sre-incident-response">SREs</a> are interested in seeing everything at once and identifying where the root cause is. Managers want reports that tell them how good service performance is and if the service level agreement (SLA) is met. In this post, we’ll focus on the service perspective and provide an overview of calculating an SLA.</p>
<p><em>Since version 8.8, we have a built in functionality to calculate SLOs —</em> <a href="https://www.elastic.co/guide/en/observability/current/slo.html"><em>check out our guide</em></a><em>!</em></p>
<h2>Foundations of calculating an SLA</h2>
<p>There are many ways to calculate and measure an SLA. The most important part is the definition of the SLA, and as a consultant, I’ve seen many different ways. Some examples include:</p>
<ul>
<li>Count of HTTP 2xx must be above 98% of all HTTP status</li>
<li>Response time of successful HTTP 2xx requests must be below x milliseconds</li>
<li>Synthetic monitor must be up at least 99%</li>
<li>95% of all batch transactions from the billing service need to complete within 4 seconds</li>
</ul>
<p>Depending on the origin of the data, calculating the SLA can be easier or more difficult. For uptime (Synthetic Monitoring), we automatically provide SLA values and offer out-of-the-box alerts to simply define alert when availability below 98% for the last 1 hour.</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/observability-sla-calculations-transforms/blog-elastic-overview-monitor-details.png" alt="overview monitor details" /></p>
<p>I personally recommend using <a href="https://www.elastic.co/blog/new-synthetic-monitoring-observability">Elastic Synthetic Monitoring</a> whenever possible to monitor service performance. Running HTTP requests and verifying the answers from the service, or doing fully fledged browser monitors and clicking through the website as a real user does, ensures a better understanding of the health of your service.</p>
<p>Sometimes this is impossible because you want to calculate the uptime of a specific Windows Service that does not offer any TCP port or HTTP interaction. Here the caveat applies that just because the service is running, it does not necessarily imply that the service is working fine.</p>
<h2>Transforms to the rescue</h2>
<p>We have identified our important service. In our case, it is the Steam Client Helper. There are two ways to solve this.</p>
<h3>Lens formula</h3>
<p>You can use Lens and formula (for a deep dive into formulas, <a href="https://www.elastic.co/blog/how-tough-was-your-workout-take-a-closer-look-at-strava-data-through-kibana-lens">check out this blog</a>). Use the Search bar to filter down the data you want. Then use the formula option in Lens. We are dividing all counts of records with Running as a state and dividing it by the overall count of records. This is a nice solution when there is a need to calculate quickly and on the fly.</p>
<pre><code class="language-sql">count(kql='windows.service.state: &quot;Running&quot; ')/count()
</code></pre>
<p>Using the formula posted above as the bar chart's vertical axis calculates the uptime percentage. We use an annotation to mark why there is a dip and why this service was below the threshold. The annotation is set to reboot, which indicates a reboot happening, and thus, the service was down for a moment. Lastly, we add a reference line and set this to our defined threshold at 98%. This ensures that a quick look at the visualization allows our eyes to gauge if we are above or below the threshold.</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/observability-sla-calculations-transforms/blog-elastic-visualization.png" alt="visualization" /></p>
<h3>Transform</h3>
<p>What if I am not interested in just one service, but there are multiple services needed for your SLA? That is where Transforms can solve this problem. Furthermore, the second issue is that this data is only available inside the Lens. Therefore, we cannot create any alerts on this.</p>
<p>Go to Transforms and create a pivot transform.</p>
<ol>
<li>
<p>Add the following filter to narrow it to only services data sets: data_stream.dataset: &quot;windows.service&quot;. If you are interested in a specific service, you can always add it to the search bar if you want to know if a specific remote management service is up in your entire fleet!</p>
</li>
<li>
<p>Select data histogram(@timestamp) and set it to your chosen unit. By default, the Elastic Agent only collects service states every 60 seconds. I am going with 1 hour.</p>
</li>
<li>
<p>Select agent.name and windows.service.name as well.</p>
</li>
</ol>
<p><img src="https://www.elastic.co/observability-labs/assets/images/observability-sla-calculations-transforms/blog-elastic-transform-configuration.png" alt="transform configuration" /></p>
<ol start="4">
<li>Now we need to define an aggregation type. We will use a value_count of windows.service.state. That just counts how many records have this value.</li>
</ol>
<p><img src="https://www.elastic.co/observability-labs/assets/images/observability-sla-calculations-transforms/blog-elastic-aggregations.png" alt="aggregations" /></p>
<ol start="5">
<li>
<p>Rename the value_count to total_count.</p>
</li>
<li>
<p>Add value_count for windows.service.state a second time and use the pencil icon to edit it to terms, which aggregates for running.</p>
</li>
</ol>
<p><img src="https://www.elastic.co/observability-labs/assets/images/observability-sla-calculations-transforms/blog-elastic-aggregations-apply.png" alt="aggregations apply" /></p>
<ol start="7">
<li>
<p>This opens up a sub-aggregation. Once again, select value_count(windows.service.state) and rename it to values.</p>
</li>
<li>
<p>Now, the preview shows us the count of records with any states and the count of running.</p>
</li>
</ol>
<p><img src="https://www.elastic.co/observability-labs/assets/images/observability-sla-calculations-transforms/blog-elastic-transform-configuration-next.png" alt="transform configuration" /></p>
<ol start="9">
<li>
<p>Here comes the tricky part. We need to write some custom aggregations to calculate the percentage of uptime. Click on the copy icon next to the edit JSON config.</p>
</li>
<li>
<p>In a new tab, go to Dev Tools. Paste what you have in the clipboard.</p>
</li>
<li>
<p>Press the play button or use the keyboard shortcut ctrl+enter/cmd+enter and run it. This will create a preview of what the data looks like. It should give you the same information as in the table preview.</p>
</li>
<li>
<p>Now, we need to calculate the percentage of up, which means doing a bucket script where we divide running.values by total_count, just like we did in the Lens visualization. Suppose you name the columns differently or use more than a single value. In that case, you will need to adapt accordingly.</p>
</li>
</ol>
<pre><code class="language-json">&quot;availability&quot;: {
        &quot;bucket_script&quot;: {
          &quot;buckets_path&quot;: {
            &quot;up&quot;: &quot;running&gt;values&quot;,
            &quot;total&quot;: &quot;total_count&quot;
          },
          &quot;script&quot;: &quot;params.up/params.total&quot;
        }
      }
</code></pre>
<ol start="13">
<li>This is the entire transform for me:</li>
</ol>
<pre><code class="language-bash">POST _transform/_preview
{
  &quot;source&quot;: {
    &quot;index&quot;: [
      &quot;metrics-*&quot;
    ]
  },
  &quot;pivot&quot;: {
    &quot;group_by&quot;: {
      &quot;@timestamp&quot;: {
        &quot;date_histogram&quot;: {
          &quot;field&quot;: &quot;@timestamp&quot;,
          &quot;calendar_interval&quot;: &quot;1h&quot;
        }
      },
      &quot;agent.name&quot;: {
        &quot;terms&quot;: {
          &quot;field&quot;: &quot;agent.name&quot;
        }
      },
      &quot;windows.service.name&quot;: {
        &quot;terms&quot;: {
          &quot;field&quot;: &quot;windows.service.name&quot;
        }
      }
    },
    &quot;aggregations&quot;: {
      &quot;total_count&quot;: {
        &quot;value_count&quot;: {
          &quot;field&quot;: &quot;windows.service.state&quot;
        }
      },
      &quot;running&quot;: {
        &quot;filter&quot;: {
          &quot;term&quot;: {
            &quot;windows.service.state&quot;: &quot;Running&quot;
          }
        },
        &quot;aggs&quot;: {
          &quot;values&quot;: {
            &quot;value_count&quot;: {
              &quot;field&quot;: &quot;windows.service.state&quot;
            }
          }
        }
      },
      &quot;availability&quot;: {
        &quot;bucket_script&quot;: {
          &quot;buckets_path&quot;: {
            &quot;up&quot;: &quot;running&gt;values&quot;,
            &quot;total&quot;: &quot;total_count&quot;
          },
          &quot;script&quot;: &quot;params.up/params.total&quot;
        }
      }
    }
  }
}
</code></pre>
<ol start="14">
<li>The preview in Dev Tools should work and be complete. Otherwise, you must debug any errors. Most of the time, it is the bucket script and the path to the values. You might have called it up instead of running. This is what the preview looks like for me.</li>
</ol>
<pre><code class="language-json">{
  &quot;running&quot;: {
    &quot;values&quot;: 1
  },
  &quot;agent&quot;: {
    &quot;name&quot;: &quot;AnnalenasMac&quot;
  },
  &quot;@timestamp&quot;: &quot;2021-12-07T19:00:00.000Z&quot;,
  &quot;total_count&quot;: 1,
  &quot;availability&quot;: 1,
  &quot;windows&quot;: {
    &quot;service&quot;: {
      &quot;name&quot;: &quot;InstallService&quot;
    }
  }
},
</code></pre>
<ol start="15">
<li>Now we only paste the bucket script into the transform creation UI after selecting Edit JSON. It looks like this:</li>
</ol>
<p><img src="https://www.elastic.co/observability-labs/assets/images/observability-sla-calculations-transforms/blog-elastic-transform-configuration-pivot-configuration-object.png" alt="transform configuration pivot configuration object" /></p>
<ol start="16">
<li>Give your transform a name, set the destination index, and run it continuously. When selecting this, please also make sure not to use @timestamp. Instead, opt for event.ingested. <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/transform-checkpoints.html">Our documentation explains this in detail</a>.</li>
</ol>
<p><img src="https://www.elastic.co/observability-labs/assets/images/observability-sla-calculations-transforms/blog-elastic-transform-details.png" alt="transform details" /></p>
<ol start="17">
<li>Click next and create and start. This can take a bit, so don’t worry.</li>
</ol>
<p>To summarize, we have now created a pivot transform using a bucket script aggregation to calculate the running time of a service in percentage. There is a caveat because Elastic Agent, per default, only collects the every 60 seconds the services state. It can be that a service is up exactly when collected and down a few seconds later. If it is that important and no other monitoring possibilities, such as <a href="https://www.elastic.co/blog/what-can-elastic-synthetics-tell-us-about-kibana-dashboards">Elastic Synthetics</a> are possible, you might want to reduce the collection time on the Agent side to get the services state every 30 seconds, 45 seconds. Depending on how important your thresholds are, you can create multiple policies having different collection times. This ensures that a super important server might collect the services state every 10 seconds because you need as much granularity and insurance for the correctness of the metric. For normal workstations where you just want to know if your remote access solution is up the majority of the time, you might not mind having a single metric every 60 seconds.</p>
<p>After you have created the transform, one additional feature you get is that the data is stored in an index, similar to in Elasticsearch. When you just do the visualization, the metric is calculated for this visualization only and not available anywhere else. Since this is now data, you can create a threshold alert to your favorite connection (Slack, Teams, Service Now, Mail, and so <a href="https://www.elastic.co/guide/en/kibana/current/action-types.html">many more to choose from</a>).</p>
<h2>Visualizing the transformed data</h2>
<p>The transform created a data view called windows-service. The first thing we want to do is change the format of the availability field to a percentage. This automatically tells Lens that this needs to be formatted as a percentage field, so you don’t need to select it manually as well as do calculations. Furthermore, in Discover, instead of seeing 0.5 you see 50%. Isn’t that cool? This is also possible for durations, like event.duration if you have it as nanoseconds! No more calculations on the fly and thinking if you need to divide by 1,000 or 1,000,000.</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/observability-sla-calculations-transforms/blog-elastic-edit-field-availability.png" alt="edit field availability" /></p>
<p>We get this view by using a simple Lens visualization with a timestamp on the vertical axis with the minimum interval for 1 day and an average of availability. Don’t worry — the other data will be populated once the transformation finishes. We can add a reference line using the value 0.98 because our target is 98% uptime of the service.</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/observability-sla-calculations-transforms/blog-elastic-line.png" alt="line" /></p>
<h2>Summary</h2>
<p>This blog post covered the steps needed to calculate the SLA for a specific data set in Elastic Observability, as well as how to visualize it. Using this calculation method opens the door to a lot of interesting use cases. You can change the bucket script and start calculating the number of sales, and the average basket size. Interested in learning more about Elastic Synthetics? Read <a href="https://www.elastic.co/guide/en/observability/current/monitor-uptime-synthetics.html">our documentation</a> or check out our free <a href="https://www.elastic.co/training/synthetics-quick-start">Synthetic Monitoring Quick Start training</a>.</p>
]]></content:encoded>
            <category>observability-labs</category>
            <enclosure url="https://www.elastic.co/observability-labs/assets/images/observability-sla-calculations-transforms/illustration-analytics-report-1680x980.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Collecting OpenShift container logs using Red Hat’s OpenShift Logging Operator]]></title>
            <link>https://www.elastic.co/observability-labs/blog/openshift-container-logs-red-hat-logging-operator</link>
            <guid isPermaLink="false">openshift-container-logs-red-hat-logging-operator</guid>
            <pubDate>Tue, 16 Jan 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Learn how to optimize OpenShift logs collected with Red Hat OpenShift Logging Operator, as well as format and route them efficiently in Elasticsearch.]]></description>
            <content:encoded><![CDATA[<p>This blog explores a possible approach to collecting and formatting OpenShift Container Platform logs and audit logs with Red Hat OpenShift Logging Operator. We recommend using Elastic® Agent for the best possible experience! We will also show how to format the logs to Elastic Common Schema (<a href="https://www.elastic.co/guide/en/ecs/current/index.html">ECS</a>) for the best experience viewing, searching, and visualizing your logs. All examples in this blog are based on OpenShift 4.14.</p>
<h2>Why use OpenShift Logging Operator?</h2>
<p>A lot of enterprise customers use OpenShift as their orchestrating solution. The advantages of this approach are:</p>
<ul>
<li>
<p>It is developed and supported by Red Hat</p>
</li>
<li>
<p>It can automatically update the OpenShift cluster along with the Operating system to make sure that they are and remain compatible</p>
</li>
<li>
<p>It can speed up developing life cycles with features like source to image</p>
</li>
<li>
<p>It uses enhanced security</p>
</li>
</ul>
<p>In our consulting experience, this latter aspect poses challenges and frictions with OpenShift administrators when we try to install an Elastic Agent to collect the logs of the pods. Indeed, Elastic Agent requires the files of the host to be mounted in the pod, and it also needs to be run in privileged mode. (Read more about the permissions required by Elastic Agent in the <a href="https://www.elastic.co/guide/en/fleet/current/running-on-kubernetes-standalone.html#_red_hat_openshift_configuration">official Elasticsearch® Documentation</a>). While the solution we explore in this post requires similar privileges under the hood, it is managed by the OpenShift Logging Operator, which is developed and supported by Red Hat.</p>
<h2>Which logs are we going to collect?</h2>
<p>In OpenShift Container Platform, we distinguish <a href="https://docs.openshift.com/container-platform/4.14/logging/cluster-logging.html#logging-architecture-overview_cluster-logging">three broad categories of logs</a>: audit, application, and infrastructure logs:</p>
<ul>
<li>
<p><strong>Audit logs</strong> describe the list of activities that affected the system by users, administrators, and other components.</p>
</li>
<li>
<p><strong>Application logs</strong> are composed of the container logs of the pods running in non-reserved namespaces.</p>
</li>
<li>
<p><strong>Infrastructure logs</strong> are composed of container logs of the pods running in reserved namespaces like openshift*, kube*, and default along with journald messages from the nodes.</p>
</li>
</ul>
<p>In the following, we will consider only audit and application logs for the sake of simplicity. In this post, we will describe how to format audit and application Logs in the format expected by the Kubernetes integration to take the most out of Elastic Observability.</p>
<h2>Getting started</h2>
<p>To collect the logs from OpenShift, we must perform some preparation steps in Elasticsearch and OpenShift.</p>
<h3>Inside Elasticsearch</h3>
<p>We first <a href="https://www.elastic.co/guide/en/fleet/8.11/install-uninstall-integration-assets.html#install-integration-assets">install the Kubernetes integration assets</a>. We are mainly interested in the index templates and ingest pipelines for the logs-kubernetes.container_logs and logs-kubernetes.audit_logs.</p>
<p>To format the logs received from the ClusterLogForwarder in <a href="https://www.elastic.co/guide/en/ecs/current/index.html">ECS</a> format, we will define a pipeline to normalize the container logs. The field naming convention used by OpenShift is slightly different from that used by ECS. To get a list of exported fields from OpenShift, refer to <a href="https://docs.openshift.com/container-platform/4.14/logging/cluster-logging-exported-fields.html">Exported fields | Logging | OpenShift Container Platform 4.14</a>. To get a list of exported fields of the Kubernetes integration, you can refer to <a href="https://www.elastic.co/guide/en/beats/filebeat/current/exported-fields-kubernetes-processor.html">Kubernetes fields | Filebeat Reference [8.11] | Elastic</a> and <a href="https://www.elastic.co/guide/en/observability/current/logs-app-fields.html">Logs app fields | Elastic Observability [8.11]</a>. Further, specific fields like kubernetes.annotations must be normalized by replacing dots with underscores. This operation is usually done automatically by Elastic Agent.</p>
<pre><code class="language-bash">PUT _ingest/pipeline/openshift-2-ecs
{
  &quot;processors&quot;: [
    {
      &quot;rename&quot;: {
        &quot;field&quot;: &quot;kubernetes.pod_id&quot;,
        &quot;target_field&quot;: &quot;kubernetes.pod.uid&quot;,
        &quot;ignore_missing&quot;: true
      }
    },
    {
      &quot;rename&quot;: {
        &quot;field&quot;: &quot;kubernetes.pod_ip&quot;,
        &quot;target_field&quot;: &quot;kubernetes.pod.ip&quot;,
        &quot;ignore_missing&quot;: true
      }
    },
    {
      &quot;rename&quot;: {
        &quot;field&quot;: &quot;kubernetes.pod_name&quot;,
        &quot;target_field&quot;: &quot;kubernetes.pod.name&quot;,
        &quot;ignore_missing&quot;: true
      }
    },
    {
      &quot;rename&quot;: {
        &quot;field&quot;: &quot;kubernetes.namespace_name&quot;,
        &quot;target_field&quot;: &quot;kubernetes.namespace&quot;,
        &quot;ignore_missing&quot;: true
      }
    },
    {
      &quot;rename&quot;: {
        &quot;field&quot;: &quot;kubernetes.namespace_id&quot;,
        &quot;target_field&quot;: &quot;kubernetes.namespace_uid&quot;,
        &quot;ignore_missing&quot;: true
      }
    },
    {
      &quot;rename&quot;: {
        &quot;field&quot;: &quot;kubernetes.container_id&quot;,
        &quot;target_field&quot;: &quot;container.id&quot;,
        &quot;ignore_missing&quot;: true
      }
    },
    {
      &quot;dissect&quot;: {
        &quot;field&quot;: &quot;container.id&quot;,
        &quot;pattern&quot;: &quot;%{container.runtime}://%{container.id}&quot;,
        &quot;ignore_failure&quot;: true
      }
    },
    {
      &quot;rename&quot;: {
        &quot;field&quot;: &quot;kubernetes.container_image&quot;,
        &quot;target_field&quot;: &quot;container.image.name&quot;,
        &quot;ignore_missing&quot;: true
      }
    },
    {
      &quot;set&quot;: {
        &quot;field&quot;: &quot;kubernetes.container.image&quot;,
        &quot;copy_from&quot;: &quot;container.image.name&quot;,
        &quot;ignore_failure&quot;: true
      }
    },
    {
      &quot;set&quot;: {
        &quot;copy_from&quot;: &quot;kubernetes.container_name&quot;,
        &quot;field&quot;: &quot;container.name&quot;,
        &quot;ignore_failure&quot;: true
      }
    },
    {
      &quot;rename&quot;: {
        &quot;field&quot;: &quot;kubernetes.container_name&quot;,
        &quot;target_field&quot;: &quot;kubernetes.container.name&quot;,
        &quot;ignore_missing&quot;: true
      }
    },
    {
      &quot;set&quot;: {
        &quot;field&quot;: &quot;kubernetes.node.name&quot;,
        &quot;copy_from&quot;: &quot;hostname&quot;,
        &quot;ignore_failure&quot;: true
      }
    },
    {
      &quot;rename&quot;: {
        &quot;field&quot;: &quot;hostname&quot;,
        &quot;target_field&quot;: &quot;host.name&quot;,
        &quot;ignore_missing&quot;: true
      }
    },
    {
      &quot;rename&quot;: {
        &quot;field&quot;: &quot;level&quot;,
        &quot;target_field&quot;: &quot;log.level&quot;,
        &quot;ignore_missing&quot;: true
      }
    },
    {
      &quot;rename&quot;: {
        &quot;field&quot;: &quot;file&quot;,
        &quot;target_field&quot;: &quot;log.file.path&quot;,
        &quot;ignore_missing&quot;: true
      }
    },
    {
      &quot;set&quot;: {
        &quot;copy_from&quot;: &quot;openshift.cluster_id&quot;,
        &quot;field&quot;: &quot;orchestrator.cluster.name&quot;,
        &quot;ignore_failure&quot;: true
      }
    },
    {
      &quot;dissect&quot;: {
        &quot;field&quot;: &quot;kubernetes.pod_owner&quot;,
        &quot;pattern&quot;: &quot;%{_tmp.parent_type}/%{_tmp.parent_name}&quot;,
        &quot;ignore_missing&quot;: true
      }
    },
    {
      &quot;lowercase&quot;: {
        &quot;field&quot;: &quot;_tmp.parent_type&quot;,
        &quot;ignore_missing&quot;: true
      }
    },
    {
      &quot;set&quot;: {
        &quot;field&quot;: &quot;kubernetes.pod.{{_tmp.parent_type}}.name&quot;,
        &quot;value&quot;: &quot;{{_tmp.parent_name}}&quot;,
        &quot;if&quot;: &quot;ctx?._tmp?.parent_type != null&quot;,
        &quot;ignore_failure&quot;: true
      }
    },
    {
      &quot;remove&quot;: {
        &quot;field&quot;: [
          &quot;_tmp&quot;,
          &quot;kubernetes.pod_owner&quot;
          ],
          &quot;ignore_missing&quot;: true
      }
    },
    {
      &quot;script&quot;: {
        &quot;description&quot;: &quot;Normalize kubernetes annotations&quot;,
        &quot;if&quot;: &quot;ctx?.kubernetes?.annotations != null&quot;,
        &quot;source&quot;: &quot;&quot;&quot;
        def keys = new ArrayList(ctx.kubernetes.annotations.keySet());
        for(k in keys) {
          if (k.indexOf(&quot;.&quot;) &gt;= 0) {
            def sanitizedKey = k.replace(&quot;.&quot;, &quot;_&quot;);
            ctx.kubernetes.annotations[sanitizedKey] = ctx.kubernetes.annotations[k];
            ctx.kubernetes.annotations.remove(k);
          }
        }
        &quot;&quot;&quot;
      }
    },
    {
      &quot;script&quot;: {
        &quot;description&quot;: &quot;Normalize kubernetes namespace_labels&quot;,
        &quot;if&quot;: &quot;ctx?.kubernetes?.namespace_labels != null&quot;,
        &quot;source&quot;: &quot;&quot;&quot;
        def keys = new ArrayList(ctx.kubernetes.namespace_labels.keySet());
        for(k in keys) {
          if (k.indexOf(&quot;.&quot;) &gt;= 0) {
            def sanitizedKey = k.replace(&quot;.&quot;, &quot;_&quot;);
            ctx.kubernetes.namespace_labels[sanitizedKey] = ctx.kubernetes.namespace_labels[k];
            ctx.kubernetes.namespace_labels.remove(k);
          }
        }
        &quot;&quot;&quot;
      }
    },
    {
      &quot;script&quot;: {
        &quot;description&quot;: &quot;Normalize special Kubernetes Labels used in logs-kubernetes.container_logs to determine service.name and service.version&quot;,
        &quot;if&quot;: &quot;ctx?.kubernetes?.labels != null&quot;,
        &quot;source&quot;: &quot;&quot;&quot;
        def keys = new ArrayList(ctx.kubernetes.labels.keySet());
        for(k in keys) {
          if (k.startsWith(&quot;app_kubernetes_io_component_&quot;)) {
            def sanitizedKey = k.replace(&quot;app_kubernetes_io_component_&quot;, &quot;app_kubernetes_io_component/&quot;);
            ctx.kubernetes.labels[sanitizedKey] = ctx.kubernetes.labels[k];
            ctx.kubernetes.labels.remove(k);
          }
        }
        &quot;&quot;&quot;
      }
    }
    ]
}
</code></pre>
<p>Similarly, to handle the audit logs like the ones collected by Kubernetes, we define an ingest pipeline:</p>
<pre><code class="language-bash">PUT _ingest/pipeline/openshift-audit-2-ecs
{
  &quot;processors&quot;: [
    {
      &quot;script&quot;: {
        &quot;source&quot;: &quot;&quot;&quot;
        def audit = [:];
        def keyToRemove = [];
        for(k in ctx.keySet()) {
          if (k.indexOf('_') != 0 &amp;&amp; !['@timestamp', 'data_stream', 'openshift', 'event', 'hostname'].contains(k)) {
            audit[k] = ctx[k];
            keyToRemove.add(k);
          }
        }
        for(k in keyToRemove) {
          ctx.remove(k);
        }
        ctx.kubernetes=[&quot;audit&quot;:audit];
        &quot;&quot;&quot;,
        &quot;description&quot;: &quot;Move all the 'kubernetes.audit' fields under 'kubernetes.audit' object&quot;
      }
    },
    {
      &quot;set&quot;: {
        &quot;copy_from&quot;: &quot;openshift.cluster_id&quot;,
        &quot;field&quot;: &quot;orchestrator.cluster.name&quot;,
        &quot;ignore_failure&quot;: true
      }
    },
    {
      &quot;set&quot;: {
        &quot;field&quot;: &quot;kubernetes.node.name&quot;,
        &quot;copy_from&quot;: &quot;hostname&quot;,
        &quot;ignore_failure&quot;: true
      }
    },
    {
      &quot;rename&quot;: {
        &quot;field&quot;: &quot;hostname&quot;,
        &quot;target_field&quot;: &quot;host.name&quot;,
        &quot;ignore_missing&quot;: true
      }
    },
    {
      &quot;script&quot;: {
        &quot;if&quot;: &quot;ctx?.kubernetes?.audit?.annotations != null&quot;,
        &quot;source&quot;: &quot;&quot;&quot;
          def keys = new ArrayList(ctx.kubernetes.audit.annotations.keySet());
          for(k in keys) {
            if (k.indexOf(&quot;.&quot;) &gt;= 0) {
              def sanitizedKey = k.replace(&quot;.&quot;, &quot;_&quot;);
              ctx.kubernetes.audit.annotations[sanitizedKey] = ctx.kubernetes.audit.annotations[k];
              ctx.kubernetes.audit.annotations.remove(k);
            }
          }
          &quot;&quot;&quot;,
        &quot;description&quot;: &quot;Normalize kubernetes audit annotations field as expected by the Integration&quot;
      }
    }
  ]
}
</code></pre>
<p>The main objective of the pipeline is to mimic what Elastic Agent is doing: storing all audit fields under the kubernetes.audit object.</p>
<p>We are not going to use the conventional @custom pipeline approach because the fields must be normalized before invoking the logs-kubernetes.container_logs integration pipeline that uses fields like kubernetes.container.name and kubernetes.labels to determine the fields service.name and service.version. Read more about custom pipelines in <a href="https://www.elastic.co/guide/en/fleet/8.11/data-streams-pipeline-tutorial.html#data-streams-pipeline-one">Tutorial: Transform data with custom ingest pipelines | Fleet and Elastic Agent Guide [8.11]</a>.</p>
<p>The OpenShift Cluster Log Forwarder writes the data in the indices app-write and audit-write by default. It is possible to change this behavior, but it still tries to prepend the prefix “app” and the suffix “write”, so we opted to send the data to the default destination and use the reroute processor to send it to the right data streams. Read more about the Reroute Processor in our blog <a href="https://www.elastic.co/blog/simplifying-log-data-management-flexible-routing-elastic">Simplifying log data management: Harness the power of flexible routing with Elastic</a> and our documentation <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/reroute-processor.html">Reroute processor | Elasticsearch Guide [8.11] | Elastic</a>.</p>
<p>In this case, we want to redirect the container logs (app-write index) to logs-kubernetes.container_logs and the Audit logs (audit-write) to logs-kubernetes.audit_logs:</p>
<pre><code class="language-bash">PUT _ingest/pipeline/app-write-reroute-pipeline
{
  &quot;processors&quot;: [
    {
      &quot;pipeline&quot;: {
        &quot;name&quot;: &quot;openshift-2-ecs&quot;,
        &quot;description&quot;: &quot;Format the Openshift data in ECS&quot;
      }
    },
    {
      &quot;set&quot;: {
        &quot;field&quot;: &quot;event.dataset&quot;,
        &quot;value&quot;: &quot;kubernetes.container_logs&quot;
      }
    },
    {
      &quot;reroute&quot;: {
        &quot;destination&quot;: &quot;logs-kubernetes.container_logs-openshift&quot;
      }
    }
  ]
}



PUT _ingest/pipeline/audit-write-reroute-pipeline
{
  &quot;processors&quot;: [
    {
      &quot;pipeline&quot;: {
        &quot;name&quot;: &quot;openshift-audit-2-ecs&quot;,
        &quot;description&quot;: &quot;Format the Openshift data in ECS&quot;
      }
    },
    {
      &quot;set&quot;: {
        &quot;field&quot;: &quot;event.dataset&quot;,
        &quot;value&quot;: &quot;kubernetes.audit_logs&quot;
      }
    },
    {
      &quot;reroute&quot;: {
        &quot;destination&quot;: &quot;logs-kubernetes.audit_logs-openshift&quot;
      }
    }
  ]
}
</code></pre>
<p>Please note that given that app-write and audit-write do not follow the data stream naming convention, we are forced to add the destination field in the reroute processor. The reroute processor will also fill up the <a href="https://www.elastic.co/guide/en/ecs/8.11/ecs-data_stream.html">data_stream fields</a> for us. Note that this step is done automatically by Elastic Agent at source.</p>
<p>Further, we create the indices with the default pipelines we created to reroute the logs according to our needs.</p>
<pre><code class="language-bash">PUT app-write
{
  &quot;settings&quot;: {
      &quot;index.default_pipeline&quot;: &quot;app-write-reroute-pipeline&quot;
   }
}


PUT audit-write
{
  &quot;settings&quot;: {
    &quot;index.default_pipeline&quot;: &quot;audit-write-reroute-pipeline&quot;
  }
}
</code></pre>
<p>Basically, what we did can be summarized in this picture:</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/openshift-container-logs-red-hat-logging-operator/openshift-summary-blog.png" alt="openshift-summary-blog" /></p>
<p>Let us take the container logs. When the operator attempts to write in the app-write index, it will invoke the default_pipeline “app-write-reroute-pipeline” that formats the logs into ECS format and reroutes the logs to logs-kubernetes.container_logs-openshift datastreams. This calls the integration pipeline that invokes, if it exists, the logs-kubernetes.container_logs@custom pipeline. Finally, the logs-kubernetes_container_logs pipeline may reroute the logs to another data set and namespace utilizing the elastic.co/dataset and elastic.co/namespace annotations as described in the Kubernetes <a href="https://docs.elastic.co/integrations/kubernetes/container-logs#rerouting-based-on-pod-annotations">integration documentation</a>, which in turn can lead to the execution of an another integration pipeline.</p>
<h3>Create a user for sending the logs</h3>
<p>We are going to use basic authentication because, at the time of writing, it is the only supported authentication method for Elasticsearch in OpenShift logging. Thus, we need a role that allows the user to write and read the app-write, and audit-write logs (required by the OpenShift agent) and auto_configure access to logs-*-* to allow custom Kubernetes rerouting:</p>
<pre><code class="language-bash">PUT _security/role/YOURROLE
{
    &quot;cluster&quot;: [
      &quot;monitor&quot;
    ],
    &quot;indices&quot;: [
      {
        &quot;names&quot;: [
          &quot;logs-*-*&quot;
        ],
        &quot;privileges&quot;: [
          &quot;auto_configure&quot;,
          &quot;create_doc&quot;
        ],
        &quot;allow_restricted_indices&quot;: false
      },
      {
        &quot;names&quot;: [
          &quot;app-write&quot;,
          &quot;audit-write&quot;,
        ],
        &quot;privileges&quot;: [
          &quot;create_doc&quot;,
          &quot;read&quot;
        ],
        &quot;allow_restricted_indices&quot;: false
      }
    ],
    &quot;applications&quot;: [],
    &quot;run_as&quot;: [],
    &quot;metadata&quot;: {},
    &quot;transient_metadata&quot;: {
      &quot;enabled&quot;: true
    }

}



PUT _security/user/YOUR_USERNAME
{
  &quot;password&quot;: &quot;YOUR_PASSWORD&quot;,
  &quot;roles&quot;: [&quot;YOURROLE&quot;]
}
</code></pre>
<h3>On OpenShift</h3>
<p>On the OpenShift Cluster, we need to follow the <a href="https://docs.openshift.com/container-platform/4.14/logging/log_collection_forwarding/log-forwarding.html">official documentation</a> of Red Hat on how to install the Red Hat OpenShift Logging and configure Cluster Logging and the Cluster Log Forwarder.</p>
<p>We need to install the Red Hat OpenShift Logging Operator, which defines the ClusterLogging and ClusterLogForwarder Resources. Afterward, we can define the Cluster Logging resource:</p>
<pre><code class="language-yaml">apiVersion: logging.openshift.io/v1
kind: ClusterLogging
metadata:
  name: instance
  namespace: openshift-logging
spec:
  collection:
    logs:
      type: vector
      vector: {}
</code></pre>
<p>The Cluster Log Forwarder is the resource responsible for defining a daemon set that will forward the logs to the remote Elasticsearch. Before creating it, we need to create in the same namespace as the ClusterLogForwarder a secret containing the Elasticsearch credentials for the user we created previously in the namespace, where the ClusterLogForwarder will be deployed:</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Secret
metadata:
  name: elasticsearch-password
  namespace: openshift-logging
type: Opaque
stringData:
  username: YOUR_USERNAME
  password: YOUR_PASSWORD
</code></pre>
<p>Finally, we create the ClusterLogForwarder resource:</p>
<pre><code class="language-yaml">kind: ClusterLogForwarder
apiVersion: logging.openshift.io/v1
metadata:
  name: instance
  namespace: openshift-logging
spec:
  outputs:
    - name: remote-elasticsearch
      secret:
        name: elasticsearch-password
      type: elasticsearch
      url: &quot;https://YOUR_ELASTICSEARCH_URL:443&quot;
      elasticsearch:
        version: 8 # The default is version 6 with the _type field
  pipelines:
    - inputRefs:
        - application
        - audit
      name: enable-default-log-store
      outputRefs:
        - remote-elasticsearch
</code></pre>
<p>Note that we explicitly defined the version of Elasticsearch to be 8, otherwise the ClusterLogForwarder will send the _type field, which is not compatible with Elasticsearch 8 and that we collect only application and audit logs.</p>
<h2>Result</h2>
<p>Once the logs are collected and passed through all the pipelines, the result is very close to the out-of-the-box Kubernetes integration. There are important differences, like the lack of host and cloud metadata information that don’t seem to be collected (at least without an additional configuration). We can view the Kubernetes container logs in the logs explorer:</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/openshift-container-logs-red-hat-logging-operator/openshift-summary-blog-graphs.png" alt="openshift-summary-blog-graphs" /></p>
<p>In this post, we described how you can use the OpenShift Logging Operator to collect the logs of containers and audit logs. We still recommend leveraging Elastic Agent to collect all your logs. It is the best user experience you can get. No need to maintain or transform the logs yourself to ECS formatting. Additionally, Elastic Agent uses API keys as the authentication method and collects metadata like cloud information that allow you in the long run to do <a href="https://www.elastic.co/blog/optimize-cloud-resources-cost-apm-metadata-elastic-observability">more</a>.</p>
<p><a href="https://www.elastic.co/observability/log-monitoring">Learn more about log monitoring with the Elastic Stack</a>.</p>
<p><em>Have feedback on this blog?</em> <a href="https://github.com/herrBez/elastic-blog-openshift-logging/issues"><em>Share it here</em></a><em>.</em></p>
<p><em>The release and timing of any features or functionality described in this post remain at Elastic's sole discretion. Any features or functionality not currently available may not be delivered on time or at all.</em></p>
]]></content:encoded>
            <category>observability-labs</category>
            <enclosure url="https://www.elastic.co/observability-labs/assets/images/openshift-container-logs-red-hat-logging-operator/139687_-_Blog_Header_Banner_V1.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Optimizing cloud resources and cost with APM metadata in Elastic Observability]]></title>
            <link>https://www.elastic.co/observability-labs/blog/optimize-cloud-resources-apm-observability</link>
            <guid isPermaLink="false">optimize-cloud-resources-apm-observability</guid>
            <pubDate>Wed, 16 Aug 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[Optimize cloud costs with Elastic APM. Learn how to leverage cloud metadata, calculate pricing, and make smarter decisions for better performance.]]></description>
            <content:encoded><![CDATA[<p>Application performance monitoring (APM) is much more than capturing and tracking errors and stack traces. Today’s cloud-based businesses deploy applications across various regions and even cloud providers. So, harnessing the power of metadata provided by the Elastic APM agents becomes more critical. Leveraging the metadata, including crucial information like cloud region, provider, and machine type, allows us to track costs across the application stack. In this blog post, we look at how we can use cloud metadata to empower businesses to make smarter and cost-effective decisions, all while improving resource utilization and the user experience.</p>
<p>First, we need an example application that allows us to monitor infrastructure changes effectively. We use a Python Flask application with the Elastic Python APM agent. The application is a simple calculator taking the numbers as a REST request. We utilize Locust — a simple load-testing tool to evaluate performance under varying workloads.</p>
<p>The next step includes obtaining the pricing information associated with the cloud services. Every cloud provider is different. Most of them offer an option to retrieve pricing through an API. But today, we will focus on Google Cloud and will leverage their pricing calculator to retrieve relevant cost information.</p>
<h2>The calculator and Google Cloud pricing</h2>
<p>To perform a cost analysis, we need to know the cost of the machines in use. Google provides a billing <a href="https://cloud.google.com/billing/v1/how-tos/catalog-api">API</a> and <a href="https://cloud.google.com/billing/docs/reference/libraries#client-libraries-install-python">Client Library</a> to fetch the necessary data programmatically. In this blog, we are not covering the API approach. Instead, the <a href="https://cloud.google.com/products/calculator">Google Cloud Pricing Calculator</a> is enough. Select the machine type and region in the calculator and set the count 1 instance. It will then report the total estimated cost for this machine. Doing this for an e2-standard-4 machine type results in 107.7071784 US$ for a runtime of 730 hours.</p>
<p>Now, let’s go to our Kibana® where we will create a new index inside Dev Tools. Since we don’t want to analyze text, we will tell Elasticsearch® to treat every text as a keyword. The index name is cloud-billing. I might want to do the same for Azure and AWS, then I can append it to the same index.</p>
<pre><code class="language-bash">PUT cloud-billing
{
  &quot;mappings&quot;: {
    &quot;dynamic_templates&quot;: [
      {
        &quot;stringsaskeywords&quot;: {
          &quot;match&quot;: &quot;*&quot;,
          &quot;match_mapping_type&quot;: &quot;string&quot;,
          &quot;mapping&quot;: {
            &quot;type&quot;: &quot;keyword&quot;
          }
        }
      }
    ]
  }
}
</code></pre>
<p>Next up is crafting our billing document:</p>
<pre><code class="language-bash">POST cloud-billing/_doc/e2-standard-4_europe-west4
{
  &quot;machine&quot;: {
    &quot;enrichment&quot;: &quot;e2-standard-4_europe-west4&quot;
  },
  &quot;cloud&quot;: {
    &quot;machine&quot;: {
       &quot;type&quot;: &quot;e2-standard-4&quot;
    },
    &quot;region&quot;: &quot;europe-west4&quot;,
    &quot;provider&quot;: &quot;google&quot;
  },
  &quot;stats&quot;: {
    &quot;cpu&quot;: 4,
    &quot;memory&quot;: 8
  },
  &quot;price&quot;: {
    &quot;minute&quot;: 0.002459068,
    &quot;hour&quot;: 0.14754408,
    &quot;month&quot;: 107.7071784
  }
}
</code></pre>
<p>We create a document and set a custom ID. This ID matches the instance name and the region since the machines' costs may differ in each region. Automatic IDs could be problematic because I might want to update what a machine costs regularly. I could use a timestamped index for that and only ever use the latest document matching. But this way, I can update and don’t have to worry about it. I calculated the price down to minute and hour prices as well. The most important thing is the machine.enrichment field, which is the same as the ID. The same instance type can exist in multiple regions, but our enrichment processor is limited to match or range. We create a matching name that can explicitly match as in e2-standard-4_europe-west4. It’s up to you to decide whether you want the cloud provider in there and make it google_e2-standard-4_europ-west-4.</p>
<h2>Calculating the cost</h2>
<p>There are multiple ways of achieving this in the Elastic Stack. In this case, we will use an enrich policy, ingest pipeline, and transform.</p>
<p>The enrich policy is rather easy to setup:</p>
<pre><code class="language-bash">PUT _enrich/policy/cloud-billing
{
  &quot;match&quot;: {
    &quot;indices&quot;: &quot;cloud-billing&quot;,
    &quot;match_field&quot;: &quot;machine.enrichment&quot;,
    &quot;enrich_fields&quot;: [&quot;price.minute&quot;, &quot;price.hour&quot;, &quot;price.month&quot;]
  }
}

POST _enrich/policy/cloud-billing/_execute
</code></pre>
<p>Don’t forget to run the _execute at the end of it. This is necessary to make the internal indices used by the enrichment in the ingest pipeline. The ingest pipeline is rather minimalistic — it calls the enrichment and renames a field. This is where our machine.enrichment field comes in. One caveat around enrichment is that when you add new documents to the cloud-billing index, you need to rerun the _execute statement. The last bit calculates the total cost with the count of unique machines seen.</p>
<pre><code class="language-bash">PUT _ingest/pipeline/cloud-billing
{
  &quot;processors&quot;: [
    {
      &quot;set&quot;: {
        &quot;field&quot;: &quot;_temp.machine_type&quot;,
        &quot;value&quot;: &quot;{{cloud.machine.type}}_{{cloud.region}}&quot;
      }
    },
    {
      &quot;enrich&quot;: {
        &quot;policy_name&quot;: &quot;cloud-billing&quot;,
        &quot;field&quot;: &quot;_temp.machine_type&quot;,
        &quot;target_field&quot;: &quot;enrichment&quot;
      }
    },
    {
      &quot;rename&quot;: {
        &quot;field&quot;: &quot;enrichment.price&quot;,
        &quot;target_field&quot;: &quot;price&quot;
      }
    },
    {
      &quot;remove&quot;: {
        &quot;field&quot;: [
          &quot;_temp&quot;,
          &quot;enrichment&quot;
        ]
      }
    },
    {
      &quot;script&quot;: {
        &quot;source&quot;: &quot;ctx.total_price=ctx.count_machines*ctx.price.hour&quot;
      }
    }
  ]
}
</code></pre>
<p>Since this is all configured now, we are ready for our Transform. For this, we need a data view that matches the APM data_streams. This is traces-apm*, metrics-apm.*, logs-apm.*. For the Transform, go to the Transform UI in Kibana and configure it in the following way:</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/optimize-cloud-resources-apm-observability/elastic-blog-1-transform-configuration.png" alt="transform configuration" /></p>
<p>We are doing an hourly breakdown, therefore, I get a document per service, per hour, per machine type. The interesting bit is the aggregations. I want to see the average CPU usage and the 75,95,99 percentile, to view the CPU usage on an hourly basis. Allowing me to identify the CPU usage across an hour. At the bottom, give the transform a name and select an index cloud-costs and select the cloud-billing ingest pipeline.</p>
<p>Here is the entire transform as a JSON document:</p>
<pre><code class="language-bash">PUT _transform/cloud-billing
{
  &quot;source&quot;: {
    &quot;index&quot;: [
      &quot;traces-apm*&quot;,
      &quot;metrics-apm.*&quot;,
      &quot;logs-apm.*&quot;
    ],
    &quot;query&quot;: {
      &quot;bool&quot;: {
        &quot;filter&quot;: [
          {
            &quot;bool&quot;: {
              &quot;should&quot;: [
                {
                  &quot;exists&quot;: {
                    &quot;field&quot;: &quot;cloud.provider&quot;
                  }
                }
              ],
              &quot;minimum_should_match&quot;: 1
            }
          }
        ]
      }
    }
  },
  &quot;pivot&quot;: {
    &quot;group_by&quot;: {
      &quot;@timestamp&quot;: {
        &quot;date_histogram&quot;: {
          &quot;field&quot;: &quot;@timestamp&quot;,
          &quot;calendar_interval&quot;: &quot;1h&quot;
        }
      },
      &quot;cloud.provider&quot;: {
        &quot;terms&quot;: {
          &quot;field&quot;: &quot;cloud.provider&quot;
        }
      },
      &quot;cloud.region&quot;: {
        &quot;terms&quot;: {
          &quot;field&quot;: &quot;cloud.region&quot;
        }
      },
      &quot;cloud.machine.type&quot;: {
        &quot;terms&quot;: {
          &quot;field&quot;: &quot;cloud.machine.type&quot;
        }
      },
      &quot;service.name&quot;: {
        &quot;terms&quot;: {
          &quot;field&quot;: &quot;service.name&quot;
        }
      }
    },
    &quot;aggregations&quot;: {
      &quot;avg_cpu&quot;: {
        &quot;avg&quot;: {
          &quot;field&quot;: &quot;system.cpu.total.norm.pct&quot;
        }
      },
      &quot;percentiles_cpu&quot;: {
        &quot;percentiles&quot;: {
          &quot;field&quot;: &quot;system.cpu.total.norm.pct&quot;,
          &quot;percents&quot;: [
            75,
            95,
            99
          ]
        }
      },
      &quot;avg_transaction_duration&quot;: {
        &quot;avg&quot;: {
          &quot;field&quot;: &quot;transaction.duration.us&quot;
        }
      },
      &quot;percentiles_transaction_duration&quot;: {
        &quot;percentiles&quot;: {
          &quot;field&quot;: &quot;transaction.duration.us&quot;,
          &quot;percents&quot;: [
            75,
            95,
            99
          ]
        }
      },
      &quot;count_machines&quot;: {
        &quot;cardinality&quot;: {
          &quot;field&quot;: &quot;cloud.instance.id&quot;
        }
      }
    }
  },
  &quot;dest&quot;: {
    &quot;index&quot;: &quot;cloud-costs&quot;,
    &quot;pipeline&quot;: &quot;cloud-costs&quot;
  },
  &quot;sync&quot;: {
    &quot;time&quot;: {
      &quot;delay&quot;: &quot;120s&quot;,
      &quot;field&quot;: &quot;@timestamp&quot;
    }
  },
  &quot;settings&quot;: {
    &quot;max_page_search_size&quot;: 1000
  }
}
</code></pre>
<p>Once the transform is created and running, we need a Kibana Data View for the index: cloud-costs. For the transaction, use the custom formatter inside Kibana and set its format to “Duration” in “microseconds.”</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/optimize-cloud-resources-apm-observability/elastic-blog-2-cloud-costs.png" alt="cloud costs" /></p>
<p>With that, everything is arranged and ready to go.</p>
<h2>Observing infrastructure changes</h2>
<p>Below I created a dashboard that allows us to identify:</p>
<ul>
<li>How much costs a certain service creates</li>
<li>CPU usage</li>
<li>Memory usage</li>
<li>Transaction duration</li>
<li>Identify cost-saving potential</li>
</ul>
<p><img src="https://www.elastic.co/observability-labs/assets/images/optimize-cloud-resources-apm-observability/elastic-blog-3-graphs.png" alt="graphs" /></p>
<p>From left to right, we want to focus on the very first chart. We have the bars representing the CPU as average in green and 95th percentile in blue on top. It goes from 0 to 100% and is normalized, meaning that even with 8 CPU cores, it will still read 100% usage and not 800%. The line graph represents the transaction duration, the average being in red, and the 95th percentile in purple. Last, we have the orange area at the bottom, which is the average memory usage on that host.</p>
<p>We immediately realize that our calculator does not need a lot of memory. Hovering over the graph reveals 2.89% memory usage. The e2-standard-8 machine that we are using has 32 GB of memory. We occasionally spike to 100% CPU in the 95th percentile. When this happens, we see that the average transaction duration spikes to 2.5 milliseconds. However, every hour this machine costs us a rounded 30 cents. Using this information, we can now downsize to a better fit. The average CPU usage is around 11-13%, and the 95th percentile is not that far away.</p>
<p>Because we are using 8 CPUs, one could now say that 12.5% represents a full core, but that is just an assumption on a piece of paper. Nonetheless, we know there is a lot of headroom, and we can downscale quite a bit. In this case, I decided to go to 2 CPUs and 2 GB of RAM, known as e2-highcpu2. This should fit my calculator application better. We barely touched the RAM, 2.89% out of 32GB are roughly 1GB of use. After the change and reboot of the calculator machine, I started the same Locust test to identify my CPU usage and, more importantly, if my transactions get slower, and if so, by how much. Ultimately, I want to decide whether 1 millisecond more latency is worth 10 more cents per hour. I added the change as an annotation in Lens.</p>
<p>After letting it run for a bit, we can now identify the smaller hosts' impact. In this case, we can see that the average did not change. However, the 95th percentile — as in 95% of all transactions are below this value — did spike up. Again, it looks bad at first, but checking in, it went from ~1.5 milliseconds to ~2.10 milliseconds, a ~0.6 millisecond increase. Now, you can decide whether that 0.6 millisecond increase is worth paying ~180$ more per month or if the current latency is good enough.</p>
<h2>Conclusion</h2>
<p>Observability is more than just collecting logs, metrics, and traces. Linking user experience to cloud costs allows your business to identify areas where you can save money. Having the right tools at your disposal will help you generate those insights quickly. Making informed decisions about how to optimize your cloud cost and ultimately improve the user experience is the bottom-line goal.</p>
<p>The dashboard and data view can be found in my <a href="https://github.com/philippkahr/blogs/tree/main/apm-cost-optimisation">GitHub repository</a>. You can download the .ndjson file and import it using the Saved Objects inside Stack Management in Kibana.</p>
<h2>Caveats</h2>
<p>Pricing is only for base machines without any disk information, static public IP addresses, and any other additional cost, such as licenses for operating systems. Furthermore, it excludes spot pricing, discounts, or free credits. Additionally, data transfer costs between services are also not included. We only calculate it based on the minute rate of the service running — we are not checking billing intervals from Google Cloud. In our case, we would bill per minute, regardless of what Google Cloud has. Using the count for unique instance.ids work as intended. However, if a machine is only running for one minute, we calculate it based on the hourly rate. So, a machine running for one minute, will cost the same as running for 50 minutes — at least how we calculate it. The transform uses calendar hour intervals; therefore, it's 8 am-9 am, 9 am-10 am, and so on.</p>
<p><em>The release and timing of any features or functionality described in this post remain at Elastic's sole discretion. Any features or functionality not currently available may not be delivered on time or at all.</em></p>
]]></content:encoded>
            <category>observability-labs</category>
            <enclosure url="https://www.elastic.co/observability-labs/assets/images/optimize-cloud-resources-apm-observability/illustration-out-of-box-data-vis-1680x980.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Universal Profiling: Detecting CO2 and energy efficiency]]></title>
            <link>https://www.elastic.co/observability-labs/blog/universal-profiling-detecting-co2-energy-efficiency</link>
            <guid isPermaLink="false">universal-profiling-detecting-co2-energy-efficiency</guid>
            <pubDate>Mon, 05 Feb 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Universal Profiling introduces the possibility to capture environmental impact. In this post, we compare Python and Go implementations and showcase the substantial CO2 savings achieved through code optimization.]]></description>
            <content:encoded><![CDATA[<p>A while ago, we posted a <a href="https://www.elastic.co/blog/importing-chess-games-elasticsearch-universal-profiling">blog</a> that detailed how we imported over 4 billion chess games with speed using Python and optimized the code leveraging our Universal Profiling&lt;sup&gt;TM&lt;/sup&gt;. This was based on Elastic Stack running on version 8.9. We are now on <a href="https://www.elastic.co/blog/whats-new-elastic-8-12-0">8.12</a>, and it is time to do a second part that shows how easy it is to observe compiled languages and how Elastic®’s Universal Profiling can help you determine the benefit of a rewrite, both from a cost and environmental friendliness angle.</p>
<h2>Why efficiency matters — for you and the environment</h2>
<p>Data centers are estimated to consume ~3% of global electricity consumption, and their usage is expected to double by 2030.* The cost of a digital service is a close proxy to its computing efficiency, and thus, being more efficient is a win-win: less energy consumed, smaller bill.</p>
<p>In the same scenario, companies want the ability to scale to more users while spending less for each user and are effectively looking into methods of reducing their energy consumption.</p>
<p>In this spirit, <a href="https://www.elastic.co/observability/universal-profiling">Universal Profiling</a> comes equipped with data and visualizations to help determine where efficiency improvement efforts are worth the most.</p>
<p><a href="https://www.elastic.co/blog/continuous-profiling-efficient-cost-effective-applications">Energy efficiency</a> measures how much a digital service consumes to produce an output given an input. It can be measured in multiple ways, and we at Elastic Observability chose CO&lt;sub&gt;2&lt;/sub&gt; emissions and annualized CO&lt;sub&gt;2&lt;/sub&gt; emissions (more details on them later).</p>
<p>Let’s take the example of an e-commerce website: the energy efficiency of the “search inventory” process could be calculated as the average CPU time needed to serve a user request. Once the baseline for this value is determined, changes to the software delivering the search process may result in more or less CPU time consumed for the same feature, resulting in less or more efficient code.</p>
<h2>How to set up and configure wattage and CO2</h2>
<p>You can find a “Settings” button in the top-right corner of the Universal Profiling views. From there, you can customize the coefficient used to calculate CO&lt;sub&gt;2&lt;/sub&gt; emissions tied to profiling data.</p>
<p>The values set here will be used only when the profiles gathered from host agents are not already associated with publicly known data certified by cloud providers. For example, suppose you have a hybrid cloud deployment with a portion of your workload running on-premise and a portion running in GCP. In that case, the values set here will only be used to calculate the CO&lt;sub&gt;2&lt;/sub&gt; emissions for the on-premise machines; we already use all the coefficients as declared by GCP to calculate the emissions of those machines.</p>
<h2>Python vs. Go</h2>
<p>Our first <a href="https://www.elastic.co/blog/importing-chess-games-elasticsearch-universal-profiling">blog post</a> implemented a solution to read PGN chess games, a text representation in Python. It showed how Universal Profiler can be leveraged to identify slow functions and help you rewrite your code faster and more efficiently. At the end of it, we were happy with the Python version. It is still used today to grab the monthly updates from the <a href="https://database.lichess.org/">Lichess database</a> and ingest them into Elasticsearch®. I always wanted a reason to work more with Go, and we rewrote Python to Go. We leveraged goroutines and channels to send data through message passing. You can see more about it in our <a href="https://github.com/philippkahr/blogs/tree/main/universal-profiling">GitHub repository</a>.</p>
<p>Rewriting in Go also means switching from an interpreted language to a compiled one. As with everything in IT, this has benefits as well as disadvantages. One disadvantage is that we must ship debug symbols for the compiled binary. When we build the binary, we can use the symbtool program to ship the debug symbols. Without debug symbols, we see uninterpretable information as frames will be labeled with hexadecimal addresses in the flame graph rather than source code annotations.</p>
<p>First, make sure that your executable includes debug symbols. Go per default builds with debug symbols. You can check this by using file yourbinary. The important part is that it is not stripped.</p>
<pre><code class="language-bash">file lichess
lichess: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, Go BuildID=gufIkqA61WnCh8haeW-2/lfn3ne3U_y8MGoFD4AvT/QJEykzbacbYEmEQpXH6U/MqVbk-402n1k3B8yPB6I, with debug_info, not stripped
</code></pre>
<p>Now we need to push the symbols using symbtool. You must create an Elasticsearch API key as the authentication method. In the Universal Profiler UI in Kibana®, an <strong>Add Data</strong> button in the top right corner will tell you exactly what to do. The command is like this. The -e is the part where you pass through the path of your executable file. In our case, this is lichess as above.</p>
<pre><code class="language-bash">symbtool push-symbols executable -t &quot;ApiKey&quot; -u &quot;elasticsearch-url&quot; -e &quot;lichess&quot;
</code></pre>
<p>Now that debug symbols are available inside the cluster, we can run both implementations with the same file simultaneously and see what Universal Profiler can tell us about it.</p>
<h2>Identifying CO2 and energy efficiency savings</h2>
<p>Python is more frequently scheduled on the CPU. Thus, it runs more often on the hardware and contributes more to the machines’ resource usage.</p>
<p>We use the differential flame graph to identify and automatically calculate the difference in the following comparison. You need to filter on process.thread.name: “python3.11” in the baseline, and for the comparison, filter for lichess.</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/universal-profiling-detecting-co2-energy-efficiency/1-elastic-blog-uni-profiling.png" alt="1 - universal profiling" /></p>
<p>Looking at the impact of annualized CO&lt;sub&gt;2&lt;/sub&gt; emissions, we see a decrease from 65.32kg of CO&lt;sub&gt;2&lt;/sub&gt; from the Python solution to 16.78kg. That is a difference of 48.54kg CO&lt;sub&gt;2&lt;/sub&gt; savings over a year.</p>
<p>If we take a step back, we’ll want to figure out why Python produces many more emissions. In the flamegraph view, we filter down to just showing Python, and we can click on the first frame called python3.11. A little popup tells us that it caused 32.95kg of emissions. That is nearly 50% of all emissions caused by the runtime. Our program itself caused the other ~32kg of CO&lt;sub&gt;2&lt;/sub&gt;. We immediately reduced 32kg of annual emissions by cutting out the Python interpreter with Go.</p>
<p>We can lock that box using a right click and click <strong>Show more information</strong>.</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/universal-profiling-detecting-co2-energy-efficiency/2-elastic-blog-uni-profiling.png" alt="2 - universal profiling graphs blue-orange" /></p>
<p>The <strong>Show more information</strong> link displays detailed information about the frame, like sample count, total CPU, core seconds, and dollar costs. We won’t go into more detail in this blog.</p>
<p><img src="https://www.elastic.co/observability-labs/assets/images/universal-profiling-detecting-co2-energy-efficiency/3-elastic-blog-uni-profiling.png" alt="3 impact estimates" /></p>
<h2>Reduce your carbon footprint today with Universal Profiling</h2>
<p>This blog post demonstrates that rewriting your code base can reduce your carbon footprint immensely. Using Universal Profiler, you could do a quick PoC to showcase how much carbon resources can be spared.</p>
<p>Learn how you can <a href="https://www.elastic.co/guide/en/observability/current/profiling-get-started.html">get started</a> with Elastic Universal Profiling today.</p>
<blockquote>
<ul>
<li>Cluster for storing the data where three nodes, each 64GB RAM and 32 CPU cores, are running GCP on Elastic Cloud.</li>
<li>The machine for sending the data is a GCP e2-standard-32, thus 128GB RAM and 32 CPU cores with a 500GB balanced disk to read the games from.</li>
<li>The file used for the games is this <a href="https://database.lichess.org/standard/lichess_db_standard_rated_2023-12.pgn.zst">Lichess database</a> containing 96,909,211 games. The extracted file size is 211GB.</li>
</ul>
</blockquote>
<p><strong>Source:</strong></p>
<p>*<a href="https://media.ccc.de/v/camp2023-57070-energy_consumption_of_data_centers">https://media.ccc.de/v/camp2023-57070-energy_consumption_of_data_centers</a></p>
<p><em>The release and timing of any features or functionality described in this post remain at Elastic's sole discretion. Any features or functionality not currently available may not be delivered on time or at all.</em></p>
]]></content:encoded>
            <category>observability-labs</category>
            <enclosure url="https://www.elastic.co/observability-labs/assets/images/universal-profiling-detecting-co2-energy-efficiency/141935_-_Blog_header_image-_Op1_V1.jpg" length="0" type="image/jpg"/>
        </item>
    </channel>
</rss>