惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Threatpost
aimingoo的专栏
aimingoo的专栏
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
T
Tailwind CSS Blog
J
Java Code Geeks
博客园_首页
Google Online Security Blog
Google Online Security Blog
Hugging Face - Blog
Hugging Face - Blog
C
CXSECURITY Database RSS Feed - CXSecurity.com
I
Intezer
P
Palo Alto Networks Blog
V
Vulnerabilities – Threatpost
雷峰网
雷峰网
O
OpenAI News
SecWiki News
SecWiki News
小众软件
小众软件
酷 壳 – CoolShell
酷 壳 – CoolShell
美团技术团队
N
News | PayPal Newsroom
Project Zero
Project Zero
Forbes - Security
Forbes - Security
IT之家
IT之家
A
Arctic Wolf
WordPress大学
WordPress大学
Jina AI
Jina AI
T
Tor Project blog
博客园 - 三生石上(FineUI控件)
S
Secure Thoughts
Google DeepMind News
Google DeepMind News
Attack and Defense Labs
Attack and Defense Labs
博客园 - 聂微东
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
P
Privacy International News Feed
Cloudbric
Cloudbric
G
GRAHAM CLULEY
博客园 - 叶小钗
H
Hacker News: Front Page
腾讯CDC
量子位
Help Net Security
Help Net Security
人人都是产品经理
人人都是产品经理
C
Cyber Attacks, Cyber Crime and Cyber Security
月光博客
月光博客
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
宝玉的分享
宝玉的分享
爱范儿
爱范儿
L
Lohrmann on Cybersecurity
Hacker News - Newest:
Hacker News - Newest: "LLM"
Recorded Future
Recorded Future
C
CERT Recently Published Vulnerability Notes

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
How to track API requests in Azure APIM to Table Storage
Sam Vanhoutt · 2026-05-02 · via DEV Community

When you run a mobile API (like us at libelo), you quickly reach a point where you want to understand how people are actually using it. Not just error counts or latency histograms, but the real detail: which operations are called, from which platform, with what location data, at what time. We log all requests with Azure Application Insights, but that is mostly for troubleshooting purposes and comes with sampling risks, and a retention window.

In our backend, I have added an extra option: write every incoming API request directly to Azure Table Storage, right from the APIM policy, in a fire-and-forget way. The cost is minimal, the data is ours to query whenever we like, and the extra latency added to your API responses is zero. And the most important thing is that we are storing this data for further analytics and platform usage insights.

This post walks through how to set it up.

Why Azure Table Storage

Table Storage gives you cheap, durable, schemaless rows with a partition and row key. That is all you need for an audit log. You can partition by date so that querying a single day is fast, you can set a retention policy to purge old data automatically, and you can query it directly from Azure Storage Explorer or export it to Power BI without any extra infrastructure.

The catch is that you need to give APIM a way to authenticate against the Table Storage endpoint. The cleanest option for write-only access from a policy is a Shared Access Signature (SAS) token stored as a Named Value. More on that below.

The send-one-way-request policy

APIM has a policy called send-one-way-request that issues an HTTP request and does not wait for a response before continuing. This is exactly what we want here. The client calling your API never experiences the round-trip to Table Storage, and the policy does not block the outbound flow.

You can place this policy in both the <outbound> section (after a successful response) and the <on-error> section (when the backend or the policy itself fails). Covering both sections means you get a complete picture of every request, regardless of outcome.

Step by step

Setting up Table Storage

First, you need a storage account and a table. Use an existing, if you have one. Otherwise, create a standard StorageV2 account with Standard_LRS redundancy in the same region as your APIM instance.

Inside the storage account, create a table. In the Azure Portal, navigate to your storage account, open the Tables blade, and click + Table. Give it a name like apirequests. That is the table name you will reference in the policy URL.

Generating the SAS token

APIM needs write access to insert new rows. Rather than giving it a full account key, you create a SAS token scoped to the table service with only the permissions it needs.

