<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Software Architecture on Steve Sun</title><link>https://sund.site/en/categories/software-architecture/</link><description>Recent content in Software Architecture on Steve Sun</description><generator>Hugo</generator><language>en</language><copyright>© 2013-2026, Steve Sun</copyright><lastBuildDate>Mon, 02 Jun 2025 07:58:17 +0800</lastBuildDate><follow_challenge><feedId>41397727810093074</feedId><userId>56666701051455488</userId></follow_challenge><atom:link href="https://sund.site/en/categories/software-architecture/index.xml" rel="self" type="application/rss+xml"/><item><title>How AI Coding Tools Like Cursor Work Under the Hood</title><link>https://sund.site/en/posts/2025/ast-chunk/</link><pubDate>Mon, 02 Jun 2025 07:58:17 +0800</pubDate><guid>https://sund.site/en/posts/2025/ast-chunk/</guid><description>&lt;p&gt;In my previous post, &lt;a href="https://sund.site/en/posts/2025/build-deepwiki/"&gt;How DeepWiki Works&lt;/a&gt;, I shared one possible way DeepWiki is implemented. I left a question there: how does DeepWiki chunk a source code repository?&lt;/p&gt;
&lt;p&gt;The answer is AST chunking.&lt;/p&gt;
&lt;p&gt;In this post I want to analyze how two software development aids — Cursor and Cline — implement &amp;ldquo;code indexing.&amp;rdquo; In fact, they are not fundamentally different from DeepWiki; all of them use AST chunking.&lt;/p&gt;
&lt;h2 id="ast"&gt;AST&lt;/h2&gt;
&lt;p&gt;An &lt;strong&gt;Abstract Syntax Tree&lt;/strong&gt; (&lt;strong&gt;AST&lt;/strong&gt;) is a tree representation of source code that reflects the code&amp;rsquo;s syntactic structure. When chunking code, ASTs help us better understand the semantic boundaries of the code.&lt;/p&gt;
&lt;p&gt;ASTs are widely used in compilers and source code analysis tools. For example, in the frontend world, Babel and the TypeScript compiler (TSC) use ASTs to transform ES6 or TypeScript code into JavaScript that browsers can run.&lt;/p&gt;
&lt;p&gt;Below is a simple example showing how an AST converts TypeScript code into a tree structure. Suppose we have this TypeScript function:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;greet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Hello, &amp;#34;&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;After being processed by an AST tool, it is abstracted into the following tree:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SourceFile
&lt;ul&gt;
&lt;li&gt;FunctionDeclaration
&lt;ul&gt;
&lt;li&gt;Identifier: &amp;ldquo;greet&amp;rdquo;&lt;/li&gt;
&lt;li&gt;Parameter
&lt;ul&gt;
&lt;li&gt;Identifier: &amp;ldquo;name&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Block
&lt;ul&gt;
&lt;li&gt;ReturnStatement
&lt;ul&gt;
&lt;li&gt;BinaryExpression
&lt;ul&gt;
&lt;li&gt;StringLiteral: &amp;ldquo;Hello, &amp;quot;&lt;/li&gt;
&lt;li&gt;Identifier: &amp;ldquo;name&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A compiler can then walk this tree and translate it node by node into JavaScript code.&lt;/p&gt;
&lt;p&gt;Once you understand ASTs, you roughly understand how DeepWiki — and even code editors like Cursor — build code indexes.&lt;/p&gt;
&lt;h2 id="cursor"&gt;Cursor&lt;/h2&gt;
&lt;p&gt;In &lt;a href="https://www.cursor.com/ja/security#codebase-indexing"&gt;Cursor&amp;rsquo;s official documentation&lt;/a&gt;, you can find a description of how it indexes user code.&lt;/p&gt;
&lt;p&gt;Cursor scans the user&amp;rsquo;s repository, computes file hashes, and builds a Merkle tree. Similar to the way Git compares file diffs, Cursor uses the Merkle tree to detect file changes in the user&amp;rsquo;s workspace and incrementally uploads modified files to Cursor&amp;rsquo;s servers.&lt;/p&gt;
&lt;p&gt;Uploaded files are then chunked and embedded, and stored in a Turbopuffer database. This is the process of building a RAG over the source code.&lt;/p&gt;
&lt;p&gt;The chunking step uses an AST tool to structure the code into a syntax tree, then cuts the serialized tree nodes into small chunks, and finally embeds them as vectors for storage.&lt;/p&gt;
&lt;p&gt;Turbopuffer does not only store the vectorized code; it also stores metadata such as the line numbers and source file paths of the code segments.&lt;/p&gt;
&lt;p&gt;When Cursor tries to autocomplete user code or generate new code from context, it queries the Turbopuffer database, finds the vectors with the highest similarity, and gets the file path and line numbers for that segment. Cursor then reads the corresponding source code from the user&amp;rsquo;s repository and puts it into the LLM&amp;rsquo;s system context. Finally, the LLM returns the newly generated code to Cursor.&lt;/p&gt;
&lt;p&gt;A &lt;a href="https://x.com/ProgramerJohann/status/1927296026861252934"&gt;user on X&lt;/a&gt; put together this flow diagram:&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption"
&gt;
 
 &lt;img src="https://sund.site/images/ast-chunk/cursor.png" alt="" loading="lazy" /&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id="cline"&gt;Cline&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://cline.bot/blog/why-cline-doesnt-index-your-codebase-and-why-thats-a-good-thing"&gt;Cline&amp;rsquo;s official blog&lt;/a&gt; offers a glimpse of how it is implemented.&lt;/p&gt;
&lt;p&gt;Cline is an AI agent that helps with coding. Cline does not upload code and build a RAG; instead, it takes a safer and more reliable approach to managing the user&amp;rsquo;s repository.&lt;/p&gt;
&lt;p&gt;Here is the developers&amp;rsquo; description of how Cline works:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;When you point Cline at a codebase, it doesn&amp;rsquo;t immediately try to read every file. Instead, it begins by understanding the architecture. Using Abstract Syntax Trees (ASTs), Cline extracts a high-level map of your code – the classes, functions, methods, and their relationships. This happens through our list_code_definition_names tool, which provides structural understanding without requiring full implementation details.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;Cline uses its &lt;code&gt;list_code_definition_names&lt;/code&gt; tool to convert source code into an AST. Cline treats that AST as a &amp;ldquo;map&amp;rdquo; of the entire codebase.&lt;/p&gt;
&lt;p&gt;When Cline runs a task automatically, it analyzes the file that needs to be modified, builds an AST for that file, and converts the AST into natural-language context (similar to how DeepWiki turns code into documents). It feeds this context to the LLM, letting the LLM decide whether to modify the file or look at another file to gather more context.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption"
&gt;
 
 &lt;img src="https://sund.site/images/ast-chunk/cline.png" alt="" loading="lazy" /&gt;
 
 &lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;If Cursor compares similarity between vector-space code snippets, Cline converts code snippets into natural-language descriptions and lets the LLM, through semantic understanding, hunt for clues across the repository and compare the semantic similarity of code segments.&lt;/p&gt;
