# 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](https://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](#dashboard-setup). |

### 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.

> **This is manual logging with a Device source** - Client logging uses the same [Enqueue](#http-custom) as backend logging. Set `LogSourceType` to `Device` and use a separate collection. If you also run [automatic request logging](#http-builtin), exclude this endpoint. Otherwise the same request is logged twice.

### How a client log travels

1. **Client app → your backend** - The app posts the event to an endpoint you own. See [In your client app](#client-app).
2. **Your backend receives it** - Your backend receives the client's payload on an endpoint you define.
3. **Your backend → pinqloq** - The SDK ships the event to pinqloq. See [Custom logging](#http-custom).

## 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

```bash
curl -X POST https://your-backend.example.com/logs \
  -H "Content-Type: application/json" \
  -d '{ "event": "checkout_completed", "orderId": "A-1042", "screen": "cart" }'
```

> **Never ship the secret key** - The pinqloq secret key is a server credential. Keep it out of mobile bundles, browser code and anything a user can inspect. The client only ever talks to your backend.

## 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

1. **Create a project** - See [Create a project](#dashboard-setup).
2. **Create a collection** - See [Create a collection](#collection-setup). You will use its name as `CollectionName` in the SDK.
3. **Register the SDK** - Call `AddPinqloq(...)`. See [SDK configuration](#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.

> **Logging HTTP requests via custom logging** - You can use custom logging to log HTTP requests. There are no restrictions. However, to ensure the log panel displays them correctly, include the following keys inside `Detail`: `RequestMethod`, `ResponseCode`, `InputJson`, and `OutputJson`. 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.

```csharp
pinqloqLogger.Enqueue(new PinqloqLogEntry
{
    Event         = "order_placed",
    Identifier    = "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](#client-app) for the client-side code.

```csharp
pinqloqLogger.Enqueue(new PinqloqLogEntry
{
    Event          = "order_placed",
    Identifier     = "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"));
```

> **Field reference** - `Event` is always required. Without it the entry is skipped. `Identifier` is always required; if the entry leaves it empty, the global `Identifier` option fills it, and 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](#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. |
| `Identifier` | 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 `Identifier` 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](#configuration). |
| `Enqueue(entries)` | Buffered, batched | Same as above, but queues a whole list in one call. Returns the number of entries accepted into the queue. |

> **Don't need custom fields?** - Skip writing the call yourself and go straight to [Built-in middleware →](#http-builtin)

## 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.

> **How response capture works** - To capture the response body, the middleware wraps the response stream with a buffer before the request travels down the pipeline. The response is written into memory first, then flushed to the client. This adds a small allocation per request. If your application is latency-sensitive or you prefer not to insert middleware into the pipeline, use [custom logging](#http-custom) instead and call `Enqueue` directly at the point where you already have the data you need.

### Before you start

1. **Create a project** - See [Create a project](#dashboard-setup).
2. **Create a collection** - See [Create a collection](#collection-setup). You will use its name as `ApiLogsCollectionName`.
3. **Set `ApiLogsCollectionName` in SDK configuration** - Optional if your secret key allows a single collection — the server resolves it automatically. Required if the key allows more than one. See [SDK configuration](#configuration).
4. **Add the middleware** - To skip certain paths (health checks, Swagger, endpoints you log manually), use `ExcludePaths`. Matching is segment-based and case-insensitive: `/api` matches `/api/orders` but not `/apixyz`.

### 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 value of `event` is the request path by default. To override it, use `AddDetail("event", …)`.

Use `SetIdentifier` to resolve the required `Identifier` 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 (`Identifier` / `AppVersionName`) when the callback returns null or empty.

```csharp
app.UsePinqloqRequestLogging(options =>
{
    // Optional: the middleware already reads the "identifier" header. Use SetIdentifier only to override it.
    options.SetIdentifier(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 (request path)
    options.AddDetail("event", ctx => ctx.Request.Path);
});
```

> **How the middleware resolves Identifier** - The middleware resolves `Identifier` in this order: `SetIdentifier` (overrides everything), then the `identifier` request header, then the global `Identifier` 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 `SetIdentifier(ctx => ctx.Request.Headers["identifier"].FirstOrDefault() ?? Environment.MachineName)` or a global `Identifier`.

### 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 | `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.

## Create a project

The dashboard is at [pinqloq.pinqponq.io](https://pinqloq.pinqponq.io). Create a project here to get your secret key.

1. **Create a project** - A secret key belongs to a project. You may want to use one project per environment to keep production and development logs separated.
2. **Copy the secret key** - Store it in user-secrets, an environment variable, or a secret manager. Never put it in client apps or front-end code.

## 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.

1. **Create a collection** - Open the dashboard, go to your project, and create a collection.
2. **Copy the collection name** - You will use this name in your SDK configuration as `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

1. **Open the log panel** - Go to [pinqloq-panel.pinqponq.io](https://pinqloq-panel.pinqponq.io) and sign in with the panel account created from the dashboard.
2. **Choose a project and collection** - Logs are grouped by project and collection. Pick the collection name you configured in `ApiLogsCollectionName` or sent on each log entry.
3. **Filter and inspect logs** - Use the panel filters to narrow logs by collection, level, source, event, device, or time range. New logs may appear after the SDK batch delay.

### 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](https://pinqloq-panel.pinqponq.io) is where those users sign in to view logs.

## SDK configuration

Add the `pinqloq` NuGet package, then register the SDK once in `Program.cs`.

```bash
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 `Identifier` as the global fallback for the required per-log `Identifier` field, so both middleware and manual logs ship with one.

```csharp
builder.Services.AddPinqloq(options =>
{
    options.SecretKey             = builder.Configuration["Pinqloq:SecretKey"]!;
    options.ApiLogsCollectionName = builder.Configuration["Pinqloq:ApiLogsCollectionName"]!;
    options.Identifier            = 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. |
| Identifier | `string?` | null | Global fallback for the required per-log `Identifier`, applied to any log (middleware or manual) that doesn't set its own. Set this or use `SetIdentifier`, otherwise the server rejects logs with an empty `Identifier`. |
| 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:

```appsettings.json
{
  "Pinqloq": {
    "SecretKey": "lgl_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "ApiLogsCollectionName": "myapp_api_logs",
    "AppVersionName": "1.0.0"
  }
}
```

```csharp
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):

```csharp
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`. |

> Callbacks are optional. If you skip them, failures are still written to your application log — throttled, with the server's response. The [built-in pinqloq middleware](#http-builtin) logs every request for you and has no call site, so its failures only go to the application log.

### Production notes

> **Keep the secret key server-side** - It's a server credential. Store it in user-secrets, environment variables, or a secret manager. Never put it in client apps, mobile bundles, or front-end code.

| 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](#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` / `Identifier`) is empty. Set `ApiLogsCollectionName` or a per-entry `CollectionName`, and make sure every log has an `Event` and an `Identifier`. 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 `Identifier` is stricter — `Enqueue` throws before sending (unless a global `Identifier` 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`. |
