Introduction
Ship logs from your apps and backend into one central panel. Two ways to log: automatic request logging with one line of middleware, and manual event logging where you call the SDK yourself.
pinqloq is a logging service.
You only need a pinqloq account and a secret key to get started.
The SDK runs inside your backend and sends your logs to the panel.
Before you start
| You need | What to do |
|---|---|
| A pinqloq account | Sign up at pinqloq.pinqponq.io. |
| A project with a secret key | Create a project in the dashboard. Your secret key is generated when you create the project. See Create a project. |
Two ways to log
Automatic request logging
One line of middleware logs every HTTP request. It captures the method, path, status, and duration automatically.
Manual event logging
Call the SDK yourself to log business events, exceptions, HTTP requests, or client events forwarded through your backend. You choose the collection, source, and fields.
Client events
Send events from a client app to pinqloq through your own backend.
A client (phone, browser, or POS device) must never hold the secret key. The client sends events to an endpoint you create on your backend. Your backend then uses the pinqloq .NET SDK to forward those events to pinqloq. The secret key stays on your server.
LogSourceType to Device and use a separate collection. If you also run automatic request logging, exclude this endpoint. Otherwise the same request is logged twice.
How a client log travels
In your client app
The app's only job is to send the event to your backend.
The client never calls pinqloq directly. It posts the event and any extra context to an endpoint on your backend. You decide what to include in the payload.
Example request to your backend
curl -X POST https://your-backend.example.com/logs \
-H "Content-Type: application/json" \
-d '{ "event": "checkout_completed", "orderId": "A-1042", "screen": "cart" }'
Custom logging
Inject IPinqloqLogger and call Enqueue from anywhere in your backend. You build the PinqloqLogEntry yourself and decide which fields to include. Use LogSourceType.Backend for backend events and LogSourceType.Device to forward events from a client app.
Before you start
CollectionName in the SDK.AddPinqloq(...). See SDK configuration.What are you logging?
Use LogSourceType.Backend for any event that originates on your backend: a business event, an exception, a background job result, an HTTP request, or anything else you want to log manually. You can call Enqueue from a controller, a service, a background job, or a middleware.
Detail: RequestMethod, ResponseCode, InputJson, and OutputJson. You can optionally also include RequestHeaders and ResponseHeaders to capture request/response headers, matching what the built-in middleware produces. Missing any of these will not cause an error, but the panel will not be able to render the request details view for that entry.
pinqloqLogger.Enqueue(new PinqloqLogEntry
{
Event = "order_placed",
DeviceIdentifier = "u-9821",
LogLevel = PinqloqLogLevel.Information,
LogSourceType = PinqloqLogSourceType.Backend,
Metadata = new()
{
["userId"] = "u-9821",
["tenantId"] = "t-502"
},
Detail = new()
{
["orderId"] = "A-1042",
["totalAmount"] = "149.99"
}
},
onFailed: (entry, error) => Console.WriteLine($"Log failed: {error.Reason}"),
onSent: entry => Console.WriteLine("Log sent"));
Use LogSourceType.Device when your backend is forwarding an event that originated on a client. The client can be a mobile app, a web app, or any other device. Create a separate collection for these logs so they stay separate from your backend logs. See In your client app for the client-side code.
pinqloqLogger.Enqueue(new PinqloqLogEntry
{
Event = "order_placed",
DeviceIdentifier = "device-8f3a12",
LogLevel = PinqloqLogLevel.Information,
LogSourceType = PinqloqLogSourceType.Device,
CollectionName = "client_logs",
Metadata = new()
{
["battery"] = "72%",
["networkType"] = "wifi"
},
Detail = new()
{
["orderId"] = "A-1042",
["itemCount"] = "3"
}
},
onFailed: (entry, error) => Console.WriteLine($"Log failed: {error.Reason}"),
onSent: entry => Console.WriteLine("Log sent"));
Event is always required. Without it the entry is skipped.
DeviceIdentifier is always required. If the entry leaves it empty, the global DeviceIdentifier option fills it; if both are empty Enqueue throws (and the request-logging middleware rejects the request with HTTP 400).
Date is optional. If not set, it is assigned when the log is queued.
CollectionName is optional. If not set, it falls back to ApiLogsCollectionName from SDK configuration; if both are empty, the server resolves it (single-collection keys only).
PinqloqLogEntry fields
| Field | Type | Default | Description |
|---|---|---|---|
Event | string | required | Log title shown in the panel. Cannot be empty. Entries without it are dropped; the rest of the batch is sent. |
LogLevel | PinqloqLogLevel | Information | Debug, Information, Warning, Error, Fatal. |
LogSourceType | PinqloqLogSourceType | Backend | Backend for backend events, Device for client events forwarded through your backend. |
CollectionName | string? | null | Target collection. Falls back to ApiLogsCollectionName when unset. If neither is set, the server resolves it (single-collection keys only). |
Date | DateTimeOffset? | null | When the event happened. Optional: if not set, the SDK assigns the timestamp when the log is queued. You can set it manually to override it. |
Metadata | Dictionary<string,string>? | null | Short searchable key-value pairs shown in the panel. Keep values brief. |
Detail | Dictionary<string,string>? | null | Larger diagnostic data such as stack traces or full request bodies. |
DeviceIdentifier | string | required | Unique identifier (device, user, or instance). Required — Enqueue throws when it is empty and no global fallback is set; the request-logging middleware rejects the request with HTTP 400. Falls back to the global DeviceIdentifier option when left empty. |
AppVersionName | string? | null | Optional version label. Falls back to the global AppVersionName option when unset; if that is also unset, the SDK sends null (not "") and the server stores it as absent. |
Which method to call
| Method | Delivery | Use it for |
|---|---|---|
Enqueue(entry) | Buffered, batched | Use for most cases. Non-blocking; returns false if the queue is full. Pass an optional onFailed callback to react to drops or rejections. See Delivery callbacks. |
Enqueue(entries) | Buffered, batched | Same as above, but queues a whole list in one call. Returns the number of entries accepted into the queue. |
Built-in middleware
One line, a fixed template, every request logged.
Call UsePinqloqRequestLogging() once in Program.cs
to log every request. The SDK fills in all fields automatically.
Use built-in pinqloq middleware when you want zero per-request code and complete coverage of every HTTP request with no extra effort. It is the right choice for most applications.
Enqueue directly at the point where you already have the data you need.
Before you start
ApiLogsCollectionName.ApiLogsCollectionName in SDK configurationvar app = builder.Build();
app.UsePinqloqRequestLogging();
ExcludePaths. Matching is segment-based and case-insensitive: /api matches /api/orders but not /apixyz.app.UsePinqloqRequestLogging(options =>
{
options.ExcludePaths("/health", "/swagger", "/api/client-events");
});
Advanced usage
Use AddMetadata or AddDetail to add custom fields to your logs. Each callback receives the HttpContext and runs once per request. If a callback returns null or throws, that key is skipped. Your keys do not overwrite built-in fields. The only exception is event (the panel title). It defaults to "{method} {path}" and lives in Metadata. To override it, use AddMetadata("event", …).
Use SetDeviceIdentifier to resolve the required DeviceIdentifier per request (for example from a claim, header, or trace id). Use SetAppVersionName to set the version label per request. Both fall back to the matching global option (DeviceIdentifier / AppVersionName) when the callback returns null or empty.
app.UsePinqloqRequestLogging(options =>
{
// Optional: the middleware already reads the "device-identifier" header. Use SetDeviceIdentifier only to override it.
options.SetDeviceIdentifier(ctx => ctx.User.FindFirst("sub")?.Value);
// Optional per-request version label — falls back to the global AppVersionName option
options.SetAppVersionName(ctx => ctx.Request.Headers["X-App-Version"]);
options.AddMetadata("userId", ctx => ctx.User.FindFirst("sub")?.Value);
options.AddMetadata("correlationId", ctx => ctx.Request.Headers["X-Correlation-Id"]);
options.AddDetail("userAgent", ctx => ctx.Request.Headers.UserAgent);
// Override the default event title (metadata["event"])
options.AddMetadata("event", ctx => ctx.Request.Path);
});
DeviceIdentifier in this order: SetDeviceIdentifier (overrides everything), then the device-identifier request header, then the global DeviceIdentifier option. If none of these yield a value, the request is rejected with HTTP 400 before it runs — nothing is logged for it. Guarantee a value with SetDeviceIdentifier(ctx => ctx.Request.Headers["device-identifier"].FirstOrDefault() ?? Environment.MachineName) or a global DeviceIdentifier.
What gets logged
Each request becomes one log. The Event field (the log title) is set to "{method} {path}" (e.g. GET /api/orders/42). The level comes from the response status. Request details go into Metadata:
| Status | Level | Metadata |
|---|---|---|
| 2xx / 3xx | Information | event, method, path, statusCode, durationMs |
| 4xx | Warning | |
| 5xx | Error |
Request and response bodies go into Detail (RequestMethod, ResponseCode, InputJson, OutputJson), truncated at 32 KB. Bodies reach the panel — exclude paths that carry secrets or PII. Request and response headers can also be captured, as RequestHeaders and ResponseHeaders.
Create a project
The dashboard is at pinqloq.pinqponq.io. Create a project here to get your secret key.
Create a collection
A collection is a named bucket where logs are stored in the panel. You need at least one before you can send logs.
ApiLogsCollectionName, or set it per log entry as CollectionName.
Collections can be edited, disabled, or deleted from the dashboard. A key can write only to
collections allowed for its project; logging to another collection returns 403 Forbidden.
Dashboard panel
After the SDK starts sending logs, use the dashboard panel to read them, manage who can see them, and control active panel sessions.
View logs
ApiLogsCollectionName or sent on each log entry.Manage panel users
| Action | How it works |
|---|---|
| Invite user | Open Team Members, choose Invite User, enter the email, set an initial password, and select the collections this user can access. Passwords must be at least 8 characters. |
| Set admin password | For an admin or owner account, use Edit to set or reset the panel password. This gives the admin access to the live log panel. |
| Edit permissions | For regular users, use Edit to change collection access. Users can only see the collections selected for them. |
| Deactivate user | Use the active/inactive badge to disable a non-admin user without deleting the account. Admin and owner accounts cannot be disabled from this control. |
| Delete user | Remove a non-admin user when they should no longer have panel access. Admin and owner accounts are protected from deletion in this screen. |
Sessions and devices
Each panel user can have up to 3 active sessions. A session is shown as an
active device in the user's Devices panel. If a user reaches the
session limit, remove an old device to free a slot before signing in again.
Plan limits
| Limit | Included usage |
|---|---|
| Projects | You can create as many projects as you need. |
| Collections | 2 collections are free. Additional collections are paid. |
| Retention | Logs are kept for 7 days for free in each collection. Keeping logs for more than 7 days is paid. |
| Panel sessions | Each panel user can have up to 3 active sessions. |
The dashboard is used to create projects, collections, keys, and panel users. The log panel at pinqloq-panel.pinqponq.io is where those users sign in to view logs.
MCP server
Ask an AI assistant about your logs instead of opening the panel. The MCP server connects Claude, Cursor, or any other Model Context Protocol (MCP) client to your logs.
The MCP server is read-only. It reads your logs through the same panel API the log panel itself uses. It cannot write logs, and it cannot change any project, collection, or user in your dashboard.
What you can ask
| Tool | What it does |
|---|---|
get_logs | Fetch logs. Filter by collection, level, device, app version, or time range. |
search_logs | Search logs by text, across the identifier, metadata, and detail fields. |
get_collections | List the collections your key can access. |
get_error_summary | Group the errors in a time range and show which ones happen most. |
Example prompts
Ask in plain language. The assistant picks the tool and the arguments for you.
| You ask | What runs |
|---|---|
| "Show me today's errors" | get_logs(level="error", startTime="today") |
| "What's failing the most in the last hour?" | get_error_summary(timeRange="1h") |
| "Search the logs for NullReferenceException" | search_logs(query="NullReferenceException") |
| "Which collections can I see?" | get_collections() |
search_logs or get_error_summary to find the failing log entry, reads the stack trace in its detail field, opens the matching file in your project, and makes the fix. The same assistant session that reads your logs also has your code open.
Before you start
MCP Key tab to generate one company-wide key, or open Users to generate a key for one member instead.secret_key header.{
"mcpServers": {
"pinqloq": {
"url": "https://pinqloq-mcp.pinqponq.io/mcp",
"headers": { "secret_key": "<your-mcp-key>" }
}
}
}
MCP Key tab, reaches every collection in the company. Give it only to admins.
A member key, generated from Users, reaches only the collections that member is allowed. These are the same collections they see when they sign in to the log panel themselves. If a member's collection access changes, their key updates with it.
SDK configuration
Add the pinqloq NuGet package, then register the SDK once in Program.cs.
dotnet add package pinqloq
SecretKey is required. Set ApiLogsCollectionName when you use the built-in pinqloq middleware or want a default collection. Logs without a CollectionName fall back to this value. Set DeviceIdentifier as the global fallback for the required per-log DeviceIdentifier field, so both middleware and manual logs ship with one.
builder.Services.AddPinqloq(options =>
{
options.SecretKey = builder.Configuration["Pinqloq:SecretKey"]!;
options.ApiLogsCollectionName = builder.Configuration["Pinqloq:ApiLogsCollectionName"]!;
options.DeviceIdentifier = Environment.MachineName;
options.AppVersionName = builder.Configuration["Pinqloq:AppVersionName"];
options.BatchSize = 200;
options.FlushInterval = TimeSpan.FromSeconds(2);
options.QueueCapacity = 10000;
options.HttpTimeout = TimeSpan.FromSeconds(10);
});
Add a matching Pinqloq section to appsettings.json so these values resolve at runtime (see From appsettings.json below).
Options
| Option | Type | Default | Description |
|---|---|---|---|
| SecretKey | string | required | Your API secret, sent as the X-Secret-Key header. |
| ApiLogsCollectionName | string? | null | Collection for the built-in pinqloq middleware and for logs without a CollectionName. Optional for single-collection keys; required when the key allows several. |
| DeviceIdentifier | string? | null | Global fallback for the required per-log DeviceIdentifier, applied to any log (middleware or manual) that doesn't set its own. Set this or use SetDeviceIdentifier, otherwise the server rejects logs with an empty DeviceIdentifier. |
| AppVersionName | string? | null | Optional version label, applied to any log that doesn't set its own. |
| BatchSize | int | 200 | Max logs per batch. |
| FlushInterval | TimeSpan | 2s | Max wait before a partial batch is sent. |
| QueueCapacity | int | 10000 | In-memory queue size. When full, new logs are dropped. |
| HttpTimeout | TimeSpan | 10s | Per-request HTTP timeout. |
From appsettings.json
Bind the options from configuration to keep the secret out of source code:
{
"Pinqloq": {
"SecretKey": "lgl_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"ApiLogsCollectionName": "myapp_api_logs",
"AppVersionName": "1.0.0"
}
}
builder.Services.AddPinqloq(options =>
builder.Configuration.GetSection("Pinqloq").Bind(options));
Delivery callbacks
Enqueue takes two optional callbacks. Pass only the ones you need. Most code only uses onFailed:
With Enqueue (fire and forget, batched in the background):
pinqloqLogger.Enqueue(entry,
onFailed: (failedEntry, error) =>
_logger.LogError(error.Exception,
"Pinqloq log failed: {Reason} ({Status}) - {Message}",
error.Reason, error.StatusCode, error.Message),
onSent: sentEntry => { /* optional: metrics, tracing, etc. */ });
onSent runs once the log reaches the server. onFailed runs when the log is dropped or rejected. If the in-memory queue is full, onFailed fires immediately with reason QueueFull and Enqueue returns false.
onFailed gives you a PinqloqLogError:
| Field | Type | Description |
|---|---|---|
| Reason | PinqloqLogFailureReason | Why it failed: Unauthorized, Forbidden, MissingCollection, QueueFull, HttpError, Timeout, Network, or Unknown. |
| StatusCode | int? | The HTTP status when the server responded; null for non-HTTP failures. |
| Message | string | A plain-text description of what went wrong. Always present. |
| Exception | Exception? | The underlying exception when one was thrown; otherwise null. |
Production notes
| Do | Why |
|---|---|
| Separate environments | Use different collections and keys for prod and dev so test data stays out of production. |
| Don't log secrets or PII | Strip passwords, tokens, auth headers and personal data. Mask anything you must keep. |
| Trim large values | Truncate big request/response bodies. Oversized batches are rejected. |
| Use Enqueue for high volume | Non-blocking and batched. It is the right choice for most logging scenarios. |
Types & enums
A PinqloqLogEntry uses two enums. In C# you pass the enum value. On the raw HTTP payload, logLevel is a number and logSourceType is a string.
PinqloqLogLevel | When to use |
|---|---|
| Debug | Verbose diagnostics. |
| Information | Normal flow (the default). |
| Warning | Recoverable issues. |
| Error | Failures. |
| Fatal | Unrecoverable failures. |
PinqloqLogSourceType | Meaning |
|---|---|
| Backend | The log originated on your backend. Use this for backend events (the default). |
| Device | The log originated on a client/device. Set this for client logging. |
On the raw payload, logLevel is the number 1–5 and logSourceType is the string "Backend" / "Device" (case-insensitive).
Troubleshooting
| Symptom | Cause & fix |
|---|---|
| 401 Unauthorized | Missing or wrong secret key. Check SecretKey and the value you copied from the dashboard. |
| 403 Forbidden | The collection isn't allowed for your key. The SDK drops logs for that collection and writes one throttled error to your app logs; other collection groups continue. Add the collection to your project, or log to an allowed one. |
| 400 Bad Request | The key allows multiple collections but no collection name was sent, logSourceType is invalid, or a required field (Event / DeviceIdentifier) is empty. Set ApiLogsCollectionName or a per-entry CollectionName, and make sure every log has an Event and a DeviceIdentifier. Details are in your app logs. |
| A single entry is missing | Entries with an empty Event are skipped server-side; the rest of the batch is stored. A missing DeviceIdentifier is stricter — Enqueue throws before sending (unless a global DeviceIdentifier is set). Set both on every entry. |
| Logs not in the panel | Batches send every ~2s or at BatchSize. Allow a short delay. Check the collection name and let the host shut down gracefully so the dispatcher can drain remaining logs. |
| Middleware does nothing | Make sure UsePinqloqRequestLogging() is registered early, before any middleware that ends the request. |
| Same event logged twice | You log an endpoint manually and also run the built-in pinqloq middleware, so the request is captured by both. Exclude that path: UsePinqloqRequestLogging(o => o.ExcludePaths("/api/client-events")). |
| Logs dropped under load | The in-memory queue is full. Raise QueueCapacity or lower FlushInterval. |
Redacting sensitive values
Sensitive values — passwords, tokens, card numbers — shouldn't end up in your logs. Add one of two attributes to your code and pinqloq masks them automatically; no other changes needed. This works with MVC controllers, not with minimal API endpoints (app.MapGet/MapPost).
Add [PinqloqRedact] above a single field to hide just that field's value, wherever it shows up — even nested inside another object. Everything else in the request and response is still logged as usual.
public class AuthResponse
{
[PinqloqRedact]
public string Token { get; set; } = "";
public string UserId { get; set; } = "";
}
Add [PinqloqRedactEndpoint] above an entire endpoint — or above the whole controller to cover every endpoint in it — to hide everything it sends and receives: every field, every header. Use it for endpoints that only ever handle sensitive data, like payments or password resets.
[ApiController]
[Route("api/user")]
public class UserController : ControllerBase
{
[HttpPost("Auth")]
public AuthResponse Auth([FromBody] AuthRequest request) => ...;
[PinqloqRedactEndpoint]
[HttpPost("Payment")]
public IActionResult Payment([FromBody] PaymentRequest request) => ...;
}
Log for Auth:
{ "token": "*****REDACTED*****", "userId": "64" }
Log for Payment (every value masked, including headers):
{ "cardNumber": "*****REDACTED*****", "amount": "*****REDACTED*****" }
[PinqloqRedact] only works on JSON bodies — if a request or response isn't JSON (form data, plain text), the field you marked is logged as-is, unmasked. [PinqloqRedactEndpoint] doesn't have this gap: a non-JSON body is still replaced with a single masked value, so it's always protected.
UsePinqloqRequestLogging, call PinqloqRedaction.Resolve(httpContext) directly — it runs the same reflection (cached per action) and returns a PinqloqRedactionPlan with RedactAll, HasRedactions, and ShouldRedact(name). Resolve it once per request and reuse it for every field you check.
AI helper
Two prompts below cover the two things an AI agent can do for you: add pinqloq logging to your project, or connect an AI assistant to logs you already have. Paste the one you need into Claude Code, Cursor, or any agent.
Integrate the SDK
A machine-readable reference for this prompt is available at documentation.md. The agent reads it, asks you which approach, collection, and fields to use, then applies your choice inside your project.
Integrate the pinqloq .NET SDK into my project. First read the full reference: https://pinqloq.pinqponq.io/documentation.md If you cannot reach that URL or fail to read the reference, stop immediately, do not guess or write any code, and tell me that you could not load the reference. In that case, I can download the markdown file manually from https://pinqloq.pinqponq.io/documentation.html#ai-prompt and provide it to you directly. Before anything else, ask me which language I prefer to continue in. Then inspect my project (Program.cs, appsettings.json, existing middleware, and DI setup) and ask me these questions one step at a time before writing any code: 1. What do you want to log? - Backend only (HTTP requests, business events, services, background jobs, etc.) - Client only (events coming from a mobile app, web app, or any other device forwarded through your backend) - Both 2. (If backend was included in question 1) What do you want to log on the backend? - HTTP requests - Other backend events (business events, exceptions, background job results, etc.) - Both 3. (If HTTP requests was included in question 2) How do you want to log HTTP requests? - Automatically log every HTTP request with built-in pinqloq middleware (note: the middleware wraps the response stream with a buffer to capture the response body, which adds a small memory allocation per request) - Manually log specific HTTP requests using custom logging (inject IPinqloqLogger and call Enqueue from wherever I have the data) You cannot access the pinqloq dashboard yourself, so also tell me the manual setup I must do there first at https://pinqloq.pinqponq.io: (1) create a project, (2) copy its secret key and store it server-side (user-secrets, an environment variable, or a secret manager; never in client or front-end code), (3) create a collection and copy its name to use as ApiLogsCollectionName or per-entry CollectionName, and (4) open Team Members, use Edit on my admin or owner account, and set a panel password of at least 8 characters (this gives me access to the live log panel to view logs). After I answer, register the SDK once with AddPinqloq(...) in Program.cs, keep the secret key in config, never log secrets or PII, and finish by summarizing both what you changed in code and the exact dashboard steps I still need to complete.
Connect an AI assistant to your logs
Use this instead if you just want to ask an assistant about logs you already have. No SDK integration needed. See MCP server for what this connects to.
Connect me to the pinqloq MCP server.
{
"mcpServers": {
"pinqloq": {
"url": "https://pinqloq-mcp.pinqponq.io/mcp",
"headers": { "secret_key": "<my-key>" }
}
}
}