&lt;p&gt;Cline&amp;rsquo;s approach is clearly safer — enterprise users don&amp;rsquo;t have to worry about Cline abusing the source code. The side effect, however, is higher token consumption. Constantly fetching context across files also takes more time. In some edge cases, Cline may even bounce back and forth between two files, falling into a loop.&lt;/p&gt;
&lt;p&gt;In my own experience, Cline performs better than Cursor&amp;rsquo;s Agent mode on certain models (Deepseek-r1, OpenAI-4o), because Cline&amp;rsquo;s semantic understanding makes better use of these models&amp;rsquo; natural-language abilities than vector similarity does.&lt;/p&gt;
&lt;p&gt;For programming-optimized Claude Sonnet, though, there is no significant difference, so users need to choose between higher security and faster response time.&lt;/p&gt;
&lt;h2 id="summary"&gt;Summary&lt;/h2&gt;
&lt;p&gt;This post mainly covered how code editors use Abstract Syntax Trees (ASTs) to build code indexes and implement code completion.&lt;/p&gt;
&lt;p&gt;In general, ASTs are an important tool for understanding the syntactic structure of code, and different implementations have their own trade-offs.&lt;/p&gt;
&lt;h2 id="further-reading"&gt;Further Reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="http://www.hubwiz.com/blog/ast-based-rag-code-chunking/"&gt;http://www.hubwiz.com/blog/ast-based-rag-code-chunking/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>A General Approach to Server-Side Performance Issues in Go</title><link>https://sund.site/en/posts/2025/go-performance/</link><pubDate>Tue, 06 May 2025 10:35:41 +0800</pubDate><guid>https://sund.site/en/posts/2025/go-performance/</guid><description>&lt;p&gt;I recently ran into a performance issue. A customer reported that two Go-based service processes running in the background of their IPC device kept climbing in memory usage, peaking at 40% of total memory. One of those processes was our log-collection agent.&lt;/p&gt;
&lt;p&gt;My first suspicion was a memory leak, because we&amp;rsquo;d had memory leaks caused by goroutine blocking in the past (I discussed this in &lt;a href="https://sund.site/en/posts/2023/goroutine-leak/"&gt;Common Patterns of Memory Leaks in Go&lt;/a&gt;), so I started by reviewing everywhere we created and released goroutines.&lt;/p&gt;
&lt;p&gt;After the last incident, we&amp;rsquo;d added goroutine leak detection at the unit-test level using &lt;code&gt;go.uber.org/goleak&lt;/code&gt;. You only need to add a single line at the start of a test:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;TestXXX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;testing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;T&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;defer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;goleak&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;VerifyNone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&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&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It automatically checks for lingering goroutines after the test finishes. For background goroutines that run on a delay, you can use &lt;code&gt;wait&lt;/code&gt; or &lt;code&gt;sleep&lt;/code&gt; in the test to wait for them to be released before the test case ends.&lt;/p&gt;
&lt;p&gt;The first round of investigation ruled out problems caused by goroutines in the code itself. So I turned my attention to another suspect: scheduled tasks.&lt;/p&gt;
&lt;p&gt;According to the customer, memory would slowly climb even with no foreground activity.&lt;/p&gt;
&lt;p&gt;In our code, we use the third-party package &lt;code&gt;github.com/robfig/cron/v3&lt;/code&gt;, which orchestrates scheduled tasks. Usage looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;c&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="nx"&gt;cron&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;New&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;@every 10s&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;callbackFunc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This structure defines a scheduled task. Its implementation is also based on goroutines, so I added Go&amp;rsquo;s built-in pprof to the dependencies in &lt;code&gt;main.go&lt;/code&gt;, rebuilt the project binary, and deployed it to a test environment using the same hardware configuration as the customer. This way, once the project started, I could pull memory information from a specific port. (For more on pprof, see &lt;a href="https://go.dev/blog/pprof"&gt;Profiling Go Programs&lt;/a&gt;.)&lt;/p&gt;
&lt;p&gt;I used pprof&amp;rsquo;s interface to grab heap data at different intervals:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;curl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;heap&lt;/span&gt;&lt;span class="mf"&gt;.1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="cp"&gt;//127.0.0.1:6060/debug/pprof/heap&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then used:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;go tool pprof -http&lt;span class="o"&gt;=&lt;/span&gt;:8099 -base heap.1.out heap.2.out
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;to compare the two results. In the web UI, I selected the In Use Space option, which let me see what memory hadn&amp;rsquo;t been released.&lt;/p&gt;
&lt;p&gt;Even after this second round, I still didn&amp;rsquo;t find a memory leak. But this time I noticed that one of the scheduled tasks ran every 10 seconds, and CPU usage clearly spiked during execution. Looking at the code for this task, it used the third-party library &lt;code&gt;github.com/shirou/gopsutil/process&lt;/code&gt; to query system process IDs and process names.&lt;/p&gt;
&lt;p&gt;Looking at the library&amp;rsquo;s source code, I found that the way it queries process IDs is by loading all process information on the system into memory and then matching the ID or name. So, if the customer&amp;rsquo;s device has a lot of processes, each query consumes a large amount of memory.&lt;/p&gt;
&lt;p&gt;Calling this library from a scheduled task that runs every 10 seconds is clearly very inefficient.&lt;/p&gt;
&lt;p&gt;After further communication with the customer, we discovered that of the two high-memory processes, the other one also showed high CPU usage. So we had the customer send us a screenshot of the &lt;code&gt;top&lt;/code&gt; command. The moment I saw the screenshot, the truth came into focus:&lt;/p&gt;
&lt;p&gt;The customer&amp;rsquo;s IPC device was a lower-performance version—while it had plenty of memory, the CPU was struggling. When multiple processes run background tasks simultaneously, the CPU periodically maxes out, causing tasks to block. And the third-party library we use implements scheduled tasks on top of goroutines. When the previous task is blocked, the next task still creates a new background goroutine, causing goroutines to pile up in memory.&lt;/p&gt;
&lt;p&gt;This was a goroutine blocking problem caused by high CPU usage and overly short intervals in the scheduled tasks.&lt;/p&gt;
&lt;p&gt;Once we knew the cause, the rest was straightforward: optimize the code logic, ship a new version, and explain the issue to the customer&amp;hellip;&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the full process of debugging this Go service performance issue. If you run into something similar, I hope this helps.&lt;/p&gt;</description></item><item><title>Pairing with AI for Programming — Testing</title><link>https://sund.site/en/posts/2024/pairing-with-ai-01/</link><pubDate>Wed, 11 Dec 2024 17:02:43 +0800</pubDate><guid>https://sund.site/en/posts/2024/pairing-with-ai-01/</guid><description>&lt;p&gt;The future paradigm of software development will be human-AI collaborative programming. This is already an indisputable fact in the software industry. Programming tools like Windsurf, Cursor, and Copilot have, on one hand, improved development efficiency; on the other hand, they&amp;rsquo;ve made code more black-boxed, less readable, and harder to maintain.&lt;/p&gt;
&lt;p&gt;I try to briefly discuss which software development practices are more suitable for improving the observability and maintainability of AI-generated code in the AI era. All articles titled &amp;ldquo;Pairing with AI for Programming&amp;rdquo; are just thoughts to get the conversation started, not a systematic methodology. I welcome readers&amp;rsquo; corrections for any mistakes.&lt;/p&gt;
&lt;h2 id="what-are-the-common-problems-with-using-ai-to-write-code"&gt;What Are the Common Problems with Using AI to Write Code?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Observability problem: AI&amp;rsquo;s implementation is incomplete and often requires manual modification of fragments&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The biggest problem with AI-generated code is that it often introduces subtle errors that are not easily noticed by humans. When humans use prompts to modify code, due to the difficulty of observing AI&amp;rsquo;s behavior, even after fixing a bug, it may lead to other regression issues (causing errors in existing logic).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Context problem: lacking global context, fragmented code lacks connections&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Due to token limits or economic considerations, many editors will optimize the input content, which can easily lead large models to misunderstand local context. They are unable to handle business logic across functional modules. Especially when the project becomes large, complex modules often depend on other modules, and adjusting business logic requires refactoring several code files.&lt;/p&gt;
&lt;h2 id="solution-approach"&gt;Solution Approach&lt;/h2&gt;
&lt;p&gt;The core problems of AI-written code can be summarized as low maintainability caused by lack of observability and lack of context. To address these two problems, we need to first review how traditional software processes make code more observable and maintainable.&lt;/p&gt;
&lt;h3 id="human-led-unit-testing"&gt;Human-Led Unit Testing&lt;/h3&gt;
&lt;p&gt;Unit tests are the specification for code. Complex business logic usually requires reading a lot of code to understand. But experienced programmers will look at the unit tests first. Good unit tests will completely write the module&amp;rsquo;s expected inputs and outputs into the test cases. In &lt;a href="https://www.amazon.com/Unit-Testing-Principles-Practices-Patterns/dp/1617296279"&gt;Unit Testing Principles, Practices, and Patterns&lt;/a&gt;, the author believes good unit tests should have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Protection against regressions&lt;/strong&gt;. That is, tests can prevent previously fixed issues from recurring in regression testing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resistance to refactoring&lt;/strong&gt;. That is, after code refactoring, tests can correctly identify whether the refactoring has affected existing functionality.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fast feedback&lt;/strong&gt;. That is, unit tests are easy to run, and when issues are found, they can be quickly located.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Easy to maintain&lt;/strong&gt;. The maintainability of tests, unlike business code, is reflected in correctly handling dependencies and shared code.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The ultimate purpose of these principles is to ensure that the system under test behaves as expected.&lt;/p&gt;
&lt;p&gt;When AI and humans collaborate on code, I personally believe that in writing unit tests, humans should lead (80%) and AI assist (20%), because unit tests define &amp;ldquo;the behavior I expect.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Once unit tests are complete, they in turn guide the AI to implement the actual business code. At this point, human involvement decreases and AI takes the lead. Humans repeatedly run unit tests, while passing the test results along with the prompt to the AI, helping AI fix program issues.&lt;/p&gt;
&lt;h3 id="writing-ai-friendly-tests-requires-good-module-design"&gt;Writing AI-Friendly Tests Requires Good Module Design&lt;/h3&gt;
&lt;p&gt;When writing good tests, you also need to pay attention to correctly splitting modules. A good test typically gives an input and verifies whether the expected result is output. If a module depends on too many external environments for branching logic, the test output will heavily depend on external state. This reduces the module&amp;rsquo;s observability.&lt;/p&gt;
&lt;p&gt;The following two pieces of experience can help you write good code:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;When writing tests, test the result of the behavior, not the steps. When writing business code, ask AI to clearly write out the steps.&lt;/p&gt;
&lt;p&gt;The &amp;ldquo;unit&amp;rdquo; of a unit test doesn&amp;rsquo;t have to be a single class or function. It can be a group of operations completing an atomic piece of business logic. (Of course, there are different schools of thought supporting class-level testing, but that&amp;rsquo;s not the focus of this article.) To make AI-generated business code refactor-resistant, you should verify the result of the AI&amp;rsquo;s behavior, not every implementation step. Coupling test code with implementation steps means that business modifications will break existing tests, making the &amp;ldquo;expected behavior&amp;rdquo; constantly have to be modified along with the &amp;ldquo;specific implementation.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;When AI starts writing business logic, you should drive it step by step, during which humans can correct the AI&amp;rsquo;s code logic for a particular step. But be careful not to break the test logic.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Stateless code (functional) is the easiest to test&lt;/p&gt;
&lt;p&gt;Because its output is invariant. Core code should be kept as stateless as possible, with state and external system dependencies placed in the application service layer. Deep and hard-to-understand core logic should be placed in the domain service layer. The details here can refer to DDD (Domain Driven Design) thinking.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption"
&gt;
 
 &lt;img src="https://sund.site/images/pairing-with-ai-01/functional_core.png" alt="functional_core.png" loading="lazy" /&gt;
 
 &lt;figcaption&gt;functional_core.png&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="summary"&gt;Summary&lt;/h2&gt;
