Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ $logdash = Logdash::create([
]);
```

Every log and metric sent by this instance carries the app.
Every log, metric and event sent by this instance carries the app.
Older service keys ignore `app`.

### Framework Integration
Expand Down Expand Up @@ -157,10 +157,10 @@ Creates a new Logdash instance.

**Parameters:**

- `apiKey` (string, optional): Your Logdash API key. If not provided, only local logging will work
- `apiKey` (string, optional): Your Logdash API key. If not provided, logs and events are only printed locally
- `host` (string, optional): Logdash API host. Defaults to `https://api.logdash.io`
- `verbose` (bool, optional): Enable verbose internal logging. Defaults to `false`
- `app` (string, optional): Name of this app within its domain, sent with every log and metric. See [Apps](#apps)
- `app` (string, optional): Name of this app within its domain, sent with every log, metric and event. See [Apps](#apps)

### Logger

Expand Down Expand Up @@ -196,6 +196,31 @@ $metrics->mutate('error_count', -1); // Decrement
$metrics->mutate('temperature', 2.5); // Increase by 2.5
```

### Events

#### `track(string $name, array $props = [], string|int|null $userId = null): void`

Track an event, such as an agent run or a signup.

```php
$logdash->track('agent_run', [
'agent' => 'support',
'steps' => 4,
'success' => true,
], $user->id);
```

Events show up in the domain's Analytics as goals, with a daily trend.
Pass the user ID for signed-in users, so one person counts once across the website and apps.
Without it, Logdash counts a visitor of that app that changes every day.
Never put personal data in props.

**Parameters:**

- `name` (string): The event name, matching `[a-z][a-z0-9_]{0,63}`, except `pageview`, `pageleave` and `browser_error`, which belong to the website tracker. An invalid name drops the event with a warning
- `props` (array, optional): Up to 10 properties with string, number or boolean values
- `userId` (string|int, optional): The signed-in user's internal ID, never an email address. An ID that contains `@` drops the event with a warning

## Log Levels

The SDK supports the following log levels (in order of severity):
Expand Down
2 changes: 1 addition & 1 deletion check-deployed-package/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Logdash Rust SDK Demo
# Logdash PHP SDK Demo

Use command below to test if the published SDK works:

Expand Down
92 changes: 92 additions & 0 deletions src/Events/Events.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php

declare(strict_types=1);

namespace Logdash\Events;

use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Logdash\Logger\InternalLogger;
use Logdash\Types\RequiredInitializationParams;

class Events
{
private const NAME = '/^[a-z][a-z0-9_]{0,63}$/D';
// Belong to the website tracker, so the API rejects them
private const WEBSITE_EVENTS = ['pageview', 'pageleave', 'browser_error'];

private Client $httpClient;

public function __construct(
private readonly RequiredInitializationParams $params
) {
$this->httpClient = new Client();
}

/**
* @param array<string, string|int|float|bool> $props
*/
public function track(string $name, array $props, string|int|null $userId): void
{
if (preg_match(self::NAME, $name) !== 1 || in_array($name, self::WEBSITE_EVENTS, true)) {
InternalLogger::getInternalLogger()->warn(
"Dropping event \"{$name}\": its name must match [a-z][a-z0-9_]{0,63}",
'and not be pageview, pageleave or browser_error'
);
return;
}

if (is_string($userId) && str_contains($userId, '@')) {
InternalLogger::getInternalLogger()->warn(
"Dropping event \"{$name}\": its userId must be an internal ID, never an email address"
);
return;
}

$event = [
'name' => $name,
...($props === [] ? [] : ['props' => $props]),
...($userId === null || $userId === '' ? [] : ['userId' => $userId]),
...($this->params->app === null ? [] : ['app' => $this->params->app]),
];

if (empty($this->params->apiKey)) {
// Like logs without an API key: printed locally only
InternalLogger::getInternalLogger()->log(
'Event',
(string) json_encode($event, JSON_PARTIAL_OUTPUT_ON_ERROR)
);
return;
}

if ($this->params->verbose) {
InternalLogger::getInternalLogger()->verbose("Tracking event {$name}");
}

try {
$this->httpClient->post($this->params->host . '/events', [
'headers' => [
'Content-Type' => 'application/json',
'project-api-key' => $this->params->apiKey,
],
// The API stores an event once per id, so a retry must send the same one
'json' => ['id' => self::uuidV4()] + $event,
'timeout' => 5,
]);
} catch (GuzzleException $e) {
if ($this->params->verbose) {
InternalLogger::getInternalLogger()->verbose("Failed to send event: " . $e->getMessage());
}
// Fail silently in production
}
}

private static function uuidV4(): string
{
$bytes = random_bytes(16);
$bytes[6] = chr((ord($bytes[6]) & 0x0f) | 0x40);
$bytes[8] = chr((ord($bytes[8]) & 0x3f) | 0x80);

return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($bytes), 4));
}
}
20 changes: 18 additions & 2 deletions src/Logdash.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Logdash;

use Logdash\Events\Events;
use Logdash\Logger\Logger;
use Logdash\Metrics\BaseMetrics;
use Logdash\Types\InitializationParams;
Expand All @@ -16,13 +17,16 @@ class Logdash
{
private Logger $logger;
private BaseMetrics $metrics;
private Events $events;

private function __construct(
Logger $logger,
BaseMetrics $metrics
BaseMetrics $metrics,
Events $events
) {
$this->logger = $logger;
$this->metrics = $metrics;
$this->events = $events;
}

/**
Expand All @@ -49,7 +53,7 @@ public static function create(?array $params = null): self
}
);

return new self($logger, $metrics);
return new self($logger, $metrics, new Events($requiredParams));
}

public function logger(): Logger
Expand All @@ -61,4 +65,16 @@ public function metrics(): BaseMetrics
{
return $this->metrics;
}

/**
* Tracks an event, such as an agent run or a signup, and never throws.
*
* @param string $name Matches [a-z][a-z0-9_]{0,63}, any other name drops the event with a warning
* @param array<string, string|int|float|bool> $props Up to 10 properties, never personal data
* @param string|int|null $userId The signed-in user's internal ID, never an email address
*/
public function track(string $name, array $props = [], string|int|null $userId = null): void
{
$this->events->track($name, $props, $userId);
}
}
5 changes: 5 additions & 0 deletions src/Logger/InternalLogger.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ public function log(string ...$data): void
$this->internalLog(LogLevel::INFO, implode(' ', $data));
}

public function warn(string ...$data): void
{
$this->internalLog(LogLevel::WARN, implode(' ', $data));
}

public function verbose(string ...$data): void
{
$this->internalLog(LogLevel::VERBOSE, implode(' ', $data));
Expand Down
3 changes: 2 additions & 1 deletion src/Logger/Logger.php
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,8 @@ private function convertToStrings(array $data): array
}

if (is_array($item) || is_object($item)) {
return json_encode($item, JSON_THROW_ON_ERROR);
// Never throws into the app, for example on a Latin-1 string
return (string) json_encode($item, JSON_INVALID_UTF8_SUBSTITUTE | JSON_PARTIAL_OUTPUT_ON_ERROR);
}

return var_export($item, true);
Expand Down
Loading
Loading