<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Stephanie Bergamo</title>
    <description>The latest articles on DEV Community by Stephanie Bergamo (@steph_baltus).</description>
    <link>https://dev.to/steph_baltus</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F57014%2F1978e8a8-6ed4-46af-9955-8707a87677fa.jpeg</url>
      <title>DEV Community: Stephanie Bergamo</title>
      <link>https://dev.to/steph_baltus</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/steph_baltus"/>
    <language>en</language>
    <item>
      <title>Go - Traces vs logs, and the four ways I got a trace tree wrong</title>
      <dc:creator>Stephanie Bergamo</dc:creator>
      <pubDate>Mon, 27 Jul 2026 16:35:00 +0000</pubDate>
      <link>https://dev.to/steph_baltus/go-traces-vs-logs-and-the-four-ways-i-got-a-trace-tree-wrong-1m07</link>
      <guid>https://dev.to/steph_baltus/go-traces-vs-logs-and-the-four-ways-i-got-a-trace-tree-wrong-1m07</guid>
      <description>&lt;p&gt;Metrics and logs were the easy part, and I wasn't ready for traces.&lt;br&gt;&lt;br&gt;
It took me a while to get it, first getting a grasp on the vocabulary, then putting it all together and managing to see a tree.  I won't blame myself too much, because a trace tree has several ways to fail &lt;em&gt;silently&lt;/em&gt;, and I hit them one after another on the same code.&lt;/p&gt;

&lt;p&gt;This is the post &lt;em&gt;I wish I'd read first&lt;/em&gt;. Not an OpenTelemetry tutorial: the handful of things that stood between me and a working &lt;code&gt;job &amp;gt; phase &amp;gt; task&lt;/code&gt; tree, in the order I tripped over them.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why traces, and not just more logs
&lt;/h2&gt;

&lt;p&gt;Logs and metrics answer questions &lt;strong&gt;you decided on in advance&lt;/strong&gt;. You chose what to log, which counter to bump. They're proactive: you predict what you'll want to know, then instrument for it.&lt;br&gt;&lt;br&gt;
When something you &lt;em&gt;didn't&lt;/em&gt; predict goes wrong, you add a log line, redeploy and wait.&lt;/p&gt;

&lt;p&gt;Traces flip that. You instrument the structure once (who called what, in what order, nested how) and then you can ask questions you never anticipated, after the fact, without touching the code. "Why was &lt;em&gt;this specific&lt;/em&gt; task re-run, and by which worker?" isn't a question you planned for. With a trace, the answer is already sitting in the data, waiting for you to visualize it.&lt;/p&gt;

&lt;p&gt;That's the reactive power, and it's why traces are worth the extra pain: they turn "I need to reproduce this with more logging" into "let me go look." The pain is just that all the ways to get it wrong are quiet.&lt;/p&gt;
&lt;h2&gt;
  
  
  Vocabulary, so the rest makes sense
&lt;/h2&gt;

&lt;p&gt;Five words, because the failures below are mostly about confusing them.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trace&lt;/strong&gt;: one tree. Everything sharing a &lt;code&gt;trace_id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Span&lt;/strong&gt;: one node, in a tree: a start, an end, some attributes, and a parent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Span event&lt;/strong&gt;:  a timestamped annotation &lt;em&gt;inside&lt;/em&gt; a span. &lt;code&gt;span.AddEvent("reaped")&lt;/code&gt;. It lives in the trace.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wide event&lt;/strong&gt;: a fat structured log line. Lives in the log system, not the trace. It &lt;em&gt;can&lt;/em&gt; carry &lt;code&gt;trace_id&lt;/code&gt; and &lt;code&gt;span_id&lt;/code&gt; as attributes, which makes it correlatable with a span, but it is not attached to one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context&lt;/strong&gt;: Go's &lt;code&gt;context.Context&lt;/code&gt;, carrying the current span. This, and only this, links a child to its parent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point is the whole article. &lt;strong&gt;Parent-child comes from the context you pass to &lt;code&gt;Start&lt;/code&gt;. Nothing else.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Not events, not naming, not attributes. If the tree is wrong, the context is wrong. &lt;/p&gt;

&lt;p&gt;The four traps below aren't random, they follow a span's life cycle: it has to be born (real provider), it has to travel across a process boundary, it has to attach to the right parent, and &lt;strong&gt;it has to die so it gets exported&lt;/strong&gt;. I hit them in exactly that order, and that order is also how you debug them.&lt;/p&gt;
&lt;h2&gt;
  
  
  Trap 1 - birth: the no-op tracer
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;otel.Tracer("x")&lt;/code&gt; doesn't fail if you call it before the provider is set up. It returns a &lt;strong&gt;no-op tracer&lt;/strong&gt;: it produces spans that are never &lt;em&gt;recorded&lt;/em&gt;, never &lt;em&gt;exported&lt;/em&gt;, and useless as parents. No panic, no warning...&lt;strong&gt;just a silent dud&lt;/strong&gt;. So if you grab your tracer and start a long-lived span &lt;em&gt;before&lt;/em&gt; calling your OTel setup, that span is a ghost, and everything descending from it starts a fresh root instead.&lt;/p&gt;

&lt;p&gt;Fix: set up the &lt;strong&gt;provider first&lt;/strong&gt;, before any &lt;code&gt;Start&lt;/code&gt;. (Obvious in hindsight; invisible while it's happening, which is kind of the theme here.)&lt;/p&gt;
&lt;h2&gt;
  
  
  Trap 2 - transport: the silent propagator
&lt;/h2&gt;

&lt;p&gt;Between two processes, &lt;code&gt;context.Context&lt;/code&gt; doesn't travel (it's a local in-memory thing). So you serialise the trace context into whatever the transport carries, and rebuild it on the other side: &lt;code&gt;Inject&lt;/code&gt; and &lt;code&gt;Extract&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What actually crosses the wire is tiny: a single &lt;code&gt;traceparent&lt;/code&gt; string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
|  |                                |                |
|  trace_id (16 bytes)              span_id (8 bytes)|
version                                              flags
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole thing. Version, trace, span, flags.&lt;/p&gt;

&lt;p&gt;The trap: &lt;code&gt;Inject&lt;/code&gt; needs a propagator to be registered, and if you forget, it writes &lt;strong&gt;nothing&lt;/strong&gt; into the carrier. No warning, no error. &lt;code&gt;Extract&lt;/code&gt; on the other side finds nothing and starts a fresh root.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;otel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetTextMapPropagator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;propagation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TraceContext&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One line, once, per binary. Miss it and the wire carries an empty map, silently.&lt;/p&gt;