&lt;p&gt;This article, as the beginning of a series on human-AI collaborative programming, attempts from a testing perspective to alleviate the observability issues of AI-generated code.&lt;/p&gt;
&lt;p&gt;In future articles, I hope to discuss, from an architectural design perspective, how to design AI-friendly architectures that are easy to maintain context for.&lt;/p&gt;
&lt;p&gt;The content of the article will continue to be updated over time, and discussion is welcome.&lt;/p&gt;</description></item><item><title>Dependency Inversion in Go</title><link>https://sund.site/en/posts/2024/go-dependency-inject/</link><pubDate>Thu, 21 Nov 2024 11:26:22 +0800</pubDate><guid>https://sund.site/en/posts/2024/go-dependency-inject/</guid><description>&lt;blockquote&gt;
&lt;p&gt;This article is fairly basic; it was material I used when training Java programmers in Go.&lt;/p&gt;&lt;/blockquote&gt;
&lt;h2 id="why-dependency-inversion-principle-dip"&gt;Why Dependency Inversion Principle (DIP)?&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Dependency_inversion_principle"&gt;Dependency Inversion&lt;/a&gt;, also called dependency inversion or DIP, is a very important design principle in software development. Many programmers have never learned about it, or only know the general idea from Java Spring. Today I&amp;rsquo;d like to use a brief article and a simple Go example to explain how to implement dependency inversion in the simplest way.&lt;/p&gt;
&lt;p&gt;If you don&amp;rsquo;t yet know what it is, you can refer to the description in Wikipedia, or read &lt;a href="https://martinfowler.com/articles/dipInTheWild.html"&gt;Martin Fowler&amp;rsquo;s article on DIP&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The Dependency Inversion Principle addresses a common risk in software development: dependency.&lt;/p&gt;
&lt;p&gt;Try to recall:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;When you try to use mocks to shield underlying details for testing, you find that the class you want to test references a large number of framework-provided interfaces, requiring you to mock many underlying implementations.&lt;/li&gt;
&lt;li&gt;When you try to modify an old low-level class, but there are too many upper-layer service classes depending on it, you worry about side effects while refactoring the upper-layer code at every dependency point.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Let&amp;rsquo;s analyze these two scenarios:&lt;/p&gt;
&lt;p&gt;In scenario 1, the application class depends on the implementation provided by the framework, making it difficult to separate the application class from the framework. The industry method for dealing with this problem is called &lt;strong&gt;Inversion of Control&lt;/strong&gt; (IoC). The application class should not depend on the framework; instead, the framework provides slots, registering the application class with the framework, and the framework uniformly dispatches the application to execute the corresponding methods.&lt;/p&gt;
&lt;p&gt;In scenario 2, the service class depends on the low-level class, making modifications to the low-level class increasingly difficult. The solution is &lt;strong&gt;Dependency Injection&lt;/strong&gt; (DI). The upper-layer class does not directly reference the low-level class, but instead, the low-level class on which the upper-layer class depends is injected at the point of use.&lt;/p&gt;
&lt;p&gt;Combining these two scenarios captures the core of the Dependency Inversion Principle:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;High-level modules should not depend on low-level modules; both should depend on abstractions.&lt;/li&gt;
&lt;li&gt;Abstractions should not depend on details. Details should depend on abstractions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These two principles ensure high cohesion and low coupling among modules, while also creating the conditions for mocking and iteratively updating modules.&lt;/p&gt;
&lt;h2 id="implementing-it-in-go"&gt;Implementing It in Go&lt;/h2&gt;
&lt;p&gt;Suppose we need to query user information from a user service. There are two interfaces: &lt;code&gt;UserRepository&lt;/code&gt; serves as the data layer responsible for querying the database, and &lt;code&gt;UserService&lt;/code&gt; handles business logic and depends on &lt;code&gt;UserRepository&lt;/code&gt;. At the same time, to facilitate testing, we also need to write a mock data layer implementation. The entire structure is shown in the figure below.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption"
&gt;
 
 &lt;img src="https://sund.site/images/go-dependency-inject/example.png" alt="Go example" loading="lazy" /&gt;
 
 &lt;figcaption&gt;Go example&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Next, very easily, we implement the two interfaces and write their implementation classes. At the same time, we also write a &lt;code&gt;NewUserService&lt;/code&gt; in the &lt;code&gt;UserService&lt;/code&gt; implementation class to inject the &lt;code&gt;UserRepository&lt;/code&gt; implementation it depends on.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Implement the specific interface in user_repository.go&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;UserRepository&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;interface&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;GetByID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&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="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&lt;/span&gt;&lt;span class="c1"&gt;// ... specific implementation of UserRepository, omitted&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&lt;/span&gt;&lt;span class="c1"&gt;// Implemented in user_service.go&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;UserService&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;interface&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;GetUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&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="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;CreateUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;age&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&lt;/span&gt;&lt;span class="c1"&gt;// ... specific implementation of UserService, omitted&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;NewUserService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;UserRepository&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;UserService&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;UserServiceImpl&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&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&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So the question arises: can we directly reference the repository in &lt;code&gt;user_service.go&lt;/code&gt;? Obviously not, because this would create a dependency between the two modules.&lt;/p&gt;