In the Azure Portal, navigate to your storage account and open the Shared access signature blade. Configure the following settings:

  • Allowed services: Table only
  • Allowed resource types: Object (this covers individual table entities)
  • Allowed permissions: Add (this is the only permission APIM needs to insert rows)
  • Expiry: Set this to something practical for your rotation cadence. One year is common for internal tooling, but make sure you have a process to rotate it before it expires.
  • Allowed protocols: HTTPS only

Click Generate SAS and connection string. Copy the value in the SAS token field. It starts with sv= and will look something like this:

sv=2022-11-02&ss=t&srt=o&sp=a&se=2026-12-31T00:00:00Z&st=2025-01-01T00:00:00Z&spr=https&sig=<signature>

Enter fullscreen mode Exit fullscreen mode

You will use this value in the next step.

Storing the SAS token as a Named Value in APIM

APIM Named Values are the right place to store configuration that policies reference but that should not be hardcoded in the policy XML. They also support Key Vault references if you want to manage the secret lifecycle there.

In the Azure Portal, navigate to your APIM instance and open the Named values blade. Click + Add and fill in the following:

  • Name: tablestorage-sas
  • Display name: tablestorage-sas
  • Type: Secret
  • Value: the SAS token you generated above

Save it. The policy will reference this value as {{tablestorage-sas}}.

Creating a policy fragment

The tracking logic needs to run in two places: the <outbound> section fires after a successful response, and the <on-error> section fires when something goes wrong. Obviously, we don't want to duplicate the entire send-one-way-request block. APIM solves this with policy fragments: reusable snippets that you define once and reference by name from any policy.

The only structural difference between the success and error cases is the ErrorReason field. context.LastError?.Reason is safe to evaluate in both contexts. In the <outbound> path there is no last error, so it returns an empty string. That means the fragment body is identical for both cases, and you do not need any branching inside it.

In the Azure Portal, navigate to your APIM instance, open the APIs section, and click Policy fragments in the left menu. Click + Create and give the fragment the name track-api-request. Paste the following XML as the fragment body:

<fragment>
  <send-one-way-request mode="new" timeout="10">
    <set-url>https://<your-storage-account>.table.core.windows.net/apirequests?{{tablestorage-sas}}</set-url>
    <set-method>POST</set-method>
    <set-header name="Content-Type" exists-action="override">
        <value>application/json</value>
    </set-header>
    <set-header name="Accept" exists-action="override">
        <value>application/json;odata=nometadata</value>
    </set-header>
    <set-header name="x-ms-version" exists-action="override">
        <value>2019-02-02</value>
    </set-header>
    <set-body>
    @{
      var now = DateTime.UtcNow;
      var requestId = context.RequestId;
      var rowKey = (DateTime.MaxValue.Ticks - now.Ticks).ToString("D19")
                  + "_" + requestId;
      return new JObject(
          new JProperty("PartitionKey", now.ToString("yyyy-MM-dd")),
          new JProperty("RowKey", rowKey),
          new JProperty("Timestamp", now),
          new JProperty("OperationId", requestId),
          new JProperty("Operation", context.Operation.Name),
          new JProperty("Path", context.Request.Url.Path),
          new JProperty("Query", context.Request.Url.QueryString),
          new JProperty("UserId", context.Request.Headers.GetValueOrDefault("app-user-id","")),
          new JProperty("UserName", context.Request.Headers.GetValueOrDefault("app-user-name","")),
          new JProperty("Lat", context.Request.Headers.GetValueOrDefault("x-lat","")),
          new JProperty("Lon", context.Request.Headers.GetValueOrDefault("x-lon","")),
          new JProperty("Language", context.Request.Headers.GetValueOrDefault("Accept-Language","")),
          new JProperty("Platform", context.Request.Headers.GetValueOrDefault("x-platform","")),
          new JProperty("Status", context.Response?.StatusCode ?? 0),
          new JProperty("Ip", context.Request.IpAddress),
          new JProperty("ErrorReason", context.LastError?.Reason ?? "")
      ).ToString();
    }
    </set-body>
  </send-one-way-request>
</fragment>

Enter fullscreen mode Exit fullscreen mode