&lt;p&gt;(If you read my &lt;a href="https://dev.to/en/2026/07/24/go-sending-traces-logs-and-metrics-to-the-victoria-stack/"&gt;Victoria* setup post&lt;/a&gt;: same failure mode as the swallowed 404s. The OTel SDK's whole philosophy is that telemetry must never break your app — which is right, and which means it will never tell you it's broken.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 3 - parent: passing the wrong context
&lt;/h2&gt;

&lt;p&gt;This one's subtle because the tree looks &lt;em&gt;almost&lt;/em&gt; right.&lt;/p&gt;

&lt;p&gt;I had a &lt;code&gt;job&lt;/code&gt; span, a &lt;code&gt;phase&lt;/code&gt; span under it, and I was starting each task from the &lt;strong&gt;job&lt;/strong&gt; context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;tracer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JobCtx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"task"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c"&gt;// parent = job, skips the phase&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One trace, correct-ish parent, no error. But my phase span had zero children — every task hung directly off the job, and the phase sat there empty and pointless. The tree was valid and wrong at the same time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;tracer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PhaseCtx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"task"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c"&gt;// parent = phase&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One word. The lesson: it's not enough for the context to be &lt;em&gt;a&lt;/em&gt; real span context — it has to be the &lt;em&gt;right&lt;/em&gt; one. The link is only ever as correct as the context you hand to &lt;code&gt;Start&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;(Propagating context across the RPC boundary to the workers is a whole other story, with its own good traps — that one's going in a longer piece.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 4 - death: the span that never ends
&lt;/h2&gt;

&lt;p&gt;A span is only exported when you call &lt;code&gt;End()&lt;/code&gt; on it. A span you never end is never sent — so if it's a &lt;em&gt;parent&lt;/em&gt;, its children point at a node that isn't there. A ghost parent, and a tree that looks broken in the UI even though every link is correct.&lt;/p&gt;

&lt;p&gt;The task spans were easy: they end when the task completes or when the reaper kills them. But the long-lived ones — &lt;code&gt;job&lt;/code&gt;, &lt;code&gt;phase&lt;/code&gt; — had no obvious end, so I'd left them open. They never shipped.&lt;/p&gt;

&lt;p&gt;Fix: end them deliberately, when the work they represent is actually over.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Coordinator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Finish&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;phaseSpan&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;End&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;jobSpan&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;End&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c"&gt;// called once Done() is true, before shutting the providers down&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And mind the order: end your spans, &lt;em&gt;then&lt;/em&gt; flush the providers. The batcher can only ship what's been ended.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus trap: there's no tree view where you'd expect one
&lt;/h2&gt;

&lt;p&gt;Once all four were fixed, my data was correct: one trace, three levels, retried tasks as siblings, and yet no tree in vmui 🤯. &lt;br&gt;
I was surprised, but it's actually expected. VictoriaTraces' built-in UI (&lt;code&gt;/select/vmui&lt;/code&gt;), has only Group / Table / JSON modes. It shows spans as flat rows. It does not draw waterfalls.&lt;/p&gt;

&lt;p&gt;VictoriaTraces stores and queries traces but delegates &lt;em&gt;visualisation&lt;/em&gt; to Jaeger or Grafana, via its Jaeger-compatible API. Point a Grafana Jaeger datasource at &lt;code&gt;http://&amp;lt;host&amp;gt;:10428/select/jaeger/&lt;/code&gt;, paste the trace_id, and there's your waterfall. Really, do it, after going through all the trouble, you want a proper visualization. &lt;/p&gt;

&lt;p&gt;You can sanity-check the tree without any of that, though: in the Table view, show &lt;code&gt;trace_id&lt;/code&gt; and &lt;code&gt;parent_span_id&lt;/code&gt;. One trace_id, every task pointing at the phase's span_id. That's the tree, in text.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern
&lt;/h2&gt;

&lt;p&gt;Every one of these failed the same way: &lt;strong&gt;no error, wrong tree.&lt;/strong&gt; No-op tracer, empty propagator, wrong parent context, unended parent: four different bugs, one identical symptom. Telemetry is built to never crash your app, which is great actually, but as a consequence it will never tell you it's misconfigured. The only feedback loop is looking at the data and asking "is this shaped the way I meant?"&lt;/p&gt;

&lt;p&gt;That's the real takeaway, more useful than any single fix: when a trace looks wrong, don't debug the tool. Check, in order: did the provider exist yet, is the propagator set, which context did I pass, did the parent end. It's almost always one of those four.&lt;/p&gt;

</description>
      <category>go</category>
      <category>opentelemetry</category>
      <category>observability</category>
      <category>traces</category>
    </item>
    <item>
      <title>Go - Sending traces, logs and metrics to the Victoria* stack</title>
      <dc:creator>Stephanie Bergamo</dc:creator>
      <pubDate>Mon, 27 Jul 2026 16:34:38 +0000</pubDate>
      <link>https://dev.to/steph_baltus/go-sending-traces-logs-and-metrics-to-the-victoria-stack-3le4</link>
      <guid>https://dev.to/steph_baltus/go-sending-traces-logs-and-metrics-to-the-victoria-stack-3le4</guid>
      <description>&lt;p&gt;The &lt;a href="https://docs.victoriametrics.com/" rel="noopener noreferrer"&gt;VictoriaMetrics&lt;/a&gt; folks now have a component for each OTel signal: VictoriaMetrics for metrics, VictoriaLogs for logs, and VictoriaTraces for traces. Nice. No collector needed, the Go SDK can talk to all three directly over OTLP/HTTP.&lt;/p&gt;

&lt;p&gt;The catch: each one listens on its own port, with its own path, and the paths are &lt;em&gt;almost&lt;/em&gt; consistent. Almost.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, the dependency dance
&lt;/h2&gt;

&lt;p&gt;The OTel Go SDK is split across a lot of modules, and each signal needs its own exporter. I ran &lt;code&gt;go get&lt;/code&gt; about fifteen times before everything compiled. Here's the deduplicated version, per component; grab only what you need.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Common to everything:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go get &lt;span class="se"&gt;\&lt;/span&gt;
  go.opentelemetry.io/otel &lt;span class="se"&gt;\&lt;/span&gt;
  go.opentelemetry.io/otel/sdk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Traces:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go get &lt;span class="se"&gt;\&lt;/span&gt;
  go.opentelemetry.io/otel/trace &lt;span class="se"&gt;\&lt;/span&gt;
  go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Metrics:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go get &lt;span class="se"&gt;\&lt;/span&gt;
  go.opentelemetry.io/otel/sdk/metric &lt;span class="se"&gt;\&lt;/span&gt;
  go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Logs:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go get &lt;span class="se"&gt;\&lt;/span&gt;
  go.opentelemetry.io/otel/sdk/log &lt;span class="se"&gt;\&lt;/span&gt;
  go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Logs, the logrus way:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go get &lt;span class="se"&gt;\&lt;/span&gt;
  github.com/sirupsen/logrus &lt;span class="se"&gt;\&lt;/span&gt;
  go.opentelemetry.io/contrib/bridges/otellogrus
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sdk/metric/metricdata&lt;/code&gt; comes with &lt;code&gt;sdk/metric&lt;/code&gt;: no separate &lt;code&gt;go get&lt;/code&gt;. Same for &lt;code&gt;semconv&lt;/code&gt;, which ships with &lt;code&gt;otel&lt;/code&gt; itself; you just import the version you want, e.g. &lt;code&gt;go.opentelemetry.io/otel/semconv/v1.41.0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Watch the spelling on &lt;code&gt;otlptracehttp&lt;/code&gt;. I typed &lt;code&gt;otltracehttp&lt;/code&gt; first and spent a minute being confused. The pattern is &lt;code&gt;otlp&lt;/code&gt; + signal + protocol, consistently, across all three.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The three endpoints
&lt;/h2&gt;

&lt;p&gt;Each signal goes to its own component, on its own port, with its own path:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Metrics&lt;/strong&gt; → VictoriaMetrics, port &lt;code&gt;8428&lt;/code&gt;
&lt;code&gt;/opentelemetry/v1/metrics&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logs&lt;/strong&gt; → VictoriaLogs, port &lt;code&gt;9428&lt;/code&gt;
&lt;code&gt;/insert/opentelemetry/v1/logs&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traces&lt;/strong&gt; → VictoriaTraces, port &lt;code&gt;10428&lt;/code&gt;
&lt;code&gt;/insert/opentelemetry/v1/traces&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Spot the odd one out: &lt;strong&gt;metrics has no &lt;code&gt;/insert&lt;/code&gt; prefix&lt;/strong&gt;. Logs and traces do. The docs are clear about it, but it's the kind of thing you'll copy-paste wrong at least once (and see the next section for why you won't find out immediately).&lt;/p&gt;

&lt;p&gt;Note that this is a local discovery setup, with everything on one box behind raw ports. In real life you'd put a reverse proxy in front and end up with &lt;code&gt;metrics.example.org&lt;/code&gt;, &lt;code&gt;logs.example.org&lt;/code&gt;, &lt;code&gt;traces.example.org&lt;/code&gt;, and save the whole which port for what struggle. &lt;/p&gt;

&lt;h2&gt;
  
  
  The hidden trap: the SDK swallows your 404s
&lt;/h2&gt;

&lt;p&gt;If you get the path wrong, nothing happens. No error, no panic, no angry log line. The exporter POSTs, gets a 404, and quietly moves on. Your app runs perfectly and your dashboards stay empty.&lt;/p&gt;

&lt;p&gt;This is by design (telemetry that crashes your app is worse than no telemetry) but it makes debugging the setup phase miserable. So before anything else, wire up the error handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;otel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetErrorHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;otel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrorHandlerFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"otel error: %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you get told. Keep it while setting things up, and honestly, just keep it.&lt;/p&gt;

&lt;h2&gt;
  
  
  WithEndpoint or WithEndpointURL?
&lt;/h2&gt;

&lt;p&gt;Both exist and they don't take the same thing, which is a fun five minutes.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;WithEndpoint&lt;/code&gt; takes &lt;code&gt;host:port&lt;/code&gt; &lt;strong&gt;without a scheme&lt;/strong&gt;, and needs friends:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;otlptracehttp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithEndpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"thanos.local:10428"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;otlptracehttp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithURLPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/insert/opentelemetry/v1/traces"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;otlptracehttp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithInsecure&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;   &lt;span class="c"&gt;// because no scheme, you say it here&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pass &lt;code&gt;http://thanos.local:10428&lt;/code&gt; to &lt;code&gt;WithEndpoint&lt;/code&gt; and it breaks — the scheme&lt;br&gt;
isn't part of the value, it's what &lt;code&gt;WithInsecure()&lt;/code&gt; is for.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;WithEndpointURL&lt;/code&gt; takes the whole thing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;otlptracehttp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithEndpointURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http://thanos.local:10428/insert/opentelemetry/v1/traces"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One option instead of three, and &lt;code&gt;http://&lt;/code&gt; already says "insecure", so no&lt;br&gt;
&lt;code&gt;WithInsecure()&lt;/code&gt; needed. Use this one.&lt;/p&gt;

&lt;p&gt;Both are available on all three exporters (&lt;code&gt;otlptracehttp&lt;/code&gt;, &lt;code&gt;otlploghttp&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;otlpmetrichttp&lt;/code&gt;), with the same behaviour.&lt;/p&gt;
&lt;h2&gt;
  
  
  Wiring it up
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;otelHost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"thanos.local"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Metrics:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;metricExporter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;otlpmetrichttp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;otlpmetrichttp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithEndpointURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http://%s:8428/opentelemetry/v1/metrics"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;otelHost&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="n"&gt;otlpmetrichttp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithTemporalitySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="n"&gt;sdkmetric&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InstrumentKind&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;metricdata&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Temporality&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;metricdata&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeltaTemporality&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;WithTemporalitySelector&lt;/code&gt; matters. By default the SDK sends cumulative temporality; delta is what you want here, otherwise your counters get double-counted on restart.&lt;/p&gt;

&lt;p&gt;Logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;exporter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;otlploghttp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;otlploghttp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithEndpointURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http://%s:9428/insert/opentelemetry/v1/logs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;otelHost&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Traces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;otlptracehttp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;otlptracehttp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithEndpointURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http://%s:10428/insert/opentelemetry/v1/traces"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;otelHost&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Share the resource
&lt;/h2&gt;

&lt;p&gt;Build the resource once and hand it to all three providers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;newResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Resource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Default&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewWithAttributes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;semconv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SchemaURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;semconv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServiceName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;semconv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServiceVersion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pro tip: if you build with &lt;code&gt;ldflags&lt;/code&gt;, wire it to the same variable you already inject.&lt;/p&gt;

&lt;p&gt;If you run several instances of the same service, add &lt;code&gt;semconv.ServiceInstanceID(...)&lt;/code&gt; while you're here. Costs one line now, saves a refactor later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: logrus straight into VictoriaLogs
&lt;/h2&gt;

&lt;p&gt;If you're on logrus, the contrib bridge means zero changes to your existing log calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;hook&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;otellogrus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewHook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;otellogrus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithLoggerProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loggerProvider&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;logrus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddHook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hook&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every &lt;code&gt;logrus.WithField(...).Info(...)&lt;/code&gt; now lands in VictoriaLogs as a structured record. Nothing else to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't forget to shut down
&lt;/h2&gt;

&lt;p&gt;All three providers buffer. Batch processors for logs and traces, a periodic reader for metrics. If your process exits without flushing, you lose whatever was in flight (which, for short-lived processes, can be everything). &lt;/p&gt;

&lt;p&gt;Collect the shutdown funcs and call them all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Observers&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;shutdowns&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Observers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Shutdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;errs&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shutdowns&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;errs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;errs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;errs&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Call site:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;obs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Shutdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;errors.Join&lt;/code&gt; rather than an early return, so one failing provider doesn't stop the other two from flushing.&lt;/p&gt;

&lt;p&gt;Two things to watch: give &lt;code&gt;Shutdown&lt;/code&gt; a fresh context with its own timeout, because, you know, if you pass the one that just got cancelled, the final flush dies before it leaves. And &lt;code&gt;os.Exit()&lt;/code&gt; doesn't run deferred functions, so if your code exits that way, your telemetry never ships.&lt;/p&gt;




&lt;p&gt;The Victoria* docs are at &lt;a href="https://docs.victoriametrics.com/" rel="noopener noreferrer"&gt;docs.victoriametrics.com&lt;/a&gt;. &lt;br&gt;
Each component has its own OpenTelemetry setup page, which is where the endpoint paths above come from.&lt;/p&gt;

</description>
      <category>go</category>
      <category>opentelemetry</category>
      <category>observability</category>
      <category>victoriametrics</category>
    </item>
    <item>
      <title>Building a B-tree in Go: The gap between theory and implementation</title>
      <dc:creator>Stephanie Bergamo</dc:creator>
      <pubDate>Wed, 14 Jan 2026 10:33:08 +0000</pubDate>
      <link>https://dev.to/steph_baltus/building-a-b-tree-in-go-the-gap-between-theory-and-implementation-55h0</link>
      <guid>https://dev.to/steph_baltus/building-a-b-tree-in-go-the-gap-between-theory-and-implementation-55h0</guid>
      <description>&lt;p&gt;I've been obsessed with database internals lately 🤓. It started with &lt;a href="https://www.databass.dev/" rel="noopener noreferrer"&gt;Alex Petrov's "Database Internals"&lt;/a&gt;: one of those books where every page makes you think "wait, THAT'S how it works?!" &lt;/p&gt;

&lt;p&gt;But there's a gap between understanding the theory and really &lt;em&gt;getting&lt;/em&gt; it. I could explain what a B-tree does to a 6yo, draw the diagrams and the likes. But could I actually build one? That itch needed scratching.&lt;/p&gt;

&lt;p&gt;So I dedicated a bit of my time off coding a B-tree from scratch in Go. About 15 hours total, spread across a few days between coffee, sport, rest and debugging sessions. I definitely didn't need one for production (PostgreSQL does this infinitely better), but I wanted to really understand what happens when I type &lt;code&gt;CREATE INDEX&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And honestly? It was one of the most fun coding challenges I've done in a while.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's a B-tree, anyway?
&lt;/h2&gt;

&lt;p&gt;If you've used a database, you've used a B-tree: they're the data structure behind most database indexes. The key insight: &lt;strong&gt;unlike binary trees where each node has 2 children, B-tree nodes can have many children&lt;/strong&gt; (determined by the "order"). This makes them perfect for disk storage where reading a block is expensive.&lt;/p&gt;

&lt;p&gt;I won't explain B-trees from scratch here: Alex Petrov's book does that brilliantly. If you're not familiar with the basics, start with chapter 2 of Database Internals. This article assumes you know what a B-tree is and focuses on what I learned building one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Implementation: Easy, Interesting, and "Wait, What?"
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Easy Part: Search
&lt;/h3&gt;

&lt;p&gt;Binary search within nodes, recurse to children. With good tests, this was maybe 2 hours including edge cases. The satisfying kind of easy where you know exactly what to do.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Interesting Part: Split
&lt;/h3&gt;

&lt;p&gt;Understanding &lt;em&gt;why&lt;/em&gt; nodes split (keep tree balanced) vs &lt;em&gt;how&lt;/em&gt; to split them (median, two halves, promote) took some head-scratching. Drawing it out on paper helped.&lt;/p&gt;

&lt;p&gt;The surprise: root splits are special. You create a brand new root with just the promoted key. The tree grows taller, not wider.&lt;/p&gt;

&lt;h3&gt;
  
  
  The "Wait, What?" Part: Cascade
&lt;/h3&gt;

&lt;p&gt;Oh wow. This is where I learned the difference between "I understand B-trees" and "I can build a B-tree."&lt;/p&gt;

&lt;p&gt;When a split cascades up, you're not just inserting a key: you're managing a whole graph of parent-child relationships. Split an internal node? Better redistribute its children too. And update every child's parent pointer. Miss one? Silent corruption. The debugging session was ...&lt;em&gt;fun&lt;/em&gt;...&lt;/p&gt;

&lt;p&gt;The full implementation is on GitLab, but the key insight was: when you split an internal node, you're not just moving one or two keys: you're &lt;strong&gt;rewiring an entire graph&lt;/strong&gt;. Get one pointer wrong and your tree &lt;strong&gt;silently&lt;/strong&gt; corrupts. Tests caught this. A lot. And when they didn't and I had to discover that by myself, I wrote new tests to confirm and made sure I fixed the bug.  &lt;/p&gt;

&lt;h2&gt;
  
  
  What Building It Taught Me (That Reading Didn't)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  B-tree vs B+tree: Not Just Academic
&lt;/h3&gt;

&lt;p&gt;I knew the theory: B-trees store data everywhere, B+trees only in leaves. But implementing it made me &lt;em&gt;feel&lt;/em&gt; the difference.&lt;/p&gt;

&lt;p&gt;In my B-tree, search can stop at &lt;em&gt;any&lt;/em&gt; level when it finds the key. Elegant! But range scans would be a nightmare (you'd have to traverse the entire tree structure). B+trees sacrifice that early-stop elegance for better range queries (linked leaves). &lt;/p&gt;

&lt;p&gt;Now when I see PostgreSQL using B+trees for indexes, I get &lt;em&gt;why&lt;/em&gt;, not just &lt;em&gt;what&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sequential inserts are... weird 🤔
&lt;/h3&gt;

&lt;p&gt;Testing with &lt;code&gt;INSERT 1, 2, 3, 4...&lt;/code&gt; created a hilariously unbalanced tree: 7 levels deep with just 50 keys, all growing to the right.&lt;/p&gt;

&lt;p&gt;Random inserts? Beautiful 3-level balanced tree.&lt;/p&gt;

&lt;p&gt;This is why databases have bulk loading strategies and why &lt;code&gt;REINDEX&lt;/code&gt; exists. Seeing my skewed tree made me appreciate those optimizations viscerally.&lt;/p&gt;

&lt;h3&gt;
  
  
  TDD Isn't Optional (or at least not for me anymore)
&lt;/h3&gt;

&lt;p&gt;I've been building software with TDD for so long now that coding without tests feels... naked? Uncomfortable? Like I'm missing something fundamental.&lt;/p&gt;

&lt;p&gt;Could I have built this B-tree without tests? Sure. Would I have shipped a dozen subtle bugs? Absolutely.&lt;/p&gt;

&lt;p&gt;Writing tests first isn't about following best practices anymore: it's how I think through implementation. The test tells me what "done" looks like before I start coding.&lt;/p&gt;

&lt;p&gt;For this project, that discipline caught:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Duplicate key insertion (oops, forgot to check)&lt;/li&gt;
&lt;li&gt;Index out of bounds on edge cases (classic off-by-one)&lt;/li&gt;
&lt;li&gt;Children count mismatch after splits (n keys → n+1 children, remembered that in the test)&lt;/li&gt;
&lt;li&gt;Parent pointers pointing to the wrong parent (caught by checking parent relationships in tests)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When cascade finally worked and all 20+ tests turned green simultaneously? That's the confidence TDD gives you. I wasn't hoping it worked: I knew it did. That's the payoff: shipping with certainty, not crossed fingers. (by now I guess you might have understand that I'm a strong believer in the increased productivity and overall quality TDD is bringing).&lt;/p&gt;

&lt;p&gt;And yes, 100% coverage throughout. Some call it obsessive. I call it professional 😇.&lt;/p&gt;

&lt;h3&gt;
  
  
  The "It Just Clicked" Moment
&lt;/h3&gt;

&lt;p&gt;There was this moment, probably 10 hours in, debugging why searches failed after cascade. I was staring at my tree visualization (can't help but thanks Copilot for that helper function!), and suddenly I &lt;em&gt;saw&lt;/em&gt; it: the children weren't following their parent after the split.&lt;/p&gt;

&lt;p&gt;That moment when the entire structure suddenly made sense (not just conceptually, but in memory, pointers, the whole graph of relationships) that's what hands-on gets you. No amount of reading gives you that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Numbers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Time:&lt;/strong&gt; ~15 hours across 4 days&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code:&lt;/strong&gt; ~200 lines of implementation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tests:&lt;/strong&gt; 20+ unit tests, 100% coverage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bugs found:&lt;/strong&gt; Too many to count (but TDD caught them all)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coffee consumed:&lt;/strong&gt; Significant&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;This was way more fun than I expected. There's something deeply satisfying about implementing a data structure you've used for years without really understanding it.&lt;/p&gt;

&lt;p&gt;Next up: LSM-trees (Log-Structured Merge trees). They represent the opposite tradeoff: B-trees optimize for reads (in-place updates), LSM-trees optimize for writes (append-only). Understanding both will show me why Cassandra chose LSM while PostgreSQL chose B+trees.&lt;/p&gt;

&lt;p&gt;Reading about compaction strategies is one thing. Implementing them will be another level entirely 🤩.&lt;/p&gt;

&lt;p&gt;If you're curious about database internals, start with "Database Internals" by Alex Petrov. Then pick something and build it. The gap between reading and implementing is where the real ~magic~ learning happens.&lt;/p&gt;

&lt;p&gt;You can see the full implementation on my &lt;a href="https://gitlab.ipsyn.net/bergacorp/oakdb" rel="noopener noreferrer"&gt;self-hosted GitLab&lt;/a&gt; (yes, I run my own instance: infra nerd life). Pull requests welcome if you spot ways to improve it! 😊&lt;/p&gt;

</description>
      <category>database</category>
      <category>go</category>
      <category>btree</category>
      <category>internals</category>
    </item>
    <item>
      <title>Hugo.io - Multiline cells in a table</title>
      <dc:creator>Stephanie Bergamo</dc:creator>
      <pubDate>Fri, 10 Jun 2022 07:22:26 +0000</pubDate>
      <link>https://dev.to/steph_baltus/hugoio-multiline-cells-in-a-table-48o6</link>
      <guid>https://dev.to/steph_baltus/hugoio-multiline-cells-in-a-table-48o6</guid>
      <description>&lt;h2&gt;
  
  
  A problem
&lt;/h2&gt;

&lt;p&gt;Building table in &lt;code&gt;markdown&lt;/code&gt; is super easy and &lt;a href="https://sourceforge.net/p/hugo-generator/wiki/markdown_syntax/#md_ex_tables" rel="noopener noreferrer"&gt;well-documented&lt;/a&gt;.&lt;br&gt;&lt;br&gt;
However, having a bit of formatting within it, like carriage return within a cell as below is not possible by default.&lt;br&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F914yisjzcxhivhqry9bn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F914yisjzcxhivhqry9bn.png" alt="multiline-cell" width="398" height="105"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://github.com/gohugoio/hugo/issues/6581" rel="noopener noreferrer"&gt;Hugo&lt;/a&gt; uses &lt;a href="https://github.com/yuin/goldmark/" rel="noopener noreferrer"&gt;Goldmark&lt;/a&gt; (a &lt;a href="https://spec.commonmark.org/0.29/" rel="noopener noreferrer"&gt;CommonMark&lt;/a&gt; implementation in &lt;code&gt;Go&lt;/code&gt;) to process the markdown. Apparently it's extremely fast, with this implementation we choose to block any HTML processing d'HTML by default. &lt;/p&gt;
&lt;h2&gt;
  
  
  A solution
&lt;/h2&gt;

&lt;p&gt;Once you know this, solving this is pretty straightforward.&lt;/p&gt;

&lt;p&gt;You can customize plenty of Goldmark options in the configuration file (&lt;code&gt;config.toml/yaml/json&lt;/code&gt;). &lt;br&gt;
So what we want to do is autorising Goldmark to render &lt;code&gt;HTML&lt;/code&gt; content :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[markup.goldmark]&lt;/span&gt;
    &lt;span class="nn"&gt;[markup.goldmark.renderer]&lt;/span&gt;
    &lt;span class="py"&gt;unsafe&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From now, within the table, we just need to add &lt;code&gt;&amp;lt;br&amp;gt;&lt;/code&gt; tag as below :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;|Adresse | Parking de La Poste&amp;lt;br&amp;gt; Le Bourg &amp;lt;br&amp;gt;97122 Baie-Mahault |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it ! &lt;/p&gt;

&lt;h2&gt;
  
  
  Free tips
&lt;/h2&gt;

&lt;p&gt;If you don't want to bother with &lt;code&gt;|&lt;/code&gt; when building tables, someone very nice has built a &lt;a href="https://www.tablesgenerator.com/markdown_tables" rel="noopener noreferrer"&gt;table generator&lt;/a&gt;.&lt;br&gt;&lt;br&gt;
We thank this person, from the bottom of our heart 💜. &lt;/p&gt;

</description>
      <category>hugo</category>
      <category>markdown</category>
      <category>formatting</category>
      <category>blogging</category>
    </item>
    <item>
      <title>Testing with Kotlin and JUnit5</title>
      <dc:creator>Stephanie Bergamo</dc:creator>
      <pubDate>Fri, 10 Jun 2022 07:10:53 +0000</pubDate>
      <link>https://dev.to/steph_baltus/testing-with-kotlin-and-junit5-3gjo</link>
      <guid>https://dev.to/steph_baltus/testing-with-kotlin-and-junit5-3gjo</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;I've recently started coding backend and mobile stuff in &lt;a href="https://kotlinlang.org/" rel="noopener noreferrer"&gt;kotlin&lt;/a&gt;, following a book, then starting a pet project, ...&lt;br&gt;&lt;br&gt;
In the meantime, I've also started to gain a lot of interest on &lt;strong&gt;&lt;a href="https://www.agilest.org/devops/test-driven-development/" rel="noopener noreferrer"&gt;Test Driven Development&lt;/a&gt;&lt;/strong&gt; and found myself wondering &lt;strong&gt;&lt;em&gt;"wow, how do I test in Kotlin ? which framework should I use?"&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To learn further, I've deciced to start a serie of articles on this topic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Basic testing&lt;/strong&gt; in Kotlin with &lt;a href="https://junit.org/junit5/docs/current/user-guide/#writing-tests" rel="noopener noreferrer"&gt;JUnit5&lt;/a&gt; from the Java ecosystem (this article)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Basic testing&lt;/strong&gt; in Kotlin with &lt;a href="https://kotest.io/" rel="noopener noreferrer"&gt;Kotest&lt;/a&gt; built for kotlin specificatlly (upcoming)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://dzone.com/articles/how-to-test-software-part-i-mocking-stubbing-and-c" rel="noopener noreferrer"&gt;Mocking, stubbing and contract testing&lt;/a&gt;&lt;/strong&gt; in Kotlin with JUnit5 and Kotest (upcoming)&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;comparision of these two testing frameworks&lt;/strong&gt; (mostly in term of features, usability, readability), this one might be a bit opinionated. (upcoming)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm a java-ist so my go-to test framework is JUnit, this is the sole reason why I'm starting by this one.&lt;/p&gt;
&lt;h3&gt;
  
  
  Agenda
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Getting started&lt;/li&gt;
&lt;li&gt;Execution and lifecycle&lt;/li&gt;
&lt;li&gt;A very simple test&lt;/li&gt;
&lt;li&gt;Checking exceptions&lt;/li&gt;
&lt;li&gt;The power of parameterized tests&lt;/li&gt;
&lt;li&gt;Conditional tests&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you just want to jump in the code, &lt;a href="https://gitlab.ipsyn.net/bikette/learn-tdd/-/tree/8d036da4e71709c56d0be96265881fe95f6da896/money/money/src/test/kotlin" rel="noopener noreferrer"&gt;please yourself&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Basics on testing with JUnit5
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Setup
&lt;/h3&gt;

&lt;p&gt;For the Gradle users, here's the setup. &lt;/p&gt;

&lt;p&gt;To be able to use the JUnit5 features, we must first add the dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;  &lt;span class="nf"&gt;testImplementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"org.junit.jupiter:junit-jupiter-api:5.8.2"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;testImplementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"org.junit.jupiter:junit-jupiter-engine:5.8.2"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we must specify we want to use JUnit to run the tests :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// For Gradle + Groovy &lt;/span&gt;
&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;useJUnitPlatform&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// For Gradle + Kotlin &lt;/span&gt;
&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="nf"&gt;useJUnitPlatform&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The entire necessary configuration can be found &lt;a href="https://gitlab.ipsyn.net/bikette/learn-tdd/-/blob/3f3b9ebc3cd3b7627dc023fca7a2bf4ff733dc45/money/money/build.gradle.kts" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Note: I'm using &lt;code&gt;gradle 7.3.3&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tests execution and orchestration
&lt;/h3&gt;

&lt;p&gt;Junit will consider as a test any function annotated with &lt;code&gt;@Test&lt;/code&gt;, &lt;code&gt;@RepeatedTest&lt;/code&gt;, &lt;code&gt;@ParameterizedTest&lt;/code&gt;, &lt;code&gt;@TestFactory&lt;/code&gt;, or &lt;code&gt;@TestTemplate&lt;/code&gt;.&lt;br&gt;&lt;br&gt;
We also have annotation to help us wrap test execution:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@BeforeAll&lt;/code&gt; executed first, before the whole test suite. useful for instiantiating external dependencies for instance.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@BeforeEach&lt;/code&gt; executed after &lt;code&gt;BeforeAll&lt;/code&gt; and before any test, useful when we need to ensure the state is clean before launching for example.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@AfterEach&lt;/code&gt; not surprisingly, executed after any test.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@AfterAll&lt;/code&gt; executed at the end of the test suite, for housekeeping purpose, pushing stats or whatever.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  A very simple test
&lt;/h3&gt;

&lt;p&gt;Now we're all set, time to code and test!&lt;br&gt;&lt;br&gt;
Let's say we have a useless class like this one :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Dollar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;

    &lt;span class="k"&gt;operator&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;times&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;multiplier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;times&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;multiplier&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Obviously we want to test this class has the expected behavior, with a very basic test:&lt;/p&gt;

&lt;p&gt;We'll need to first import the JUnit5 helpers :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.junit.jupiter.api.Assertions&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.junit.jupiter.api.Test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then we can create our test as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;`should&lt;/span&gt; &lt;span class="n"&gt;multiply&lt;/span&gt; &lt;span class="nf"&gt;correctly`&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;five&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Dollar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;five&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;times&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nc"&gt;Assertions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;five&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can now execute this, using our favorite IDE or the simple gradle command :&lt;br&gt;&lt;br&gt;
&lt;code&gt;./gradlew test&lt;/code&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  A quick word on naming
&lt;/h4&gt;

&lt;p&gt;Well, no strong rule here, but a test method should :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;test one thing, and one thing only&lt;/li&gt;
&lt;li&gt;have an explicit name so the expectation is clear&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You may have notice that I'm using backtick here.&lt;br&gt;&lt;br&gt;
This is a Kotlin capability, identifier for variable and method can use them, and although there's nothing mandatory here, I find it clearer.&lt;/p&gt;
&lt;h3&gt;
  
  
  What about checking exceptions ?
&lt;/h3&gt;

&lt;p&gt;Ok, now we don't want to multiply our dollar by zero, so we change our code a bit, to raise an exception if this occurs.  &lt;/p&gt;

&lt;p&gt;Let's write the test first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;`should&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="n"&gt;multiplying&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="p"&gt;(){&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;one&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Dollar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;assertThrows&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;NoMoneyException&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;one&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;times&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yep, it's just that easy, okay, this does not even compile, since the NoMoneyException class does not exists. Let's create it !&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NoMoneyException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Throwable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We then update our &lt;code&gt;times&lt;/code&gt; operator :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;operator&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;times&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;multiplier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Dollar&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;multiplier&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nc"&gt;NoMoneyException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Can't multiply by zero"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Dollar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;times&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;multiplier&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can run since and see the green test :)&lt;/p&gt;

&lt;h3&gt;
  
  
  Let's execute the same test with different inputs !
&lt;/h3&gt;

&lt;p&gt;I think you'll agree, if we add more test cases, we'll be loosing readability and we'll duplicate the same test again and again.  &lt;/p&gt;

&lt;h4&gt;
  
  
  Grouped assertions
&lt;/h4&gt;

&lt;p&gt;Well there's a few great &lt;a href="https://junit.org/junit5/docs/current/user-guide/#writing-tests-assertions-kotlin" rel="noopener noreferrer"&gt;Kotlin assertions&lt;/a&gt; that comes with Junit, let's play with &lt;code&gt;assertAll&lt;/code&gt; and a collection with a multiplier and the expected result.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;`should&lt;/span&gt; &lt;span class="n"&gt;multiply&lt;/span&gt; &lt;span class="n"&gt;using&lt;/span&gt; &lt;span class="nf"&gt;stream`&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;five&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Dollar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;inputs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;arrayListOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;arrayListOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nf"&gt;arrayListOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nf"&gt;arrayListOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;assertAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"should provide the expected result"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;inputs&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// Remove this line and use the collection directly&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;assertEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Dollar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;five&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;times&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Parameterized tests
&lt;/h4&gt;

&lt;p&gt;Well, I've used a lot of  &lt;strong&gt;&lt;a href="https://dave.cheney.net/2013/06/09/writing-table-driven-tests-in-go" rel="noopener noreferrer"&gt;Table Driven Tests&lt;/a&gt;&lt;/strong&gt; in &lt;a href="https://go.dev/" rel="noopener noreferrer"&gt;golang&lt;/a&gt;, this is super helpful to write compact and repeatable tests.&lt;br&gt;&lt;br&gt;
With JUnit, we can achieve the same with &lt;strong&gt;Parameterized tests&lt;/strong&gt;.  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests" rel="noopener noreferrer"&gt;Parameterized tests&lt;/a&gt; makes tests more readable and avoid duplicates, but don't take my word for it, let's code !  &lt;/p&gt;

&lt;p&gt;First, we'll need to add a new dependency :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;   &lt;span class="nf"&gt;testImplementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"org.junit.jupiter:junit-jupiter-params:5.8.2"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now let's replace our previous example and use a &lt;code&gt;CsvSource&lt;/code&gt; with the &lt;code&gt;multiplier&lt;/code&gt;  and the &lt;code&gt;expected&lt;/code&gt; value :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@ParameterizedTest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"multiply {0} by 5 should return {1}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@CsvSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"2, 10"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"3, 15"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"10, 50"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;`should&lt;/span&gt; &lt;span class="n"&gt;multiply&lt;/span&gt; &lt;span class="nf"&gt;correctly`&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;multiplier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt; &lt;span class="p"&gt;){&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;five&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Dollar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nc"&gt;Assertions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Dollar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;five&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;times&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;multiplier&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So basically, we :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;add the &lt;code&gt;@ParameterizedTest&lt;/code&gt; annotation and optionnally define a name,&lt;/li&gt;
&lt;li&gt;add the annotation for the the type of argument source we want to provide (&lt;code&gt;@CsvSource&lt;/code&gt; here) with the related test cases&lt;/li&gt;
&lt;li&gt;enjoy ! &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You may have noticed &lt;strong&gt;the name of the test&lt;/strong&gt;? Well, this little trick makes our test super explicit and easier to debug, see for yourself :&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbtcngepcu33k6yk0bcis.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbtcngepcu33k6yk0bcis.png" alt="test label" width="800" height="266"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With that, you can directly see which testcase fails, see for yourself: &lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmqf23p8utoych7f160to.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmqf23p8utoych7f160to.png" alt="Parameterized tests output" width="654" height="184"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I don't know about you, but I personnally find it &lt;strong&gt;more readable than the group assertions&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For more info about &lt;strong&gt;customizing the display name&lt;/strong&gt;, see &lt;a href="https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests-display-names" rel="noopener noreferrer"&gt;this part&lt;/a&gt; of the JUnit documentation.&lt;/p&gt;

&lt;p&gt;Another nice thing, is that we can use several types of arguments as inputs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ValueSource&lt;/code&gt; allows to pass a list of arguments of primitive types, String and Class: useful for a testing a single argument. &lt;a href="https://gitlab.ipsyn.net/bikette/learn-tdd/-/blob/8d036da4e71709c56d0be96265881fe95f6da896/money/money/src/test/kotlin/TestMultiplicationParameterized.kt#L30" rel="noopener noreferrer"&gt;See an example here&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CsvSource&lt;/code&gt; as show-cased here, we can pass an unlimited list of arguments as a string representing a CSV input&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CsvFileSource&lt;/code&gt; same as the previous file, except we use a CSV file. &lt;a href="https://gitlab.ipsyn.net/bikette/learn-tdd/-/blob/8d036da4e71709c56d0be96265881fe95f6da896/money/money/src/test/kotlin/TestMultiplicationParameterized.kt#L47" rel="noopener noreferrer"&gt;See an example here&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;EnumSource&lt;/code&gt; lets you run the same test for each constant (or a selected set) of a given ENUM. &lt;a href="https://gitlab.ipsyn.net/bikette/learn-tdd/-/blob/8d036da4e71709c56d0be96265881fe95f6da896/money/money/src/test/kotlin/TestMultiplicationParameterized.kt#L54" rel="noopener noreferrer"&gt;See an example here&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MethodSource&lt;/code&gt; this one is super powerful, we can basically have anything we want as input source (say a JSON or a Parquet file), process it with the &lt;code&gt;MethodSource&lt;/code&gt; and use it to execute our tests. &lt;a href="https://gitlab.ipsyn.net/bikette/learn-tdd/-/blob/8d036da4e71709c56d0be96265881fe95f6da896/money/money/src/test/kotlin/TestMultiplicationParameterized.kt#L75" rel="noopener noreferrer"&gt;See an example here&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ArgumentSource&lt;/code&gt; this one goes even further than &lt;code&gt;MethodSource&lt;/code&gt;. With a new class, implementing the &lt;code&gt;ArgumentProvider&lt;/code&gt; interface, you can generate input data. &lt;a href="https://gitlab.ipsyn.net/bikette/learn-tdd/-/blob/8d036da4e71709c56d0be96265881fe95f6da896/money/money/src/test/kotlin/TestMultiplicationParameterized.kt#L84" rel="noopener noreferrer"&gt;See an example here&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We also have a bit of syntaxic sugar with &lt;code&gt;@NullSource&lt;/code&gt;, &lt;code&gt;@EmptySource&lt;/code&gt; and &lt;code&gt;@NullAndEmptySource&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;h4&gt;
  
  
  Be careful when using BeforeEach and "parameterized" or "repeated" tests
&lt;/h4&gt;

&lt;p&gt;Each item of a parameterized or repeated test suite is considered as a single test.&lt;br&gt;&lt;br&gt;
Therefore, whatever is defined in &lt;code&gt;BeforeEach&lt;/code&gt; will be executed before each occurence of the test source.  &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Say we define a &lt;code&gt;BeforeEach&lt;/code&gt; as follows :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@BeforeEach&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;initEach&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hello I'm a test"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After executing &lt;code&gt;should multiply correctly&lt;/code&gt; which have 3 rows in its &lt;code&gt;CsvSource&lt;/code&gt;, we'll have the following output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hello I'm a test
hello I'm a test
hello I'm a test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Do we really want to execute this test ?
&lt;/h3&gt;

&lt;p&gt;JUnit comes with multiple way to decide whether a test sould be executed or not, depending of the context.&lt;br&gt;&lt;br&gt;
First we can totally disable a test or a class, by adding the &lt;code&gt;@Disabled&lt;/code&gt; annotation.  &lt;/p&gt;

&lt;p&gt;In addition, we can programmatically define condition execution depending on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the &lt;strong&gt;Operating System&lt;/strong&gt;, using &lt;code&gt;@EnabledOnOs&lt;/code&gt;, &lt;code&gt;@DisabledOnOs&lt;/code&gt; and the &lt;code&gt;OS&lt;/code&gt; Enum:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@EnabledOnOs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MAC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@EnabledOnOs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MAC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;OS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LINUX&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;the &lt;strong&gt;JRE&lt;/strong&gt;, with &lt;code&gt;@EnabledOnJre&lt;/code&gt;, &lt;code&gt;@EnabledForJreRange&lt;/code&gt;, &lt;code&gt;@DisabledOnJre&lt;/code&gt;, &lt;code&gt;@DisabledForJreRange&lt;/code&gt; and the &amp;nbsp;&lt;code&gt;JRE&lt;/code&gt; Enum:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@EnabledOnJre&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;JAVA_8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@EnabledForJreRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;JRE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;JAVA_11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;JRE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;JAVA_16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;system properties&lt;/strong&gt;, with &lt;code&gt;@EnabledIfSystemProperty&lt;/code&gt; and &lt;code&gt;@DisabledIfSystemProperty&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@EnabledIfSystemProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;named&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"os.arch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;matches&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;".*64.*"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;one or multiple &lt;strong&gt;environment variable(s)&lt;/strong&gt;, &lt;code&gt;@EnabledIfEnvironmentVariable&lt;/code&gt;, &lt;code&gt;@EnabledIfEnvironmentVariables&lt;/code&gt;, &lt;code&gt;@DisabledIfEnvironmentVariable&lt;/code&gt; and &lt;code&gt;@DisabledIfEnvironmentVariable&lt;/code&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@EnabledIfEnvironmentVariable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;named&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"EXEC_ENV"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;matches&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;".*ci.*"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@EnabledIfEnvironmentVariable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;named&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"DEBUG"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;matches&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that the two singular annotations are repeatable.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a &lt;strong&gt;custom condition&lt;/strong&gt;*, using &lt;code&gt;@EnabledIf&lt;/code&gt; and &lt;code&gt;@DisabledIf&lt;/code&gt; with a method name or its FQN (if the method is not in the same class) as string:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@EnabledIf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"execAlways"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;`something&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="nf"&gt;test`&lt;/span&gt;&lt;span class="p"&gt;(){}&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;execAlways&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;That's it for this first part, covering the basics on testing Kotlin code with JUnit, we can already check a lot of things with :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;grouped assertions&lt;/li&gt;
&lt;li&gt;exception check&lt;/li&gt;
&lt;li&gt;parameterized tests&lt;/li&gt;
&lt;li&gt;conditional tests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I hope you'll find this useful, you can find the full test implementation &lt;a href="https://gitlab.ipsyn.net/bikette/learn-tdd/-/tree/8d036da4e71709c56d0be96265881fe95f6da896/money/money/src/test/kotlin" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>bestpractices</category>
      <category>tdd</category>
    </item>
    <item>
      <title>Ingesting an S3 file into an RDS PostgreSQL table</title>
      <dc:creator>Stephanie Bergamo</dc:creator>
      <pubDate>Fri, 10 Jun 2022 07:02:30 +0000</pubDate>
      <link>https://dev.to/steph_baltus/ingesting-an-s3-file-into-an-rds-postgresql-table-4n3b</link>
      <guid>https://dev.to/steph_baltus/ingesting-an-s3-file-into-an-rds-postgresql-table-4n3b</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;I'm using &lt;a href="https://aws.amazon.com/redshift/" rel="noopener noreferrer"&gt;Redshift&lt;/a&gt; for a while now, and one feature I find particularly useful, is the ability to &lt;a href="https://docs.aws.amazon.com/redshift/latest/dg/r_COPY.html" rel="noopener noreferrer"&gt;load a table from the content of an S3 file&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="s1"&gt;'s3://bucket-name/path/file.ext'&lt;/span&gt;
  &lt;span class="n"&gt;iam_role&lt;/span&gt; &lt;span class="s1"&gt;'arn:aws:iam::&amp;lt;aws-account-id&amp;gt;:role/&amp;lt;role-name&amp;gt;'&lt;/span&gt;
  &lt;span class="n"&gt;region&lt;/span&gt; &lt;span class="s1"&gt;'region-code'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lately, I needed to do the very same thing with a &lt;strong&gt;PostgreSQL database, hosted on RDS&lt;/strong&gt;.&lt;br&gt;&lt;br&gt;
The good news is : YES WE CAN !&lt;/p&gt;
&lt;h2&gt;
  
  
  The solution
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Setup
&lt;/h3&gt;

&lt;p&gt;AWS pre-installs (among other things) 2 extensions on our RDS :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;aws_commons&lt;/li&gt;
&lt;li&gt;aws_s3&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now, since we need to interact with S3, we simply need to run the following command, assuming&lt;br&gt;
our &lt;code&gt;user&lt;/code&gt; is a superuser or has database owner privileges:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;aws_s3&lt;/span&gt; &lt;span class="k"&gt;CASCADE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command installs both &lt;code&gt;aws_commons&lt;/code&gt; and &lt;code&gt;aws_s3&lt;/code&gt;.&lt;br&gt;&lt;br&gt;
Okay, that was the easy part.&lt;/p&gt;

&lt;p&gt;Well, now we have to work on the necessary IAM role for our RDS instance...and that's where we'll find the first pitfall...&lt;br&gt;&lt;br&gt;
Under the hood, installing the &lt;code&gt;aws_s3&lt;/code&gt; extension also adds an IAM role, managed by AWS (and therefore, read only for us)&lt;br&gt;
 to interact with plenty of AWS services.&lt;br&gt;
As we can see below, the role is linked to the &lt;code&gt;S3Import&lt;/code&gt; function :&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqs1rvkufesu695npupy0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqs1rvkufesu695npupy0.png" alt="AWS Role" width="798" height="142"&gt;&lt;/a&gt;&lt;br&gt;
The problem is, &lt;strong&gt;we can only link a single role to a function&lt;/strong&gt; and this one does not have any access to our S3 bucket, therefore this cannot work.&lt;/p&gt;

&lt;p&gt;In the end, even if the policy is super short in &lt;a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/PostgreSQL.Procedural.Importing.html#USER_PostgreSQL.S3Import.ARNRole" rel="noopener noreferrer"&gt;the doc&lt;/a&gt;, here is what we actually need :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3import"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3:GetObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3:ListBucket"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::bucket-name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::bucket-name/*"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"rds:CrossRegionCommunication"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ec2:AllocateAddress"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ec2:AssociateAddress"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ec2:AuthorizeSecurityGroupIngress"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ec2:CreateNetworkInterface"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ec2:CreateSecurityGroup"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ec2:DeleteNetworkInterface"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ec2:DeleteSecurityGroup"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ec2:DescribeAddresses"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ec2:DescribeAvailabilityZones"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ec2:DescribeCoipPools"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ec2:DescribeInternetGateways"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ec2:DescribeLocalGatewayRouteTables"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ec2:DescribeLocalGatewayRouteTableVpcAssociations"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ec2:DescribeLocalGateways"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ec2:DescribeSecurityGroups"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ec2:DescribeSubnets"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ec2:DescribeVpcAttribute"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ec2:DescribeVpcs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ec2:DisassociateAddress"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ec2:ModifyNetworkInterfaceAttribute"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ec2:ModifyVpcEndpoint"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ec2:ReleaseAddress"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ec2:RevokeSecurityGroupIngress"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ec2:CreateVpcEndpoint"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ec2:DescribeVpcEndpoints"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ec2:DeleteVpcEndpoints"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ec2:AssignPrivateIpAddresses"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"ec2:UnassignPrivateIpAddresses"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"sns:Publish"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"logs:CreateLogGroup"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:logs:*:*:log-group:/aws/rds/*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:logs:*:*:log-group:/aws/docdb/*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:logs:*:*:log-group:/aws/neptune/*"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"logs:CreateLogStream"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"logs:PutLogEvents"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"logs:DescribeLogStreams"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:logs:*:*:log-group:/aws/rds/*:log-stream:*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:logs:*:*:log-group:/aws/docdb/*:log-stream:*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:logs:*:*:log-group:/aws/neptune/*:log-stream:*"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"kinesis:CreateStream"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"kinesis:PutRecord"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"kinesis:PutRecords"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"kinesis:DescribeStream"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"kinesis:SplitShard"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"kinesis:MergeShards"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"kinesis:DeleteStream"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"kinesis:UpdateShardCount"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:kinesis:*:*:stream/aws-rds-das-*"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"cloudwatch:PutMetricData"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"StringEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"cloudwatch:namespace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AWS/RDS"&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once created, we we'll need to :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;unlike the managed role from the &lt;code&gt;s3Import&lt;/code&gt; function;&lt;/li&gt;
&lt;li&gt;link the new role to the &lt;code&gt;s3Import&lt;/code&gt; function;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Careful now, &lt;strong&gt;swapping and applying the role may takes a few minutes&lt;/strong&gt;, don't believe what the AWS console tells you.&lt;/p&gt;

&lt;p&gt;That's it now, we're all set !&lt;/p&gt;

&lt;h3&gt;
  
  
  Usage
&lt;/h3&gt;

&lt;p&gt;To load the content of a CSV file from S3,  we'll just need to :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;aws_s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_import_from_s3&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
   &lt;span class="s1"&gt;'target-table'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="s1"&gt;'columns list, separated by a comma, OPTIONAL'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="s1"&gt;'DELIMITER &lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;,&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt; CSV HEADER'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="n"&gt;aws_commons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_s3_uri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
           &lt;span class="s1"&gt;'bucket-name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="s1"&gt;'file-prefix'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="s1"&gt;'region-code'&lt;/span&gt;
       &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When providing an empty string as column list, the command will try to map the file columns to the table columns.&lt;/p&gt;

&lt;h3&gt;
  
  
  The pitfalls
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Constraints
&lt;/h4&gt;

&lt;p&gt;Here are some constraints to keep in mind, not necessarily documented :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the bucket name &lt;strong&gt;must not contain a period &lt;code&gt;.&lt;/code&gt;&lt;/strong&gt; (I got this error message : &lt;code&gt;S3 bucket names with a period (.) are not supported&lt;/code&gt;). Yeah I know we should have &lt;code&gt;path-style&lt;/code&gt; S3 bucket, but hey, one has to deal with legacy right ?!&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;;&lt;/code&gt; &lt;a href="https://stackoverflow.com/questions/62302191/importing-csv-file-with-semicolon-delimiters-from-s3-to-rds-postgres-database" rel="noopener noreferrer"&gt;cannot be used as a delimiter&lt;/a&gt;, but just so you know, this is not a PostgreSQL limitation as it accepts any character as a delimiter, as long as it's single one-byte character; &lt;/li&gt;
&lt;li&gt;just like the &lt;a href="https://www.postgresql.org/docs/9.2/sql-copy.html" rel="noopener noreferrer"&gt;&lt;code&gt;COPY&lt;/code&gt;command from PostgreSQL&lt;/a&gt; &lt;strong&gt;only text, CSV, or binary format are supported&lt;/strong&gt;; &lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Error handling
&lt;/h4&gt;

&lt;p&gt;When an error occurs, the PG client doesn't get a real, explicit error message. I mean, either for an unsupported bucket&lt;br&gt;
name or a missing access right, we get the very same error message :&lt;br&gt;
&lt;code&gt;Unable to generate pre-signed url, look at engine log for details&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;We must go dig in the PG logs, from the RDS console to get an explicit error message and debug efficiently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Once we get through the installation and understand the limits and pitfalls, this feature is super useful and easy to use.&lt;/p&gt;

&lt;p&gt;Unfortunately (well, this is not really surprising) &lt;strong&gt;we can't install the two extensions cannot on simple EC2&lt;/strong&gt;.&lt;br&gt;
Life would be so nice if we could use AWS feature as we wish, where we wish !&lt;/p&gt;

&lt;p&gt;So we get to choose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;either we go for RDS, but we stick to the AWS handpicked extensions (exit &lt;a href="https://www.timescale.com/" rel="noopener noreferrer"&gt;timescale&lt;/a&gt;, &lt;a href="https://github.com/citusdata/citus" rel="noopener noreferrer"&gt;citus&lt;/a&gt; or their &lt;a href="https://github.com/citusdata/cstore_fdw" rel="noopener noreferrer"&gt;columnar storage&lt;/a&gt;, ... ),&lt;/li&gt;
&lt;li&gt;or we use other solutions, and we deprive ourselves of all the benefits of hosted / managed services. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://g33k.life/en/tech/load-rds-via-s3/" rel="noopener noreferrer"&gt;https://g33k.life/en/tech/load-rds-via-s3/&lt;/a&gt; on December 16, 2020.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>data</category>
      <category>engineering</category>
      <category>tips</category>
    </item>
  </channel>
</rss>