&lt;p&gt;This is the core of dependency inversion: the upper-layer module does not directly reference the lower-layer module; instead, the executing class initializes the Service and injects the dependent lower-layer service.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// In main.go&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;main&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;MySQLUserRepository&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;userService&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;NewUserService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&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&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This way, when writing test mock code, you don&amp;rsquo;t need to modify any code logic. You can simply replace the parameter of &lt;code&gt;NewUserService&lt;/code&gt; in the test with a fake test instance.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// In user_service_test.go&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;TestUserService&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;MockTestUserRepository&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;userService&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;NewUserService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&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&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In addition, if the data layer changes its implementation or migrates to another database, you only need to modify two places: the data layer&amp;rsquo;s implementer and the dependency injector. The caller &lt;code&gt;UserService&lt;/code&gt; is completely unaffected. The entire project won&amp;rsquo;t form a dependency trap.&lt;/p&gt;
&lt;h2 id="summary"&gt;Summary&lt;/h2&gt;
&lt;p&gt;The two core principles of the Dependency Inversion Principle:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Modules do not depend on other modules, but both depend on abstract interfaces.&lt;/li&gt;
&lt;li&gt;Abstract interfaces do not depend on implementations; implementations depend on abstract interfaces.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Implementing these two principles in Go isn&amp;rsquo;t difficult—you just need to transform the original caller-implementer relationship into a registrar-caller-implementer relationship. There are also some libraries and frameworks in Go that implement dependency inversion, but the core ideas are not different.&lt;/p&gt;</description></item><item><title>Monitoring System Project Retrospective</title><link>https://sund.site/en/posts/2024/metrics-project-retro/</link><pubDate>Thu, 24 Oct 2024 15:52:22 +0800</pubDate><guid>https://sund.site/en/posts/2024/metrics-project-retro/</guid><description>&lt;p&gt;This article is a retrospective of a large chunk of my work over the past 3 years. As the project&amp;rsquo;s architect, I&amp;rsquo;ll also reflect on some issues left over from the early stages of the project and share my personal approach to solving them.&lt;/p&gt;
&lt;h2 id="the-project-relies-heavily-on-an-open-source-component-over-customized"&gt;The Project Relies Heavily on an Open-Source Component, Over-Customized&lt;/h2&gt;
&lt;p&gt;Our project is a collection/monitoring system for logs and software/hardware performance metrics that runs on edge devices. Considering the performance of edge computing devices (IPCs), when selecting open-source components, we emphasized being lightweight and supporting a rich set of output standards. In the early days, the department architect chose Fluent-Bit as the core component. Fluent-Bit is an open-source, lightweight, mildly extensible data collector written in C. It was originally used for log collection and has gradually evolved into a full-featured Agent. Compared to the popular &lt;a href="https://github.com/open-telemetry"&gt;OpenTelemetry&lt;/a&gt;, Fluent-Bit is more out-of-the-box and lighter, but harder to modify and extend.&lt;/p&gt;
&lt;p&gt;At the very beginning, the whole team had no experience with monitoring systems, so we dug quite a few pitfalls when designing the system. First, users had to perform overly cumbersome operations on the UI, having to sequentially configure the output target (address, port, protocol, format, encryption method, etc.), the type of metrics to collect, and finally click &amp;ldquo;Apply&amp;rdquo; manually.&lt;/p&gt;
&lt;p&gt;After several iterations, we appropriately simplified the operational logic. But like most programs running on industrial PCs, users typically don&amp;rsquo;t actively modify the configuration on the UI after initial setup. End users care more about system resource usage and stability. So initially, the team designed this project as a heavy-interaction consumer-facing product—which was a lesson learned.&lt;/p&gt;
&lt;p&gt;Second, to accommodate the UI design flow (for example, allowing users to create multiple different configuration items to different target addresses), the backend developers came up with complex workarounds. Because Fluent-Bit is a single-process event-driven model with only a single configuration file, every time the configuration file is modified, the Fluent-Bit process must be restarted. This created a risk of &lt;strong&gt;data loss during restarts&lt;/strong&gt; for a stable-running monitoring system. Additionally, if &lt;strong&gt;a newly added configuration item is wrong, it can cause the entire generated configuration file to fail, leading to issues like the Fluent-Bit process hanging&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;To solve these problems, the backend engineers came up with various tricks using Fluent-Bit&amp;rsquo;s parameters. For example, using different tags to route different user configuration items, configuring parameters and filter rules separately for each configuration item. Another example was setting the cache data packet size and cache timeout to 0, so that after Fluent-Bit restarts, it would first try to resend the data cached in the file system, indirectly preventing user data loss.&lt;/p&gt;
&lt;p&gt;These tricks not only increased maintenance difficulty but, from the user&amp;rsquo;s perspective, did not bring any real value improvement.&lt;/p&gt;
&lt;p&gt;In retrospect, &lt;strong&gt;if the early UI design had been changed to a separate configuration page, it would have simplified the operational flow and reduced the complexity of business code.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Third, the core project&amp;rsquo;s dependency on Fluent-Bit made it very difficult to migrate to other open-source components. Combined with Fluent-Bit&amp;rsquo;s high update frequency, the company&amp;rsquo;s security compliance requirements meant our team had to upgrade Fluent-Bit periodically, while also doing regression testing for all configuration options. Adding to this, Fluent-Bit has very poor customizability; while it supports implementing Output plugins in Go, Input plugins can only be written in C. As a result, to collect data from internal applications, we had to use its TCP and HTTP plugins as intermediaries, deploying multiple Agents to collect data from different internal services. This made later integration testing even more difficult.&lt;/p&gt;
&lt;p&gt;Overall, Fluent-Bit&amp;rsquo;s performance basically met expectations, but various small bugs (for example, the pgsql plugin would block the entire process when the target was unreachable) were not taken seriously by the open-source community maintainers, and the code we submitted to the open-source community was rejected for various reasons. If I had to choose again, I would lean toward using other more extensible open-source components.&lt;/p&gt;
&lt;h2 id="unfamiliarity-with-go-led-to-chaotic-project-structure"&gt;Unfamiliarity with Go Led to Chaotic Project Structure&lt;/h2&gt;
&lt;p&gt;The second challenge the team faced was unfamiliarity with Go. Most of the development members only had Java development experience, so naturally, they wrote Go like Java. Due to the limitations of the framework (Go-Gin), problems arose frequently during development.&lt;/p&gt;
&lt;p&gt;The first problem came from object orientation and dependency inversion. Dependency inversion is not unfamiliar to those using Java Spring, but implementing dependency inversion in Go requires using Interface encapsulation, combined with the Go-Mock library for unit testing. Team members unfamiliar with the language&amp;rsquo;s features often incorrectly encapsulated abstractions, or simply nested functions inside functions, writing &lt;a href="https://zh.wikipedia.org/zh-sg/%E9%9D%A2%E6%9D%A1%E5%BC%8F%E4%BB%A3%E7%A0%81"&gt;spaghetti code&lt;/a&gt;. This fully exposed the fact that most domestic Java programmers have not actually received good OOP training. Engineering practices like unit testing and integration testing are also formalistic. Software quality in most enterprises still relies on manual verification by testers.&lt;/p&gt;
&lt;p&gt;The second problem is that Go discourages over-abstraction. For things like generics and exception handling, you have to repeat trivial code snippets step by step, which causes Sonar static checks to fail often. Inexperienced colleagues would then use various clever tricks to evade static checks. This also demonstrates the necessity of regular code reviews for development teams.&lt;/p&gt;
&lt;p&gt;Fourth, Go is actually a programming language with a less-than-complete community. Many of its frameworks (like the most popular gorm, which is actually a personal project), and tools mature in the Java toolchain like Flyway, need to be replaced by combining multiple open-source projects in Go. So Go is only suitable for developing projects of medium or smaller scale, or for performance-critical platform core components. (Domestically) it isn&amp;rsquo;t suitable for complex business scenarios.&lt;/p&gt;
&lt;h2 id="api-granularity-too-fine-resource-objects-not-properly-abstracted"&gt;API Granularity Too Fine, Resource Objects Not Properly Abstracted&lt;/h2&gt;
&lt;p&gt;In the early days, the team was plagued by management chaos: architecturally, business models weren&amp;rsquo;t properly abstracted, and resource objects were broken into too many small pieces; management-wise, tasks were decomposed too simplistically, with each colleague individually responsible for a module, leading to dedicated APIs designed for every business process, creating heavy maintenance pressure. Fortunately, with few business scenarios, automated testing could ensure interface reliability to some extent.&lt;/p&gt;
&lt;p&gt;At first, when doing automated integration testing, we still used BDD form, writing tests based on business operations. Later, we gradually realized that for this kind of monitoring system, the real user operation logic is actually very simple—what&amp;rsquo;s complex are the exceptions that may arise from different types of data, different Input, and Output configurations. So we switched to data-driven testing, using configuration files to comprehensively test different types of Fluent-Bit configurations.&lt;/p&gt;
&lt;p&gt;In summary, the modifications to Fluent-Bit configuration could actually be implemented entirely with 3~4 broad APIs. In addition to the over-designed flow mentioned earlier, the uncertainty in the early project stages caused developers to over-focus on loose coupling while ignoring maintainability.&lt;/p&gt;
&lt;h2 id="flawed-pipeline-design"&gt;Flawed Pipeline Design&lt;/h2&gt;
&lt;p&gt;Initially, the project followed the integration testing and deployment pattern of other teams in the department, putting Python-written test cases and project deployment scripts in a separate Gitlab repo. The result was that every time the project was deployed, someone had to manually go to a webpage to modify the version number to trigger the pipeline. From a continuous integration perspective, having business code and test cases separated meant that every commit had to be submitted to a different repo, and in case of conflicts, multiple integration tests had to be run separately (long time, slow feedback).&lt;/p&gt;
&lt;p&gt;Later, we made some adjustments, merging multiple small modules into a &lt;a href="https://zh.wikipedia.org/wiki/Monorepo"&gt;Monorepo&lt;/a&gt;, while putting some API-related integration tests inside the backend code to reduce the number of commits and make atomic commits easier.&lt;/p&gt;
&lt;p&gt;However, the deployment problem remained unresolved, because there were too many modules on the edge platform, system integration required cooperation from multiple teams, deployment and release cycles were long, and there were too many points of failure. For this situation, the department&amp;rsquo;s technical lead set strict processes for code submission, testing, review, and documentation updates, but the root problem was still ambiguous team responsibilities, the department&amp;rsquo;s teams spanning multiple countries and time zones, and the lack of a unified scheduling and communication mechanism. These problems can only be gradually alleviated by management, or as the business converges, reducing and diverting project teams.&lt;/p&gt;
&lt;h2 id="summary"&gt;Summary&lt;/h2&gt;
&lt;p&gt;Overall, many of the problems our team encountered stemmed from a lack of project and technical team management experience in the project&amp;rsquo;s early stages. Not understanding the business vision, they brought their experience in making consumer-facing SaaS products to the industrial sector, applying familiar development paradigms to manufacturing. Of course, to be honest, on the business side, the department has many long processes, and business leaders can only feel around blindly; user feedback has to first reach the Support team, then be reported upward, and only finally reach the development team. This meant that the products we developed took at least 3-6 months to receive effective feedback. Iteration cycles were too long, and R&amp;amp;D worked in isolation.&lt;/p&gt;</description></item><item><title>Notes on the RESTful Web Services Cookbook</title><link>https://sund.site/en/posts/2024/restful-api-cookbook/</link><pubDate>Sat, 13 Jul 2024 16:12:34 +0800</pubDate><guid>https://sund.site/en/posts/2024/restful-api-cookbook/</guid><description>&lt;p&gt;&lt;a href="https://www.oreilly.com/library/view/restful-web-services/9780596809140/"&gt;RESTful Web Services Cookbook&lt;/a&gt; is a short, concise guide to designing RESTful APIs. This post (notes) records the key points from the book.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Since RESTful conventions are second nature to most backend developers, I will skip the well-known parts and focus on the details in the book that many developers tend to overlook.&lt;/p&gt;&lt;/blockquote&gt;
&lt;h2 id="http-methods"&gt;HTTP Methods&lt;/h2&gt;
&lt;h3 id="get"&gt;GET&lt;/h3&gt;
&lt;p&gt;Performs &lt;strong&gt;safe&lt;/strong&gt; and &lt;strong&gt;idempotent&lt;/strong&gt; retrieval of information.&lt;/p&gt;
&lt;h3 id="post"&gt;POST&lt;/h3&gt;
&lt;p&gt;The target of execution is a collection of resources (a factory), not a specific URI.&lt;/p&gt;
&lt;p&gt;Use cases:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create a new resource by treating a resource as a factory.&lt;/li&gt;
&lt;li&gt;Modify one or more resources through a controller resource.&lt;/li&gt;
&lt;li&gt;Execute queries that require a large data input (many parameters).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;When no other HTTP method seems appropriate, perform an unsafe or non-idempotent operation.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Approach:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Designate an existing resource as the factory for creating new resources. Although any resource can be used as a factory, the common practice is to use a collection resource.&lt;/li&gt;
&lt;li&gt;Have the client submit a POST request to the factory resource, attaching a representation of the resource to be created. Through the optional &lt;strong&gt;Slug&lt;/strong&gt; header, the client can suggest a name to the server as part of the URI of the created resource.&lt;/li&gt;
&lt;li&gt;After the resource is created, return response code &lt;strong&gt;201 (Created)&lt;/strong&gt; and include the URI of the new resource in the &lt;strong&gt;Location&lt;/strong&gt; header.&lt;/li&gt;
&lt;li&gt;If the response body contains a full representation of the new resource, include the URI of the new resource in the &lt;strong&gt;Content-Location&lt;/strong&gt; header.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="put"&gt;PUT&lt;/h3&gt;
&lt;p&gt;Only use PUT to create a new resource when the client controls the structure of the URI. &lt;strong&gt;In other words, PUT can also create a resource, but only when the client specifies the URI.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="determining-the-granularity-of-resource-objects"&gt;Determining the Granularity of Resource Objects&lt;/h2&gt;
&lt;p&gt;Resources should be designed to match the client&amp;rsquo;s usage patterns, not based on existing database or object models.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cacheability&lt;/li&gt;
&lt;li&gt;Reduce modification frequency&lt;/li&gt;
&lt;li&gt;Mutability — separate mutable from immutable data&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="how-to-design-composite-resources"&gt;How to Design Composite Resources?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Composite resources&lt;/strong&gt; reduce the visibility of the uniform interface because their representations contain data that overlaps with other resources.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If composite resources are used &lt;strong&gt;infrequently&lt;/strong&gt;, consider using &lt;strong&gt;caching&lt;/strong&gt; instead.&lt;/li&gt;
&lt;li&gt;Consider the network overhead — would a composite resource reduce server throughput and increase latency?&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="http-body"&gt;HTTP Body&lt;/h2&gt;
&lt;p&gt;Taking a JSON body as an example:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;It is best to include a self-referential link.&lt;/li&gt;
&lt;li&gt;If the results are paginated, it is best to include a link to the next page.&lt;/li&gt;
&lt;li&gt;If the results are paginated, indicate the size of the collection (the total).&lt;/li&gt;
&lt;li&gt;If the queried object is localized, add a property to indicate the language of the localized content.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;John&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;urn:example:user:1234&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;link&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;rel&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;self&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;href&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;http://www.example.org/person/john&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;address&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;urn:example:address:4567&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;link&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;rel&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;self&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;href&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;http://www.example.org/person/john/address&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="http-response"&gt;HTTP Response&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;For client errors, return a 4xx status code plus a Date (the time the error occurred).&lt;/li&gt;
&lt;li&gt;For server errors, return a 5xx status code plus a Date (the time the error occurred).&lt;/li&gt;
&lt;li&gt;The body should describe the error. If there are external documents and links for reference, provide a Link header or include the link directly in the body.&lt;/li&gt;
&lt;li&gt;To support later tracing and analysis, errors are logged on the server. Provide an identifier or link that can be used to locate the error.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="designing-the-query-structure"&gt;Designing the Query Structure&lt;/h2&gt;
&lt;h3 id="designing-query-requests"&gt;Designing Query Requests&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;To improve caching and performance, try to avoid range queries. Workarounds include:
&lt;ul&gt;
&lt;li&gt;Use predefined queries&lt;/li&gt;
&lt;li&gt;Alternatively, use the HTTP header: Range&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Avoid using general-purpose query languages (SQL, XPATH).&lt;/li&gt;
&lt;li&gt;Avoid tight coupling between the URI and the underlying data storage (treating the backend as a database on the front end).&lt;/li&gt;
&lt;li&gt;For requests with many parameters, consider using POST (since URIs have a maximum length)
&lt;ul&gt;
&lt;li&gt;The downside of a POST interface is that it loses caching ability&lt;/li&gt;
&lt;li&gt;POST requests are not cacheable, so the Cache-Control and Expires headers are useless&lt;/li&gt;
&lt;li&gt;To solve the caching problem, have the POST create a temporary resource, return the link to the client, and let the client use GET to fetch that resource next time&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="designing-query-response-results"&gt;Designing Query Response Results&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Return a collection. Add appropriate cache expiration headers.&lt;/li&gt;
&lt;li&gt;If there are no results, return an &lt;strong&gt;empty collection&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;</description></item><item><title>How to Design an Industry-Standard Audit System</title><link>https://sund.site/en/posts/2024/audit-system-design/</link><pubDate>Mon, 15 Apr 2024 16:44:40 +0800</pubDate><guid>https://sund.site/en/posts/2024/audit-system-design/</guid><description>&lt;p&gt;An audit trail is a service within a system that records critical security information such as user behavior logs and control component activity logs. Logs are typically arranged in chronological order, recording &amp;ldquo;who did what and when.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Below is the Kubernetes official documentation&amp;rsquo;s description of its audit service:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Kubernetes auditing provides a security-relevant, time-ordered set of records documenting the sequence of activities that affected the system by individual users, by applications using the Kubernetes API, and by the control plane itself.&lt;/p&gt;
&lt;p&gt;The audit feature enables cluster administrators to answer the following questions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What happened?&lt;/li&gt;
&lt;li&gt;When did it happen?&lt;/li&gt;
&lt;li&gt;Who initiated it?&lt;/li&gt;
&lt;li&gt;On what (which) objects did the activity occur?&lt;/li&gt;
&lt;li&gt;Where was it observed?&lt;/li&gt;
&lt;li&gt;Where was it initiated from?&lt;/li&gt;
&lt;li&gt;What are the subsequent actions taken on the activity?&lt;/li&gt;
&lt;/ul&gt;&lt;/blockquote&gt;
&lt;h2 id="what-capabilities-should-an-audit-system-have"&gt;What Capabilities Should an Audit System Have?&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Log content is tamper-proof.&lt;/li&gt;
&lt;li&gt;Log chain structure is complete: individual log entries cannot be arbitrarily added or removed.&lt;/li&gt;
&lt;li&gt;Compatibility: clients sending logs should avoid invasive designs.&lt;/li&gt;
&lt;li&gt;The system&amp;rsquo;s encryption service should be initialized as early as possible to reduce unprotected log exposure.&lt;/li&gt;
&lt;li&gt;Service restart/shutdown should not cause audit log inconsistency. If a service is shut down under emergency conditions, the audit logs should remain verifiable.&lt;/li&gt;
&lt;li&gt;Key security: encryption keys (used to compute integrity checks) should be stored in a dedicated key store and reside in memory for the shortest possible time.&lt;/li&gt;
&lt;li&gt;Performance: ability to verify protected logs within seconds.&lt;/li&gt;
&lt;li&gt;Log rotation friendliness: audit logs should be compatible with typical log rotation strategies of distributed systems.&lt;/li&gt;
&lt;li&gt;Observability: logs should be easily parsed (machine-readable) and human-readable. Compatible with mainstream log processor formats, with dimensions designed to facilitate future filtering and screening.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="related-industry-standards"&gt;Related Industry Standards&lt;/h2&gt;
&lt;p&gt;Common industry standards related to auditing include IEC 62443 and NIST SP 800-92. Below are the audit-related sections in IEC.&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Industry Standard&lt;/th&gt;
 &lt;th&gt;Section&lt;/th&gt;
 &lt;th&gt;Security Level&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;IEC 62443-4-2:2019&lt;/td&gt;
 &lt;td&gt;CR2.8&lt;/td&gt;
 &lt;td&gt;SL-C 1&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;IEC 62443-4-2:2019&lt;/td&gt;
 &lt;td&gt;CR6.1&lt;/td&gt;
 &lt;td&gt;SL-C 1&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;IEC 62443-4-2:2019&lt;/td&gt;
 &lt;td&gt;CR6.2&lt;/td&gt;
 &lt;td&gt;SL_C 2&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;IEC 62443-4-2:2019&lt;/td&gt;
 &lt;td&gt;CR1.13&lt;/td&gt;
 &lt;td&gt;SL_C 1&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;IEC 62443-4-2:2019&lt;/td&gt;
 &lt;td&gt;CR2.9&lt;/td&gt;
 &lt;td&gt;SL_C 1&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;IEC 62443-4-2:2019&lt;/td&gt;
 &lt;td&gt;CR2.10&lt;/td&gt;
 &lt;td&gt;SL_C 1&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;IEC 62443-4-2:2019&lt;/td&gt;
 &lt;td&gt;CR3.7&lt;/td&gt;
 &lt;td&gt;SL_C 1&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;IEC 62443-4-2:2019&lt;/td&gt;
 &lt;td&gt;CR3.9&lt;/td&gt;
 &lt;td&gt;SL_C 2&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="what-protocols-or-standards-should-audit-log-format-follow"&gt;What Protocols or Standards Should Audit Log Format Follow?&lt;/h2&gt;