A few things worth pointing out in the body expression.

The RowKey.

The row key is constructed by subtracting the current ticks from DateTime.MaxValue.Ticks and zero-padding the result to 19 digits, then appending the request ID. Table Storage sorts rows alphabetically within a partition, so this descending tick value means that when you open a day's partition in Storage Explorer or query it via the REST API, the most recent requests appear at the top. Without this trick you would have to sort results in the client.

The PartitionKey.

Using the date in yyyy-MM-dd format keeps each day's data in its own partition. Queries scoped to a single day will only scan that partition, which keeps them fast even when the table accumulates months of data.

The fields.

The policy captures the APIM operation name, the full path and query string, user identity fields that were extracted from the JWT earlier in the inbound policy, device location and platform headers sent by the client, the HTTP status code from the response, and the client IP address. The ErrorReason field is populated from context.LastError?.Reason, which returns an empty string on the success path and the actual failure reason on the error path.

The Accept header.

The value application/json;odata=nometadata tells the Table Storage REST API to return the minimal OData envelope. For a write operation this does not change the response, but it is good practice and avoids any surprises if you later add response handling.

The APIM policy

With the fragment in place, the policy itself becomes lightweight. Both sections reference the fragment by name using <include-fragment>, and APIM expands it at runtime:

<outbound>
    <base />
    <include-fragment fragment-id="track-api-request" />
</outbound>
<on-error>
    <base />
    <include-fragment fragment-id="track-api-request" />
</on-error>

Enter fullscreen mode Exit fullscreen mode

This is the policy you attach at the API or product level. The fragment handles all the tracking logic, and any future change to what gets recorded only needs to happen in one place.

Deploying with Bicep

If you are managing your APIM configuration with Bicep (or azd), the policy fragment maps to a Microsoft.ApiManagement/service/policyFragments child resource. Define the fragment body as a variable, substitute the storage account name at deploy time, and declare the resource as a dependency of any API that uses it:

var trackApiRequestFragment = '''
<fragment>
  <send-one-way-request mode="new" timeout="10">
    <set-url>https://__STORAGE_ACCOUNT_NAME__.table.core.windows.net/apirequests?{{tablestorage-sas}}</set-url>
    ...
  </send-one-way-request>
</fragment>
'''

resource apimService 'Microsoft.ApiManagement/service@2023-05-01-preview' existing = {
  name: apiManagementName
}

resource trackApiRequestPolicyFragment 'Microsoft.ApiManagement/service/policyFragments@2023-05-01-preview' = {
  name: 'track-api-request'
  parent: apimService
  properties: {
    description: 'Tracks every API request to Table Storage in a fire-and-forget way'
    format: 'rawxml'
    value: replace(trackApiRequestFragment, '__STORAGE_ACCOUNT_NAME__', storageAccount.name)
  }
}

Enter fullscreen mode Exit fullscreen mode

The main API policy then only needs the two <include-fragment> references, and the storage account name placeholder is resolved without touching the policy string itself. Storing the SAS token as a Named Value can be linked with an Azure KeyVault secret. The {{tablestorage-sas}} placeholder in the fragment simply needs to match the name of the Named Value you created in the portal.

What you end up with

Once this is running, every API request that passes through APIM writes a row to the apirequests table. You can open Azure Storage Explorer and browse by date partition, export a day's worth of data to CSV, or query directly via the Table Storage REST API. For more structured analysis you can connect the table to Power BI or Azure Data Factory.

The setup has been running in production on libelo without any noticeable overhead on API response times, and the cost for the storage itself is negligible at consumer app scale. It gives a lightweight, always-on audit trail that complements Application Insights rather than replacing it, and it is easy to extend by adding fields to the JSON body whenever you need to track something new.

The main thing to watch out for is the SAS token expiry. Set a calendar reminder to rotate it before the expiry date, or move the secret into Key Vault and reference it from the Named Value so that rotation becomes a Key Vault operation rather than a manual portal step.

And as a result we are now having a nice overview of the requests in our own platform:

Portal overview