Issues (6)

src/Agent.php (5 issues)

1
<?php
2
3
namespace SamuelBednarcik\ElasticAPMAgent;
4
5
use GuzzleHttp\ClientInterface;
6
use Psr\Http\Message\ResponseInterface;
7
use SamuelBednarcik\ElasticAPMAgent\Builder\AbstractEventBuilder;
8
use SamuelBednarcik\ElasticAPMAgent\Builder\TransactionBuilder;
9
use SamuelBednarcik\ElasticAPMAgent\Events\Error;
10
use SamuelBednarcik\ElasticAPMAgent\Events\Span;
11
use SamuelBednarcik\ElasticAPMAgent\Events\Transaction;
12
use SamuelBednarcik\ElasticAPMAgent\Exception\AgentStateException;
13
use SamuelBednarcik\ElasticAPMAgent\Exception\BadEventRequestException;
14
use SamuelBednarcik\ElasticAPMAgent\Serializer\ElasticAPMSerializer;
15
use Symfony\Component\HttpFoundation\Request;
16
17
class Agent
18
{
19
    const AGENT_NAME = 'samuelbednarcik/elastic-apm-agent';
20
    const AGENT_VERSION = 'dev';
21
22
    /**
23
     * @var AgentConfiguration
24
     */
25
    private $config;
26
27
    /**
28
     * @var ClientInterface
29
     */
30
    private $client;
31
32
    /**
33
     * @var ElasticAPMSerializer
34
     */
35
    private $serializer;
36
37
    /**
38
     * @var CollectorInterface[]
39
     */
40
    private $collectors = [];
41
42
    /**
43
     * @var Transaction|null
44
     */
45
    private $transaction;
46
47
    /**
48
     * @var Span[]
49
     */
50
    private $spans = [];
51
52
    /**
53
     * @var Error[]
54
     */
55
    private $errors = [];
56
57
    /**
58
     * @var bool
59
     */
60
    private $started = false;
61
62
    /**
63
     * @param AgentConfiguration $config
64
     * @param ClientInterface $client
65
     * @param ElasticAPMSerializer $serializer
66
     * @param CollectorInterface[] $collectors
67
     */
68
    public function __construct(
69
        AgentConfiguration $config,
70
        ClientInterface $client,
71
        ElasticAPMSerializer $serializer,
72
        array $collectors = []
73
    ) {
74
        $this->config = $config;
75
        $this->client = $client;
76
        $this->serializer = $serializer;
77
        $this->collectors = $collectors;
78
    }
79
80
    /**
81
     * @return AgentConfiguration
82
     */
83
    public function getConfig(): AgentConfiguration
84
    {
85
        return $this->config;
86
    }
87
88
    /**
89
     * @return null|Transaction
90
     */
91
    public function getTransaction(): ?Transaction
92
    {
93
        return $this->transaction;
94
    }
95
96
    /**
97
     * @param Request|null $request
98
     * @return Transaction
99
     * @throws AgentStateException
100
     * @throws \Exception
101
     */
102
    public function start(Request $request = null): Transaction
103
    {
104
        if ($this->started === true) {
105
            throw new AgentStateException('Agent already started!');
106
        }
107
108
        $this->transaction = TransactionBuilder::buildFromRequest($request);
109
        $this->started = true;
110
        return $this->transaction;
111
    }
112
113
    /**
114
     * @param string $result
115
     * @return Transaction
116
     * @throws AgentStateException
117
     * @throws \Exception
118
     */
119
    public function stop(string $result): Transaction
120
    {
121
        if ($this->started !== true) {
122
            throw new AgentStateException('You have to call start method before the stop method!');
123
        }
124
125
        $this->transaction->setResult($result);
0 ignored issues
show
The method setResult() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

125
        $this->transaction->/** @scrutinizer ignore-call */ 
126
                            setResult($result);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
126
        $this->transaction->setDuration(
127
            TransactionBuilder::calculateDuration(microtime(true) * 1000000, $this->transaction->getTimestamp())
0 ignored issues
show
It seems like $this->transaction->getTimestamp() can also be of type null; however, parameter $transactionTimestamp of SamuelBednarcik\ElasticA...er::calculateDuration() does only seem to accept double, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

127
            TransactionBuilder::calculateDuration(microtime(true) * 1000000, /** @scrutinizer ignore-type */ $this->transaction->getTimestamp())
Loading history...
128
        );
129
130
        $this->spans = $this->collect();
131
132
        return $this->transaction;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->transaction could return the type null which is incompatible with the type-hinted return SamuelBednarcik\ElasticAPMAgent\Events\Transaction. Consider adding an additional type-check to rule them out.
Loading history...
133
    }
134
135
    /**
136
     * Capture an error in current transaction
137
     * @param Error $error
138
     */
139
    public function captureError(Error $error)
140
    {
141
        $this->errors[] = $error;
142
    }
143
144
145
    /**
146
     * Collect spans from registered collectors
147
     * @return Span[]
148
     * @throws \Exception
149
     */
150
    private function collect(): array
151
    {
152
        $spans = [];
153
154
        foreach ($this->collectors as $collector) {
155
            foreach ($collector->getSpans() as $span) {
156
                $spans[] = $this->prepareSpan($span);
157
            }
158
        }
159
160
        return $spans;
161
    }
162
163
    /**
164
     * @throws \GuzzleHttp\Exception\GuzzleException
165
     * @throws BadEventRequestException
166
     */
167
    public function sendAll(): ResponseInterface
168
    {
169
        $this->prepareTransaction();
170
171
        $request = new EventIntakeRequest($this->serializer);
172
        $request->setMetadata($this->config->getMetadata());
0 ignored issues
show
It seems like $this->config->getMetadata() can also be of type null; however, parameter $metadata of SamuelBednarcik\ElasticA...eRequest::setMetadata() does only seem to accept SamuelBednarcik\ElasticAPMAgent\Events\Metadata, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

172
        $request->setMetadata(/** @scrutinizer ignore-type */ $this->config->getMetadata());
Loading history...
173
        $request->addTransaction($this->transaction);
0 ignored issues
show
It seems like $this->transaction can also be of type null; however, parameter $transaction of SamuelBednarcik\ElasticA...quest::addTransaction() does only seem to accept SamuelBednarcik\ElasticAPMAgent\Events\Transaction, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

173
        $request->addTransaction(/** @scrutinizer ignore-type */ $this->transaction);
Loading history...
174
        $request->setSpans($this->spans);
175
        $request->setErrors($this->errors);
176
177
        return $this->send($request);
178
    }
179
180
    /**
181
     * @param EventIntakeRequest $request
182
     * @return ResponseInterface
183
     * @throws BadEventRequestException
184
     * @throws \GuzzleHttp\Exception\GuzzleException
185
     */
186
    public function send(EventIntakeRequest $request): ResponseInterface
187
    {
188
        return $this->client->request('POST', $this->config->getServerUrl() . EventIntakeRequest::INTAKE_ENDPOINT, [
189
            'body' => $request->getRequestBody(),
190
            'headers' => [
191
                'Content-Type' => EventIntakeRequest::CONTENT_TYPE
192
            ]
193
        ]);
194
    }
195
196
    /**
197
     * Prepare transaction for sending to APM.
198
     */
199
    private function prepareTransaction()
200
    {
201
        $this->transaction->setSpanCount([
202
            'started' => count($this->spans)
203
        ]);
204
205
        if (empty($this->transaction->getContext()) || empty($this->spans)) {
206
            $this->transaction->setSampled(false);
207
        }
208
    }
209
210
    /**
211
     * Prepare span for for sending to APM.
212
     * @param Span $span
213
     * @return Span
214
     * @throws \Exception
215
     */
216
    private function prepareSpan(Span $span): Span
217
    {
218
        $span->setTransactionId($this->transaction->getId());
219
        $span->setTraceId($this->transaction->getTraceId());
220
221
        if ($span->getId() === null) {
222
            $span->setId(AbstractEventBuilder::generateRandomBitsInHex(64));
223
        }
224
225
        if ($span->getParentId() === null) {
226
            $span->setParentId($this->transaction->getId());
227
        }
228
229
        if ($span->getStart() === null) {
230
            $span->setStart(
231
                intval(round(($span->getTimestamp() - $this->transaction->getTimestamp()) / 1000))
232
            );
233
        }
234
235
        return $span;
236
    }
237
}
238