&lt;p&gt;For locally running software, Syslog typically has better system compatibility. For projects using ELK to collect logs, CEF is more suitable. In other cases, custom JSON is recommended.&lt;/p&gt;
&lt;p&gt;Below is a comparison of the three formats (protocols).&lt;/p&gt;
&lt;h3 id="common-event-format-cef"&gt;&lt;a href="https://docs.elastic.co/en/integrations/cef"&gt;Common Event Format (CEF)&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;A log format used by Elastic-Search, designed based on event-sourcing principles. The advantage is less redundant information, suitable for building monitoring systems in conjunction with the ELK stack. Its transport is based on the Syslog protocol while extending readable key-value pairs. The text-based design also allows CEF-format logs to be written to files. Overall, it is the most balanced of these formats in terms of readability, efficiency, and standardization.&lt;/p&gt;
&lt;h3 id="syslog"&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc5424"&gt;Syslog&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Syslog is the default audit log format for Linux operating systems, typically using its RFC 5424 version. Most SIEM&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt; systems support importing this format.
The Syslog protocol has great adaptability, and mTLS-based Syslog transport can maximize system security while remaining compatible with traditional software. However, for microservices, implementing and maintaining the standard protocol is costly. Therefore, services like AWS CloudTrail and OpenTelemetry have opted for the simpler HTTPS + JSON format.&lt;/p&gt;
&lt;h3 id="json-lines"&gt;&lt;a href="https://jsonlines.org/"&gt;JSON Lines&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Most SaaS products use JSON—it&amp;rsquo;s simple and efficient. JSON has the characteristic of more redundant information, but the structure is easy to parse. For example, below are the fields in the log model mentioned in the &lt;a href="https://opentelemetry.io/docs/specs/otel/logs/data-model/"&gt;OpenTelemetry official documentation&lt;/a&gt;:&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Field Name&lt;/th&gt;
 &lt;th&gt;Description&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;Timestamp&lt;/td&gt;
 &lt;td&gt;Time when the event occurred.&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;ObservedTimestamp&lt;/td&gt;
 &lt;td&gt;Time when the event was observed.&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;TraceId&lt;/td&gt;
 &lt;td&gt;Request trace id.&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;SpanId&lt;/td&gt;
 &lt;td&gt;Request span id.&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;TraceFlags&lt;/td&gt;
 &lt;td&gt;W3C trace flag.&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;SeverityText&lt;/td&gt;
 &lt;td&gt;The severity text (also known as log level).&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;SeverityNumber&lt;/td&gt;
 &lt;td&gt;Numerical value of the severity.&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Body&lt;/td&gt;
 &lt;td&gt;The body of the log record.&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Resource&lt;/td&gt;
 &lt;td&gt;Describes the source of the log.&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Attributes&lt;/td&gt;
 &lt;td&gt;Additional structured information.&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="what-security-requirements-apply-to-audit-logs"&gt;What Security Requirements Apply to Audit Logs?&lt;/h2&gt;
&lt;p&gt;For audit logs, security requirements are higher than for general log systems.&lt;/p&gt;
&lt;p&gt;Security can typically be considered from three dimensions: Confidentiality, Integrity, and Availability.&lt;/p&gt;
&lt;h3 id="confidentiality"&gt;Confidentiality&lt;/h3&gt;
&lt;p&gt;Attackers can exploit system security vulnerabilities to obtain special privileges and then view certain audit logs.&lt;/p&gt;
&lt;p&gt;The following measures can be taken:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Encrypt logs: use encryption technology to protect logs, ensuring that only authorized users can access and modify them.&lt;/li&gt;
&lt;li&gt;Access control: restrict access to log-sending and log-receiving interfaces.&lt;/li&gt;
&lt;li&gt;Sensitive information filtering: do not record user sensitive information in logs, such as passwords, certificates, etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="integrity"&gt;Integrity&lt;/h3&gt;
&lt;p&gt;Attackers can exploit system security vulnerabilities to modify or delete certain audit logs.&lt;/p&gt;
&lt;p&gt;In addition to the encryption and access control mentioned above, the following measures can also be taken:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Integrity checks&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;: add hash values to log entries so that any tampering or truncation can be quickly detected during log verification.&lt;/li&gt;
&lt;li&gt;Regular backups: regularly back up logs to prevent attackers from deleting or modifying all log entries.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Log file limitations&lt;/strong&gt;: in addition to limiting the size of log files, it&amp;rsquo;s typically necessary to limit the number of backups, maximum backup days, etc. Below are the parameters in Kubernetes for log file storage:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--audit-log-path&lt;/code&gt; specifies the log file path used to write audit events. If this flag is not specified, the log backend is disabled.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--audit-log-maxage&lt;/code&gt; defines the maximum number of days to retain old audit log files.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--audit-log-maxbackup&lt;/code&gt; defines the maximum number of audit log files to retain.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--audit-log-maxsize&lt;/code&gt; defines the maximum size (in megabytes) of audit log files before rotation.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="availability"&gt;Availability&lt;/h3&gt;
&lt;p&gt;Attackers can attack the audit trail service, causing the audit trail service to run out of memory, disk space, etc.&lt;/p&gt;
&lt;p&gt;The audit service should cache audit-related context, such as the mapping between service names and IDs, event IDs and descriptions, etc. When different services send messages to the audit service, the message structure should be designed with minimal length as a principle. The audit service&amp;rsquo;s policy should allow users to configure log levels, filter rules, etc., to reduce system burden.&lt;/p&gt;
&lt;h2 id="log-export"&gt;Log Export&lt;/h2&gt;
&lt;p&gt;In addition to exporting file-format logs, the audit service usually needs to support export to third-party systems. We typically refer to third-party services that analyze and store logs as SIEM (Security Information and Event Management). In Kubernetes, the module that exports logs to third-party web services is called a webhook.&lt;/p&gt;
&lt;p&gt;Exporting to third-party systems can typically use the standard Syslog format or JSON Lines, which has the widest support. Additionally, you need to consider log truncation, and the configuration of third-party systems&amp;rsquo; batch and stream processing. You can refer to &lt;a href="https://kubernetes.io/zh-cn/docs/tasks/debug/debug-cluster/audit/#webhook-backend"&gt;this Kubernetes document&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="architecture-designs-of-open-source-projects"&gt;Architecture Designs of Open-Source Projects&lt;/h2&gt;
&lt;p&gt;Due to different design focuses, each of the following open-source projects needs careful consideration of its advantages and disadvantages, whether its features meet your needs, and whether the system environment is distributed or monolithic.&lt;/p&gt;
&lt;h3 id="auditd"&gt;Auditd&lt;/h3&gt;
&lt;p&gt;&lt;figure
 class="image-caption"
&gt;
 
 &lt;img src="https://sund.site/images/audit-system-design/Linux-Auditd-Architecture.png" alt="auditd-architecture" loading="lazy" /&gt;
 
 &lt;figcaption&gt;auditd-architecture&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;The default audit service for most Linux systems, when paired with tools like rsyslog, can solve local device log collection, viewing, and filtering. rsyslog&amp;rsquo;s string template-based log format configuration can meet the integration needs of users using different SIEM systems.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Advantages: process-based communication, standard log format, easy export. Excellent performance.&lt;/li&gt;
&lt;li&gt;Disadvantages: the process model is not suitable for network services.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="aws-cloud-trail-and-kubernetes"&gt;AWS Cloud Trail and Kubernetes&lt;/h3&gt;
&lt;p&gt;&lt;figure
 class="image-caption"
&gt;
 
 &lt;img src="https://sund.site/images/audit-system-design/aws-audit.png" alt="aws log" loading="lazy" /&gt;
 
 &lt;figcaption&gt;aws log&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;AWS CloudTrail adopts a model where application services actively push audit events. Users can set policies for designing tracking services, and the collected logs flow as needed into subsequent batch and stream processing toolchains.&lt;/p&gt;
&lt;p&gt;Kubernetes&amp;rsquo;s log collection is similar to AWS&amp;rsquo;s implementation, also based on a centralized service, but this architecture is not designed solely for audit logs. It follows many Kubernetes declarative design philosophies and is well worth studying.&lt;/p&gt;
&lt;p&gt;&lt;figure
 class="image-caption"
&gt;
 
 &lt;img src="https://sund.site/images/audit-system-design/k8s-audit.png" alt="kubernetes log" loading="lazy" /&gt;
 
 &lt;figcaption&gt;kubernetes log&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;For example, Kubernetes has stages specifically designed for auditing:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Each request can be recorded with its associated stages. The defined stages are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;RequestReceived - The stage corresponds to the event generated when the audit handler receives a request, and before delegating to the remaining handlers.&lt;/li&gt;
&lt;li&gt;ResponseStarted - The event is generated after the response message headers are sent, but before the response message body is sent. Only long-running requests (such as watch) generate this stage.&lt;/li&gt;
&lt;li&gt;ResponseComplete - When the response message body is complete and no more data needs to be transmitted.&lt;/li&gt;
&lt;li&gt;Panic - Generated when a panic occurs.&lt;/li&gt;
&lt;/ul&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;a href="https://kubernetes.io/zh-cn/docs/tasks/debug/debug-cluster/audit/"&gt;Kubernetes audit events&lt;/a&gt; use a different message structure from the Event API&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;In summary, the cloud platform&amp;rsquo;s audit service design can be summarized as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Advantages: microservice design, more flexible JSON format logs, centralized log collection service easily integrates with more application services and exports to open-source data processing tools.&lt;/li&gt;
&lt;li&gt;Disadvantages: distributed architecture requires higher storage, server-side encryption, communication security, and integrity.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="opentelemetry"&gt;OpenTelemetry&lt;/h3&gt;
&lt;p&gt;&lt;figure
 class="image-caption"
&gt;
 
 &lt;img src="https://sund.site/images/audit-system-design/opentel.png" alt="OpenTel" loading="lazy" /&gt;
 
 &lt;figcaption&gt;OpenTel&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;OpenTelemetry is currently the most mainstream logging framework in cloud-native environments. It supports both invasive (SDK) and non-invasive (Agent) log collection modes. The Collector design allows some log processing work to be done on the log sender side.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Advantages: microservice design, supports infrastructure like Kubernetes, multi-language and multi-platform SDK and extensibility. Comprehensive security and integrity considerations. Suitable for small and medium-sized enterprises.&lt;/li&gt;
&lt;li&gt;Disadvantages: in most cases, log collection still requires invasive modification of code inside the app. Log collection tools don&amp;rsquo;t support languages like Go well (as of this writing).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="summary"&gt;Summary&lt;/h2&gt;
&lt;p&gt;An audit trail refers to the time-ordered records of all operations or events affecting the system, used to track system activity and verify whether violations have occurred.&lt;/p&gt;
&lt;p&gt;Audit logs should have the following characteristics:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tamper-proof (encrypted storage, integrity verification)&lt;/li&gt;
&lt;li&gt;High performance (fast verification)&lt;/li&gt;
&lt;li&gt;Observability (machine/human readable)&lt;/li&gt;
&lt;li&gt;Security (confidentiality, availability, integrity)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Common audit log formats include Syslog, CEF, and JSON, with the main differences being redundant information, readability, and compatibility with log collection systems.&lt;/p&gt;
&lt;p&gt;Audit logs have high security requirements:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Confidentiality: only authorized users can access, achieved through access control&lt;/li&gt;
&lt;li&gt;Availability: prevent deletion or destruction by attackers, achieved through resource limits, multi-replica storage, etc.&lt;/li&gt;
&lt;li&gt;Integrity: prevent tampering or truncation, achieved through encryption, integrity verification, etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Some typical audit log system architectures:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Native Linux log programs such as Auditd, rsyslog&lt;/li&gt;
&lt;li&gt;Cloud products like AWS&lt;/li&gt;
&lt;li&gt;OpenTelemetry&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;SIEM stands for Security Information and Event Management. &lt;a href="https://www.microsoft.com/en-us/security/business/security-101/what-is-siem"&gt;https://www.microsoft.com/en-us/security/business/security-101/what-is-siem&lt;/a&gt;&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;For log encryption, the server typically adds an additional checksum chain to logs for verification. You can refer to Amazon&amp;rsquo;s implementation of &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingServerSideEncryption.html"&gt;server-side encryption (SSE-S3)&lt;/a&gt;.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;&lt;a href="https://kubernetes.io/zh-cn/docs/reference/config-api/apiserver-audit.v1/#audit-k8s-io-v1-Event"&gt;Kubernetes audit event structure definition&lt;/a&gt;&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item></channel></rss>