Repository: kubernetes-sigs/security-profiles-operator
Pull request #513 (2021-06-30 12:44:15)

Add support for log enricher profile recording
#### What type of PR is this?

/kind feature

#### What this PR does / why we need it:

Implements profile recording by using the log enricher.

It also fixes a couple of issues with the log enricher:

- We store the `containerID` here, which should be `rawContainerID`: https://github.com/kubernetes-sigs/security-profiles-operator/blob/cd71f464ca8606ff427bf8a2826c682d92053a28/internal/pkg/daemon/enricher/container.go#L100
- We re-init the local `containers` for every loop: https://github.com/kubernetes-sigs/security-profiles-operator/blob/cd71f464ca8606ff427bf8a2826c682d92053a28/internal/pkg/daemon/enricher/container.go#L66
- We can completely remove the `containers` map and only rely on the cache to stay safe: https://github.com/kubernetes-sigs/security-profiles-operator/blob/cd71f464ca8606ff427bf8a2826c682d92053a28/internal/pkg/daemon/enricher/container.go#L58

#### Which issue(s) this PR fixes:

PoC of https://github.com/kubernetes-sigs/security-profiles-operator/issues/510

#### Does this PR have test?

None

#### Special notes for your reviewer:

Possible follow-upds:
- Update the docs to cover the new feature and `ProfileRecording` field
- Use GRPC streaming for the syscalls (open a stream on the beginning of the enricher and use that one for pushing the syscalls)

#### Does this PR introduce a user-facing change?

```release-note
Added support for recording profiles by using the log enricher.
```


k8s-ci-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/security-profiles-operator/pull/513#" title="Author self-approved">saschagrunert</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Fsecurity-profiles-operator).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/kubernetes-sigs/security-profiles-operator/blob/master/OWNERS)~~ [saschagrunert]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
k8s-ci-robot: @saschagrunert: The following test **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-security-profiles-operator-test-unit | 12ac26b72c9d6a6f07a395564769aab393eac76f | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/513/pull-security-profiles-operator-test-unit/1410217629225848832) | `/test pull-security-profiles-operator-test-unit`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=security-profiles-operator&pr=513). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Asaschagrunert). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/security-profiles-operator/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @saschagrunert: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-security-profiles-operator-test-unit | 12ac26b72c9d6a6f07a395564769aab393eac76f | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/513/pull-security-profiles-operator-test-unit/1410217629225848832) | `/test pull-security-profiles-operator-test-unit`
pull-security-profiles-operator-verify | 12ac26b72c9d6a6f07a395564769aab393eac76f | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/513/pull-security-profiles-operator-verify/1410217629188100096) | `/test pull-security-profiles-operator-verify`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=security-profiles-operator&pr=513). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Asaschagrunert). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/security-profiles-operator/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @saschagrunert: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-security-profiles-operator-verify | 12ac26b72c9d6a6f07a395564769aab393eac76f | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/513/pull-security-profiles-operator-verify/1410217629188100096) | `/test pull-security-profiles-operator-verify`
pull-security-profiles-operator-test-unit | 8e2eb30cdb8ca8efe0a46b260fb71a6c5915bca1 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/513/pull-security-profiles-operator-test-unit/1410218012979499008) | `/test pull-security-profiles-operator-test-unit`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=security-profiles-operator&pr=513). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Asaschagrunert). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/security-profiles-operator/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @saschagrunert: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-security-profiles-operator-test-unit | 8e2eb30cdb8ca8efe0a46b260fb71a6c5915bca1 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/513/pull-security-profiles-operator-test-unit/1410218012979499008) | `/test pull-security-profiles-operator-test-unit`
pull-security-profiles-operator-verify | 8e2eb30cdb8ca8efe0a46b260fb71a6c5915bca1 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/513/pull-security-profiles-operator-verify/1410218012941750272) | `/test pull-security-profiles-operator-verify`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=security-profiles-operator&pr=513). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Asaschagrunert). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/security-profiles-operator/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @saschagrunert: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-security-profiles-operator-verify | 1535e06f644fec5fec739761131069807f101a07 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/513/pull-security-profiles-operator-verify/1410218968509059072) | `/test pull-security-profiles-operator-verify`
pull-security-profiles-operator-test-unit | 1535e06f644fec5fec739761131069807f101a07 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/513/pull-security-profiles-operator-test-unit/1410218968534224896) | `/test pull-security-profiles-operator-test-unit`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=security-profiles-operator&pr=513). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Asaschagrunert). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/security-profiles-operator/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @saschagrunert: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-security-profiles-operator-test-unit | 0d40186d60b329aad2e9bc60da0789a6d6bd3269 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/513/pull-security-profiles-operator-test-unit/1410220088665051136) | `/test pull-security-profiles-operator-test-unit`
pull-security-profiles-operator-verify | 0d40186d60b329aad2e9bc60da0789a6d6bd3269 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/513/pull-security-profiles-operator-verify/1410220088648273920) | `/test pull-security-profiles-operator-verify`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=security-profiles-operator&pr=513). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Asaschagrunert). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/security-profiles-operator/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @saschagrunert: The following test **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-security-profiles-operator-verify | 09ec20da5fb25ca5fe328fd0b47bfb654a057759 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/513/pull-security-profiles-operator-verify/1410233759847944192) | `/test pull-security-profiles-operator-verify`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=security-profiles-operator&pr=513). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Asaschagrunert). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/security-profiles-operator/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
codecov-commenter: # [Codecov](https://codecov.io/gh/kubernetes-sigs/security-profiles-operator/pull/513?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#513](https://codecov.io/gh/kubernetes-sigs/security-profiles-operator/pull/513?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (09ec20d) into [master](https://codecov.io/gh/kubernetes-sigs/security-profiles-operator/commit/93c66bf40cbc851651a25fb4a4d273b8c59d8f2c?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (93c66bf) will **decrease** coverage by `1.15%`.
> The diff coverage is `41.66%`.


```diff
@@            Coverage Diff             @@
##           master     #513      +/-   ##
==========================================
- Coverage   44.07%   42.92%   -1.16%     
==========================================
  Files          24       24              
  Lines        1207     1258      +51     
==========================================
+ Hits          532      540       +8     
- Misses        644      684      +40     
- Partials       31       34       +3     
```

k8s-ci-robot: @saschagrunert: The following test **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-security-profiles-operator-test-e2e | 1e0ff324ad40d1b7afc17070804d76aadf3afa77 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/513/pull-security-profiles-operator-test-e2e/1410258321759277056) | `/test pull-security-profiles-operator-test-e2e`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=security-profiles-operator&pr=513). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Asaschagrunert). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/security-profiles-operator/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @saschagrunert: The following test **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-security-profiles-operator-test-e2e | 73eea5b4f69aef4504c990482fdf74c7ff497336 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/513/pull-security-profiles-operator-test-e2e/1410492997086220288) | `/test pull-security-profiles-operator-test-e2e`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=security-profiles-operator&pr=513). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Asaschagrunert). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/security-profiles-operator/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
saschagrunert: /retest
k8s-ci-robot: @saschagrunert: The following test **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-security-profiles-operator-test-e2e | 73eea5b4f69aef4504c990482fdf74c7ff497336 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/513/pull-security-profiles-operator-test-e2e/1410497048955850752) | `/test pull-security-profiles-operator-test-e2e`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=security-profiles-operator&pr=513). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Asaschagrunert). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/security-profiles-operator/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @saschagrunert: The following test **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-security-profiles-operator-test-e2e | 29144fd563da1d4e31327f829a22fea7990c566a | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/513/pull-security-profiles-operator-test-e2e/1410515633640050688) | `/test pull-security-profiles-operator-test-e2e`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=security-profiles-operator&pr=513). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Asaschagrunert). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/security-profiles-operator/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @saschagrunert: The following test **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-security-profiles-operator-test-e2e | b918a295333a652b3d8f664de4f5cc76f342edfc | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/513/pull-security-profiles-operator-test-e2e/1410527121637380096) | `/test pull-security-profiles-operator-test-e2e`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=security-profiles-operator&pr=513). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Asaschagrunert). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/security-profiles-operator/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
saschagrunert: /retest
k8s-ci-robot: @saschagrunert: The following test **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-security-profiles-operator-verify | 670bc5528b423a0ba84e15263352c13e705b8d56 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/513/pull-security-profiles-operator-verify/1410887794028449792) | `/test pull-security-profiles-operator-verify`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=security-profiles-operator&pr=513). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Asaschagrunert). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/security-profiles-operator/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @saschagrunert: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-security-profiles-operator-verify | 670bc5528b423a0ba84e15263352c13e705b8d56 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/513/pull-security-profiles-operator-verify/1410887794028449792) | `/test pull-security-profiles-operator-verify`
pull-security-profiles-operator-test-e2e | 670bc5528b423a0ba84e15263352c13e705b8d56 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/513/pull-security-profiles-operator-test-e2e/1410887794129113088) | `/test pull-security-profiles-operator-test-e2e`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=security-profiles-operator&pr=513). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Asaschagrunert). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/security-profiles-operator/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @saschagrunert: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-security-profiles-operator-verify | 670bc5528b423a0ba84e15263352c13e705b8d56 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/513/pull-security-profiles-operator-verify/1410887794028449792) | `/test pull-security-profiles-operator-verify`
pull-security-profiles-operator-test-e2e | 670bc5528b423a0ba84e15263352c13e705b8d56 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/513/pull-security-profiles-operator-test-e2e/1410887794129113088) | `/test pull-security-profiles-operator-test-e2e`
pull-security-profiles-operator-test-unit | 2a2ee25e8e0019fbd687dd577d584d8c0cae26c1 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/513/pull-security-profiles-operator-test-unit/1410888383873421312) | `/test pull-security-profiles-operator-test-unit`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=security-profiles-operator&pr=513). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Asaschagrunert). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/security-profiles-operator/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
saschagrunert: /retest
saschagrunert: I'll update the docs in a follow-up PR.
k8s-ci-robot: @saschagrunert: The following test **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-security-profiles-operator-test-e2e | b4ae46c56661cb92e13a1845d05593fed0d5c163 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/513/pull-security-profiles-operator-test-e2e/1410901990795382784) | `/test pull-security-profiles-operator-test-e2e`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=security-profiles-operator&pr=513). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Asaschagrunert). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/security-profiles-operator/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
saschagrunert: /test pull-security-profiles-operator-test-e2e
k8s-ci-robot: @saschagrunert: The following test **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-security-profiles-operator-test-e2e | 978db029eea7b45e35d74c3a564c32f74baf1540 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/513/pull-security-profiles-operator-test-e2e/1410911604576358400) | `/test pull-security-profiles-operator-test-e2e`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=security-profiles-operator&pr=513). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Asaschagrunert). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/security-profiles-operator/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
saschagrunert: /test pull-security-profiles-operator-test-e2e
k8s-ci-robot: @saschagrunert: The following test **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-security-profiles-operator-test-e2e | 978db029eea7b45e35d74c3a564c32f74baf1540 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/513/pull-security-profiles-operator-test-e2e/1410915559389794304) | `/test pull-security-profiles-operator-test-e2e`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=security-profiles-operator&pr=513). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Asaschagrunert). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/security-profiles-operator/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
saschagrunert: /test pull-security-profiles-operator-test-e2e
k8s-ci-robot: @saschagrunert: The following test **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-security-profiles-operator-test-e2e | fd26441a65c3133d8d668be84486733ff5ac9cd6 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_security-profiles-operator/513/pull-security-profiles-operator-test-e2e/1410934982490198016) | `/test pull-security-profiles-operator-test-e2e`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=security-profiles-operator&pr=513). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Asaschagrunert). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/security-profiles-operator/issues?q=is:issue+is:open) when you hit one in your PR.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
saschagrunert: /retest
k8s-ci-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/security-profiles-operator/pull/513#pullrequestreview-698348470" title="LGTM">JAORMX</a>*, *<a href="https://github.com/kubernetes-sigs/security-profiles-operator/pull/513#" title="Author self-approved">saschagrunert</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Fsecurity-profiles-operator).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/kubernetes-sigs/security-profiles-operator/blob/master/OWNERS)~~ [saschagrunert]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->

Patch:
diff --git a/.golangci.yml b/.golangci.yml
index e64b6c38b..d33d7a58f 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -78,6 +78,8 @@ linters:
     # - testpackage
     # - wsl
 linters-settings:
+  nestif:
+    min-complexity: 10
   gci:
     local-prefixes: sigs.k8s.io/security-profiles-operator
   errcheck:
diff --git a/api/profilerecording/v1alpha1/profilerecording_types.go b/api/profilerecording/v1alpha1/profilerecording_types.go
index f908b4100..6284fc7a9 100644
--- a/api/profilerecording/v1alpha1/profilerecording_types.go
+++ b/api/profilerecording/v1alpha1/profilerecording_types.go
@@ -26,12 +26,23 @@ const (
 	ProfileRecordingKindSeccompProfile ProfileRecordingKind = "SeccompProfile"
 )
 
+type ProfileRecorder string
+
+const (
+	ProfileRecorderHook ProfileRecorder = "hook"
+	ProfileRecorderLogs ProfileRecorder = "logs"
+)
+
 // ProfileRecordingSpec defines the desired state of ProfileRecording.
 type ProfileRecordingSpec struct {
 	// Kind of object to be recorded.
 	// +kubebuilder:validation:Enum=SeccompProfile
 	Kind ProfileRecordingKind `json:"kind"`
 
+	// Recorder to be used.
+	// +kubebuilder:validation:Enum=hook;logs
+	Recorder ProfileRecorder `json:"recorder"`
+
 	// PodSelector selects the pods to record. This field follows standard
 	// label selector semantics. An empty podSelector matches all pods in this
 	// namespace.
diff --git a/api/server/api.pb.go b/api/server/api.pb.go
index bc0a890fa..53300181b 100644
--- a/api/server/api.pb.go
+++ b/api/server/api.pb.go
@@ -15,7 +15,7 @@
 
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.26.0
+// 	protoc-gen-go v1.27.1
 // 	protoc        v3.16.0
 // source: api/server/api.proto
 
@@ -176,6 +176,155 @@ func (x *MetricsAuditRequest) GetSyscall() string {
 	return ""
 }
 
+type RecordSyscallRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Profile string `protobuf:"bytes,1,opt,name=profile,proto3" json:"profile,omitempty"`
+	Syscall string `protobuf:"bytes,2,opt,name=syscall,proto3" json:"syscall,omitempty"`
+}
+
+func (x *RecordSyscallRequest) Reset() {
+	*x = RecordSyscallRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_api_server_api_proto_msgTypes[1]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *RecordSyscallRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RecordSyscallRequest) ProtoMessage() {}
+
+func (x *RecordSyscallRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_api_server_api_proto_msgTypes[1]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use RecordSyscallRequest.ProtoReflect.Descriptor instead.
+func (*RecordSyscallRequest) Descriptor() ([]byte, []int) {
+	return file_api_server_api_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *RecordSyscallRequest) GetProfile() string {
+	if x != nil {
+		return x.Profile
+	}
+	return ""
+}
+
+func (x *RecordSyscallRequest) GetSyscall() string {
+	if x != nil {
+		return x.Syscall
+	}
+	return ""
+}
+
+type SyscallsRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Profile string `protobuf:"bytes,1,opt,name=profile,proto3" json:"profile,omitempty"`
+}
+
+func (x *SyscallsRequest) Reset() {
+	*x = SyscallsRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_api_server_api_proto_msgTypes[2]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *SyscallsRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SyscallsRequest) ProtoMessage() {}
+
+func (x *SyscallsRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_api_server_api_proto_msgTypes[2]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use SyscallsRequest.ProtoReflect.Descriptor instead.
+func (*SyscallsRequest) Descriptor() ([]byte, []int) {
+	return file_api_server_api_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *SyscallsRequest) GetProfile() string {
+	if x != nil {
+		return x.Profile
+	}
+	return ""
+}
+
+type SyscallsResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Syscalls []string `protobuf:"bytes,1,rep,name=syscalls,proto3" json:"syscalls,omitempty"`
+}
+
+func (x *SyscallsResponse) Reset() {
+	*x = SyscallsResponse{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_api_server_api_proto_msgTypes[3]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *SyscallsResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SyscallsResponse) ProtoMessage() {}
+
+func (x *SyscallsResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_api_server_api_proto_msgTypes[3]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use SyscallsResponse.ProtoReflect.Descriptor instead.
+func (*SyscallsResponse) Descriptor() ([]byte, []int) {
+	return file_api_server_api_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *SyscallsResponse) GetSyscalls() []string {
+	if x != nil {
+		return x.Syscalls
+	}
+	return nil
+}
+
 type EmptyResponse struct {
 	state         protoimpl.MessageState
 	sizeCache     protoimpl.SizeCache
@@ -185,7 +334,7 @@ type EmptyResponse struct {
 func (x *EmptyResponse) Reset() {
 	*x = EmptyResponse{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_api_server_api_proto_msgTypes[1]
+		mi := &file_api_server_api_proto_msgTypes[4]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -198,7 +347,7 @@ func (x *EmptyResponse) String() string {
 func (*EmptyResponse) ProtoMessage() {}
 
 func (x *EmptyResponse) ProtoReflect() protoreflect.Message {
-	mi := &file_api_server_api_proto_msgTypes[1]
+	mi := &file_api_server_api_proto_msgTypes[4]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -211,7 +360,7 @@ func (x *EmptyResponse) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use EmptyResponse.ProtoReflect.Descriptor instead.
 func (*EmptyResponse) Descriptor() ([]byte, []int) {
-	return file_api_server_api_proto_rawDescGZIP(), []int{1}
+	return file_api_server_api_proto_rawDescGZIP(), []int{4}
 }
 
 var File_api_server_api_proto protoreflect.FileDescriptor
@@ -235,15 +384,37 @@ var file_api_server_api_proto_rawDesc = []byte{
 	0x61, 0x6c, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x79, 0x73, 0x63, 0x61,
 	0x6c, 0x6c, 0x22, 0x20, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x45,
 	0x43, 0x43, 0x4f, 0x4d, 0x50, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x45, 0x4c, 0x49, 0x4e,
-	0x55, 0x58, 0x10, 0x01, 0x22, 0x0f, 0x0a, 0x0d, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x73,
-	0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x5d, 0x0a, 0x18, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74,
-	0x79, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f,
-	0x72, 0x12, 0x41, 0x0a, 0x0f, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x41, 0x75, 0x64, 0x69,
-	0x74, 0x49, 0x6e, 0x63, 0x12, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x65, 0x74, 0x72, 0x69,
-	0x63, 0x73, 0x41, 0x75, 0x64, 0x69, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12,
-	0x2e, 0x61, 0x70, 0x69, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
-	0x73, 0x65, 0x22, 0x00, 0x42, 0x06, 0x5a, 0x04, 0x2f, 0x61, 0x70, 0x69, 0x62, 0x06, 0x70, 0x72,
-	0x6f, 0x74, 0x6f, 0x33,
+	0x55, 0x58, 0x10, 0x01, 0x22, 0x4a, 0x0a, 0x14, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x53, 0x79,
+	0x73, 0x63, 0x61, 0x6c, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07,
+	0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70,
+	0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x79, 0x73, 0x63, 0x61, 0x6c,
+	0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x79, 0x73, 0x63, 0x61, 0x6c, 0x6c,
+	0x22, 0x2b, 0x0a, 0x0f, 0x53, 0x79, 0x73, 0x63, 0x61, 0x6c, 0x6c, 0x73, 0x52, 0x65, 0x71, 0x75,
+	0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x18, 0x01,
+	0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x22, 0x2e, 0x0a,
+	0x10, 0x53, 0x79, 0x73, 0x63, 0x61, 0x6c, 0x6c, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
+	0x65, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x79, 0x73, 0x63, 0x61, 0x6c, 0x6c, 0x73, 0x18, 0x01, 0x20,
+	0x03, 0x28, 0x09, 0x52, 0x08, 0x73, 0x79, 0x73, 0x63, 0x61, 0x6c, 0x6c, 0x73, 0x22, 0x0f, 0x0a,
+	0x0d, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x97,
+	0x02, 0x0a, 0x18, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x50, 0x72, 0x6f, 0x66, 0x69,
+	0x6c, 0x65, 0x73, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x12, 0x41, 0x0a, 0x0f, 0x4d,
+	0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x41, 0x75, 0x64, 0x69, 0x74, 0x49, 0x6e, 0x63, 0x12, 0x18,
+	0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x41, 0x75, 0x64, 0x69,
+	0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x45,
+	0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x40,
+	0x0a, 0x0d, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x53, 0x79, 0x73, 0x63, 0x61, 0x6c, 0x6c, 0x12,
+	0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x53, 0x79, 0x73, 0x63,
+	0x61, 0x6c, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x61, 0x70, 0x69,
+	0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00,
+	0x12, 0x39, 0x0a, 0x08, 0x53, 0x79, 0x73, 0x63, 0x61, 0x6c, 0x6c, 0x73, 0x12, 0x14, 0x2e, 0x61,
+	0x70, 0x69, 0x2e, 0x53, 0x79, 0x73, 0x63, 0x61, 0x6c, 0x6c, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
+	0x73, 0x74, 0x1a, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x79, 0x73, 0x63, 0x61, 0x6c, 0x6c,
+	0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x3b, 0x0a, 0x0d, 0x52,
+	0x65, 0x73, 0x65, 0x74, 0x53, 0x79, 0x73, 0x63, 0x61, 0x6c, 0x6c, 0x73, 0x12, 0x14, 0x2e, 0x61,
+	0x70, 0x69, 0x2e, 0x53, 0x79, 0x73, 0x63, 0x61, 0x6c, 0x6c, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
+	0x73, 0x74, 0x1a, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65,
+	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x06, 0x5a, 0x04, 0x2f, 0x61, 0x70, 0x69,
+	0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
@@ -259,18 +430,27 @@ func file_api_server_api_proto_rawDescGZIP() []byte {
 }
 
 var file_api_server_api_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
-var file_api_server_api_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
+var file_api_server_api_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
 var file_api_server_api_proto_goTypes = []interface{}{
 	(MetricsAuditRequest_Type)(0), // 0: api.MetricsAuditRequest.Type
 	(*MetricsAuditRequest)(nil),   // 1: api.MetricsAuditRequest
-	(*EmptyResponse)(nil),         // 2: api.EmptyResponse
+	(*RecordSyscallRequest)(nil),  // 2: api.RecordSyscallRequest
+	(*SyscallsRequest)(nil),       // 3: api.SyscallsRequest
+	(*SyscallsResponse)(nil),      // 4: api.SyscallsResponse
+	(*EmptyResponse)(nil),         // 5: api.EmptyResponse
 }
 var file_api_server_api_proto_depIdxs = []int32{
 	0, // 0: api.MetricsAuditRequest.type:type_name -> api.MetricsAuditRequest.Type
 	1, // 1: api.SecurityProfilesOperator.MetricsAuditInc:input_type -> api.MetricsAuditRequest
-	2, // 2: api.SecurityProfilesOperator.MetricsAuditInc:output_type -> api.EmptyResponse
-	2, // [2:3] is the sub-list for method output_type
-	1, // [1:2] is the sub-list for method input_type
+	2, // 2: api.SecurityProfilesOperator.RecordSyscall:input_type -> api.RecordSyscallRequest
+	3, // 3: api.SecurityProfilesOperator.Syscalls:input_type -> api.SyscallsRequest
+	3, // 4: api.SecurityProfilesOperator.ResetSyscalls:input_type -> api.SyscallsRequest
+	5, // 5: api.SecurityProfilesOperator.MetricsAuditInc:output_type -> api.EmptyResponse
+	5, // 6: api.SecurityProfilesOperator.RecordSyscall:output_type -> api.EmptyResponse
+	4, // 7: api.SecurityProfilesOperator.Syscalls:output_type -> api.SyscallsResponse
+	5, // 8: api.SecurityProfilesOperator.ResetSyscalls:output_type -> api.EmptyResponse
+	5, // [5:9] is the sub-list for method output_type
+	1, // [1:5] is the sub-list for method input_type
 	1, // [1:1] is the sub-list for extension type_name
 	1, // [1:1] is the sub-list for extension extendee
 	0, // [0:1] is the sub-list for field type_name
@@ -295,6 +475,42 @@ func file_api_server_api_proto_init() {
 			}
 		}
 		file_api_server_api_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*RecordSyscallRequest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_api_server_api_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*SyscallsRequest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_api_server_api_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*SyscallsResponse); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_api_server_api_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*EmptyResponse); i {
 			case 0:
 				return &v.state
@@ -313,7 +529,7 @@ func file_api_server_api_proto_init() {
 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
 			RawDescriptor: file_api_server_api_proto_rawDesc,
 			NumEnums:      1,
-			NumMessages:   2,
+			NumMessages:   5,
 			NumExtensions: 0,
 			NumServices:   1,
 		},
diff --git a/api/server/api.proto b/api/server/api.proto
index 00b08ddb9..3cc050d23 100644
--- a/api/server/api.proto
+++ b/api/server/api.proto
@@ -21,6 +21,9 @@ option go_package = "/api";
 
 service SecurityProfilesOperator {
     rpc MetricsAuditInc(MetricsAuditRequest) returns (EmptyResponse) {}
+    rpc RecordSyscall(RecordSyscallRequest) returns (EmptyResponse) {}
+    rpc Syscalls(SyscallsRequest) returns (SyscallsResponse) {}
+    rpc ResetSyscalls(SyscallsRequest) returns (EmptyResponse) {}
 }
 
 message MetricsAuditRequest {
@@ -37,4 +40,17 @@ message MetricsAuditRequest {
     string syscall = 7;
 }
 
+message RecordSyscallRequest {
+    string profile = 1;
+    string syscall = 2;
+}
+
+message SyscallsRequest {
+    string profile = 1;
+}
+
+message SyscallsResponse {
+    repeated string syscalls = 1;
+}
+
 message EmptyResponse {}
diff --git a/api/server/api_grpc.pb.go b/api/server/api_grpc.pb.go
index c91de561f..9cfa6c7ab 100644
--- a/api/server/api_grpc.pb.go
+++ b/api/server/api_grpc.pb.go
@@ -19,6 +19,9 @@ const _ = grpc.SupportPackageIsVersion7
 // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
 type SecurityProfilesOperatorClient interface {
 	MetricsAuditInc(ctx context.Context, in *MetricsAuditRequest, opts ...grpc.CallOption) (*EmptyResponse, error)
+	RecordSyscall(ctx context.Context, in *RecordSyscallRequest, opts ...grpc.CallOption) (*EmptyResponse, error)
+	Syscalls(ctx context.Context, in *SyscallsRequest, opts ...grpc.CallOption) (*SyscallsResponse, error)
+	ResetSyscalls(ctx context.Context, in *SyscallsRequest, opts ...grpc.CallOption) (*EmptyResponse, error)
 }
 
 type securityProfilesOperatorClient struct {
@@ -38,11 +41,41 @@ func (c *securityProfilesOperatorClient) MetricsAuditInc(ctx context.Context, in
 	return out, nil
 }
 
+func (c *securityProfilesOperatorClient) RecordSyscall(ctx context.Context, in *RecordSyscallRequest, opts ...grpc.CallOption) (*EmptyResponse, error) {
+	out := new(EmptyResponse)
+	err := c.cc.Invoke(ctx, "/api.SecurityProfilesOperator/RecordSyscall", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *securityProfilesOperatorClient) Syscalls(ctx context.Context, in *SyscallsRequest, opts ...grpc.CallOption) (*SyscallsResponse, error) {
+	out := new(SyscallsResponse)
+	err := c.cc.Invoke(ctx, "/api.SecurityProfilesOperator/Syscalls", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *securityProfilesOperatorClient) ResetSyscalls(ctx context.Context, in *SyscallsRequest, opts ...grpc.CallOption) (*EmptyResponse, error) {
+	out := new(EmptyResponse)
+	err := c.cc.Invoke(ctx, "/api.SecurityProfilesOperator/ResetSyscalls", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
 // SecurityProfilesOperatorServer is the server API for SecurityProfilesOperator service.
 // All implementations must embed UnimplementedSecurityProfilesOperatorServer
 // for forward compatibility
 type SecurityProfilesOperatorServer interface {
 	MetricsAuditInc(context.Context, *MetricsAuditRequest) (*EmptyResponse, error)
+	RecordSyscall(context.Context, *RecordSyscallRequest) (*EmptyResponse, error)
+	Syscalls(context.Context, *SyscallsRequest) (*SyscallsResponse, error)
+	ResetSyscalls(context.Context, *SyscallsRequest) (*EmptyResponse, error)
 	mustEmbedUnimplementedSecurityProfilesOperatorServer()
 }
 
@@ -53,6 +86,15 @@ type UnimplementedSecurityProfilesOperatorServer struct {
 func (UnimplementedSecurityProfilesOperatorServer) MetricsAuditInc(context.Context, *MetricsAuditRequest) (*EmptyResponse, error) {
 	return nil, status.Errorf(codes.Unimplemented, "method MetricsAuditInc not implemented")
 }
+func (UnimplementedSecurityProfilesOperatorServer) RecordSyscall(context.Context, *RecordSyscallRequest) (*EmptyResponse, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method RecordSyscall not implemented")
+}
+func (UnimplementedSecurityProfilesOperatorServer) Syscalls(context.Context, *SyscallsRequest) (*SyscallsResponse, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method Syscalls not implemented")
+}
+func (UnimplementedSecurityProfilesOperatorServer) ResetSyscalls(context.Context, *SyscallsRequest) (*EmptyResponse, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method ResetSyscalls not implemented")
+}
 func (UnimplementedSecurityProfilesOperatorServer) mustEmbedUnimplementedSecurityProfilesOperatorServer() {
 }
 
@@ -85,6 +127,60 @@ func _SecurityProfilesOperator_MetricsAuditInc_Handler(srv interface{}, ctx cont
 	return interceptor(ctx, in, info, handler)
 }
 
+func _SecurityProfilesOperator_RecordSyscall_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(RecordSyscallRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(SecurityProfilesOperatorServer).RecordSyscall(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/api.SecurityProfilesOperator/RecordSyscall",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(SecurityProfilesOperatorServer).RecordSyscall(ctx, req.(*RecordSyscallRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _SecurityProfilesOperator_Syscalls_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(SyscallsRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(SecurityProfilesOperatorServer).Syscalls(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/api.SecurityProfilesOperator/Syscalls",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(SecurityProfilesOperatorServer).Syscalls(ctx, req.(*SyscallsRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _SecurityProfilesOperator_ResetSyscalls_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(SyscallsRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(SecurityProfilesOperatorServer).ResetSyscalls(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/api.SecurityProfilesOperator/ResetSyscalls",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(SecurityProfilesOperatorServer).ResetSyscalls(ctx, req.(*SyscallsRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
 // SecurityProfilesOperator_ServiceDesc is the grpc.ServiceDesc for SecurityProfilesOperator service.
 // It's only intended for direct use with grpc.RegisterService,
 // and not to be introspected or modified (even as a copy)
@@ -96,6 +192,18 @@ var SecurityProfilesOperator_ServiceDesc = grpc.ServiceDesc{
 			MethodName: "MetricsAuditInc",
 			Handler:    _SecurityProfilesOperator_MetricsAuditInc_Handler,
 		},
+		{
+			MethodName: "RecordSyscall",
+			Handler:    _SecurityProfilesOperator_RecordSyscall_Handler,
+		},
+		{
+			MethodName: "Syscalls",
+			Handler:    _SecurityProfilesOperator_Syscalls_Handler,
+		},
+		{
+			MethodName: "ResetSyscalls",
+			Handler:    _SecurityProfilesOperator_ResetSyscalls_Handler,
+		},
 	},
 	Streams:  []grpc.StreamDesc{},
 	Metadata: "api/server/api.proto",
diff --git a/deploy/base/crds/profilerecording.yaml b/deploy/base/crds/profilerecording.yaml
index f03bb5bf8..9b067865b 100644
--- a/deploy/base/crds/profilerecording.yaml
+++ b/deploy/base/crds/profilerecording.yaml
@@ -92,9 +92,16 @@ spec:
                       are ANDed.
                     type: object
                 type: object
+              recorder:
+                description: Recorder to be used.
+                enum:
+                - hook
+                - logs
+                type: string
             required:
             - kind
             - podSelector
+            - recorder
             type: object
           status:
             description: ProfileRecordingStatus contains status of the ProfileRecording.
diff --git a/deploy/base/webhook_deployment.yaml b/deploy/base/webhook_deployment.yaml
index 7bd329437..8f25a60af 100644
--- a/deploy/base/webhook_deployment.yaml
+++ b/deploy/base/webhook_deployment.yaml
@@ -35,6 +35,11 @@ spec:
           - containerPort: 9443
             name: webhook
             protocol: TCP
+          env:
+            - name: OPERATOR_NAMESPACE
+              valueFrom:
+                fieldRef:
+                  fieldPath: metadata.namespace
       volumes:
       - name: cert
         secret:
diff --git a/deploy/namespace-operator.yaml b/deploy/namespace-operator.yaml
index 5120053f8..9d2110837 100644
--- a/deploy/namespace-operator.yaml
+++ b/deploy/namespace-operator.yaml
@@ -174,9 +174,16 @@ spec:
                       are ANDed.
                     type: object
                 type: object
+              recorder:
+                description: Recorder to be used.
+                enum:
+                - hook
+                - logs
+                type: string
             required:
             - kind
             - podSelector
+            - recorder
             type: object
           status:
             description: ProfileRecordingStatus contains status of the ProfileRecording.
@@ -1419,6 +1426,11 @@ spec:
       containers:
       - args:
         - webhook
+        env:
+        - name: OPERATOR_NAMESPACE
+          valueFrom:
+            fieldRef:
+              fieldPath: metadata.namespace
         image: gcr.io/k8s-staging-sp-operator/security-profiles-operator:latest
         imagePullPolicy: Always
         name: security-profiles-operator
diff --git a/deploy/operator.yaml b/deploy/operator.yaml
index fd8bd11c5..f0b1a57e8 100644
--- a/deploy/operator.yaml
+++ b/deploy/operator.yaml
@@ -174,9 +174,16 @@ spec:
                       are ANDed.
                     type: object
                 type: object
+              recorder:
+                description: Recorder to be used.
+                enum:
+                - hook
+                - logs
+                type: string
             required:
             - kind
             - podSelector
+            - recorder
             type: object
           status:
             description: ProfileRecordingStatus contains status of the ProfileRecording.
@@ -1417,6 +1424,11 @@ spec:
       containers:
       - args:
         - webhook
+        env:
+        - name: OPERATOR_NAMESPACE
+          valueFrom:
+            fieldRef:
+              fieldPath: metadata.namespace
         image: gcr.io/k8s-staging-sp-operator/security-profiles-operator:latest
         imagePullPolicy: Always
         name: security-profiles-operator
diff --git a/examples/profilerecording.yaml b/examples/profilerecording-hook.yaml
similarity index 94%
rename from examples/profilerecording.yaml
rename to examples/profilerecording-hook.yaml
index 5dc009414..5bf04f4ac 100644
--- a/examples/profilerecording.yaml
+++ b/examples/profilerecording-hook.yaml
@@ -7,6 +7,7 @@ metadata:
   name: test-recording
 spec:
   kind: SeccompProfile
+  recorder: hook
   podSelector:
     matchLabels:
       app: alpine
diff --git a/examples/profilerecording-logs.yaml b/examples/profilerecording-logs.yaml
new file mode 100644
index 000000000..64cb28ece
--- /dev/null
+++ b/examples/profilerecording-logs.yaml
@@ -0,0 +1,13 @@
+---
+apiVersion: security-profiles-operator.x-k8s.io/v1alpha1
+kind: ProfileRecording
+metadata:
+  # The name of the Recording is the same as the resulting `SeccompProfile` CRD
+  # after reconciliation.
+  name: test-recording
+spec:
+  kind: SeccompProfile
+  recorder: logs
+  podSelector:
+    matchLabels:
+      app: alpine
diff --git a/hack/deploy-localhost.patch b/hack/deploy-localhost.patch
index 9a05e3b43..a441933d5 100644
--- a/hack/deploy-localhost.patch
+++ b/hack/deploy-localhost.patch
@@ -1,8 +1,8 @@
 diff --git a/deploy/operator.yaml b/deploy/operator.yaml
-index fd8bd11..07c664e 100644
+index f0b1a57..eb44040 100644
 --- a/deploy/operator.yaml
 +++ b/deploy/operator.yaml
-@@ -1351,7 +1351,7 @@ metadata:
+@@ -1358,7 +1358,7 @@ metadata:
    name: security-profiles-operator
    namespace: security-profiles-operator
  spec:
@@ -11,7 +11,7 @@ index fd8bd11..07c664e 100644
    selector:
      matchLabels:
        app: security-profiles-operator
-@@ -1374,8 +1374,8 @@ spec:
+@@ -1381,8 +1381,8 @@ spec:
            valueFrom:
              fieldRef:
                fieldPath: metadata.namespace
@@ -22,7 +22,7 @@ index fd8bd11..07c664e 100644
          name: security-profiles-operator
          securityContext:
            allowPrivilegeEscalation: false
-@@ -1400,7 +1400,7 @@ metadata:
+@@ -1407,7 +1407,7 @@ metadata:
    name: security-profiles-operator-webhook
    namespace: security-profiles-operator
  spec:
@@ -31,10 +31,10 @@ index fd8bd11..07c664e 100644
    selector:
      matchLabels:
        app: security-profiles-operator
-@@ -1417,8 +1417,8 @@ spec:
-       containers:
-       - args:
-         - webhook
+@@ -1429,8 +1429,8 @@ spec:
+           valueFrom:
+             fieldRef:
+               fieldPath: metadata.namespace
 -        image: gcr.io/k8s-staging-sp-operator/security-profiles-operator:latest
 -        imagePullPolicy: Always
 +        image: localhost/security-profiles-operator:latest
diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go
index 313288f19..4b27772fc 100644
--- a/internal/pkg/config/config.go
+++ b/internal/pkg/config/config.go
@@ -57,16 +57,25 @@ const (
 	// the operator to work on only a single Kubernetes namespace.
 	RestrictNamespaceEnvKey = "RESTRICT_TO_NAMESPACE"
 
-	// SeccompProfileRecordAnnotationKey is the annotation on a Pod that
+	// SeccompProfileRecordHookAnnotationKey is the annotation on a Pod that
 	// triggers the oci-seccomp-bpf-hook to trace the syscalls of a Pod and
 	// created a seccomp profile.
-	SeccompProfileRecordAnnotationKey = "io.containers.trace-syscall"
+	SeccompProfileRecordHookAnnotationKey = "io.containers.trace-syscall/"
+
+	// SeccompProfileRecordLogsAnnotationKey is the annotation on a Pod that
+	// triggers the internal log enricher to trace the syscalls of a Pod and
+	// created a seccomp profile.
+	SeccompProfileRecordLogsAnnotationKey = "io.containers.trace-logs/"
 
 	// HealthProbePort is the port where the liveness probe will be served.
 	HealthProbePort = 8085
 
 	// AuditLogPath is the path to the auditd log file.
 	AuditLogPath = "/var/log/audit/audit.log"
+
+	// LogEnricherProfile is the seccomp profile name for tracing syscalls from
+	// the log enricher.
+	LogEnricherProfile = "log-enricher-trace"
 )
 
 // ProfileRecordingOutputPath is the path where the recorded profiles will be
diff --git a/internal/pkg/daemon/enricher/container.go b/internal/pkg/daemon/enricher/container.go
index 7237a230d..46bf70858 100644
--- a/internal/pkg/daemon/enricher/container.go
+++ b/internal/pkg/daemon/enricher/container.go
@@ -27,6 +27,7 @@ import (
 	"github.com/go-logr/logr"
 	"github.com/pkg/errors"
 
+	"sigs.k8s.io/security-profiles-operator/internal/pkg/config"
 	"sigs.k8s.io/security-profiles-operator/internal/pkg/util"
 )
 
@@ -45,17 +46,16 @@ func (e *Enricher) getContainerInfo(
 		return info.(*containerInfo), nil
 	}
 
-	config, err := e.impl.InClusterConfig()
+	clusterConfig, err := e.impl.InClusterConfig()
 	if err != nil {
 		return nil, errors.Wrap(err, "get in-cluster config")
 	}
 
-	clientset, err := e.impl.NewForConfig(config)
+	clientset, err := e.impl.NewForConfig(clusterConfig)
 	if err != nil {
 		return nil, errors.Wrap(err, "load in-cluster config")
 	}
 
-	containers := make(map[string]*containerInfo)
 	if err := util.Retry(
 		func() (retryErr error) {
 			pods, err := e.impl.ListPods(clientset, nodeName)
@@ -63,18 +63,19 @@ func (e *Enricher) getContainerInfo(
 				return errors.Wrapf(err, "list node %s's pods", nodeName)
 			}
 
-			containers = make(map[string]*containerInfo)
 			for p := range pods.Items {
-				pod := pods.Items[p]
+				pod := &pods.Items[p]
 
 				for c := range pod.Status.ContainerStatuses {
 					containerStatus := pod.Status.ContainerStatuses[c]
 					containerID := containerStatus.ContainerID
+					containerName := containerStatus.Name
 
 					if containerID == "" {
 						logger.Info(
 							"container ID is still empty, retrying",
-							"containerName", containerStatus.Name,
+							"podName", pod.Name,
+							"containerName", containerName,
 						)
 						return errContainerIDEmpty
 					}
@@ -83,21 +84,23 @@ func (e *Enricher) getContainerInfo(
 					if rawContainerID == "" {
 						logger.Error(
 							err, "unable to get container ID",
-							"containerName", pod.Name,
+							"podName", pod.Name,
+							"containerName", containerName,
 						)
 						continue
 					}
 
+					recordProfile := pod.Annotations[config.SeccompProfileRecordLogsAnnotationKey+containerName]
 					info := &containerInfo{
 						podName:       pod.Name,
 						containerName: containerStatus.Name,
 						namespace:     pod.Namespace,
 						containerID:   rawContainerID,
+						recordProfile: recordProfile,
 					}
-					containers[rawContainerID] = info
 
 					// Update the cache
-					if err := e.infoCache.Set(containerID, info); err != nil {
+					if err := e.infoCache.Set(rawContainerID, info); err != nil {
 						return errors.Wrap(err, "update cache")
 					}
 				}
@@ -111,12 +114,13 @@ func (e *Enricher) getContainerInfo(
 		return nil, errors.Wrap(err, "get container info for pods")
 	}
 
-	info, found := containers[containerID]
-	if !found {
-		return nil, errors.Wrap(err, "no container info for container ID")
+	if info, err := e.infoCache.Get(
+		containerID,
+	); !errors.Is(err, ttlcache.ErrNotFound) {
+		return info.(*containerInfo), nil
 	}
 
-	return info, err
+	return nil, errors.New("no container info for container ID")
 }
 
 func (e *Enricher) getContainerID(processID int) (string, error) {
diff --git a/internal/pkg/daemon/enricher/enricher.go b/internal/pkg/daemon/enricher/enricher.go
index 584388133..9e39bada2 100644
--- a/internal/pkg/daemon/enricher/enricher.go
+++ b/internal/pkg/daemon/enricher/enricher.go
@@ -164,6 +164,18 @@ func (e *Enricher) Run() error {
 		); err != nil {
 			e.logger.Error(err, "unable to update metrics")
 		}
+
+		if info.recordProfile != "" {
+			if _, err := e.impl.RecordSyscall(
+				client,
+				&api.RecordSyscallRequest{
+					Profile: info.recordProfile,
+					Syscall: syscallName,
+				},
+			); err != nil {
+				e.logger.Error(err, "unable to record syscall")
+			}
+		}
 	}
 
 	return errors.Wrap(tailFile.Err(), "enricher failed")
diff --git a/internal/pkg/daemon/enricher/enricherfakes/fake_impl.go b/internal/pkg/daemon/enricher/enricherfakes/fake_impl.go
index c4b115853..d2c841426 100644
--- a/internal/pkg/daemon/enricher/enricherfakes/fake_impl.go
+++ b/internal/pkg/daemon/enricher/enricherfakes/fake_impl.go
@@ -101,12 +101,11 @@ type FakeImpl struct {
 		result1 *v1.PodList
 		result2 error
 	}
-	MetricsAuditIncStub        func(api.SecurityProfilesOperatorClient, *api.MetricsAuditRequest, ...grpc.CallOption) (*api.EmptyResponse, error)
+	MetricsAuditIncStub        func(api.SecurityProfilesOperatorClient, *api.MetricsAuditRequest) (*api.EmptyResponse, error)
 	metricsAuditIncMutex       sync.RWMutex
 	metricsAuditIncArgsForCall []struct {
 		arg1 api.SecurityProfilesOperatorClient
 		arg2 *api.MetricsAuditRequest
-		arg3 []grpc.CallOption
 	}
 	metricsAuditIncReturns struct {
 		result1 *api.EmptyResponse
@@ -142,6 +141,20 @@ type FakeImpl struct {
 		result1 *os.File
 		result2 error
 	}
+	RecordSyscallStub        func(api.SecurityProfilesOperatorClient, *api.RecordSyscallRequest) (*api.EmptyResponse, error)
+	recordSyscallMutex       sync.RWMutex
+	recordSyscallArgsForCall []struct {
+		arg1 api.SecurityProfilesOperatorClient
+		arg2 *api.RecordSyscallRequest
+	}
+	recordSyscallReturns struct {
+		result1 *api.EmptyResponse
+		result2 error
+	}
+	recordSyscallReturnsOnCall map[int]struct {
+		result1 *api.EmptyResponse
+		result2 error
+	}
 	TailFileStub        func(string, tail.Config) (*tail.Tail, error)
 	tailFileMutex       sync.RWMutex
 	tailFileArgsForCall []struct {
@@ -520,20 +533,19 @@ func (fake *FakeImpl) ListPodsReturnsOnCall(i int, result1 *v1.PodList, result2
 	}{result1, result2}
 }
 
-func (fake *FakeImpl) MetricsAuditInc(arg1 api.SecurityProfilesOperatorClient, arg2 *api.MetricsAuditRequest, arg3 ...grpc.CallOption) (*api.EmptyResponse, error) {
+func (fake *FakeImpl) MetricsAuditInc(arg1 api.SecurityProfilesOperatorClient, arg2 *api.MetricsAuditRequest) (*api.EmptyResponse, error) {
 	fake.metricsAuditIncMutex.Lock()
 	ret, specificReturn := fake.metricsAuditIncReturnsOnCall[len(fake.metricsAuditIncArgsForCall)]
 	fake.metricsAuditIncArgsForCall = append(fake.metricsAuditIncArgsForCall, struct {
 		arg1 api.SecurityProfilesOperatorClient
 		arg2 *api.MetricsAuditRequest
-		arg3 []grpc.CallOption
-	}{arg1, arg2, arg3})
+	}{arg1, arg2})
 	stub := fake.MetricsAuditIncStub
 	fakeReturns := fake.metricsAuditIncReturns
-	fake.recordInvocation("MetricsAuditInc", []interface{}{arg1, arg2, arg3})
+	fake.recordInvocation("MetricsAuditInc", []interface{}{arg1, arg2})
 	fake.metricsAuditIncMutex.Unlock()
 	if stub != nil {
-		return stub(arg1, arg2, arg3...)
+		return stub(arg1, arg2)
 	}
 	if specificReturn {
 		return ret.result1, ret.result2
@@ -547,17 +559,17 @@ func (fake *FakeImpl) MetricsAuditIncCallCount() int {
 	return len(fake.metricsAuditIncArgsForCall)
 }
 
-func (fake *FakeImpl) MetricsAuditIncCalls(stub func(api.SecurityProfilesOperatorClient, *api.MetricsAuditRequest, ...grpc.CallOption) (*api.EmptyResponse, error)) {
+func (fake *FakeImpl) MetricsAuditIncCalls(stub func(api.SecurityProfilesOperatorClient, *api.MetricsAuditRequest) (*api.EmptyResponse, error)) {
 	fake.metricsAuditIncMutex.Lock()
 	defer fake.metricsAuditIncMutex.Unlock()
 	fake.MetricsAuditIncStub = stub
 }
 
-func (fake *FakeImpl) MetricsAuditIncArgsForCall(i int) (api.SecurityProfilesOperatorClient, *api.MetricsAuditRequest, []grpc.CallOption) {
+func (fake *FakeImpl) MetricsAuditIncArgsForCall(i int) (api.SecurityProfilesOperatorClient, *api.MetricsAuditRequest) {
 	fake.metricsAuditIncMutex.RLock()
 	defer fake.metricsAuditIncMutex.RUnlock()
 	argsForCall := fake.metricsAuditIncArgsForCall[i]
-	return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
+	return argsForCall.arg1, argsForCall.arg2
 }
 
 func (fake *FakeImpl) MetricsAuditIncReturns(result1 *api.EmptyResponse, result2 error) {
@@ -714,6 +726,71 @@ func (fake *FakeImpl) OpenReturnsOnCall(i int, result1 *os.File, result2 error)
 	}{result1, result2}
 }
 
+func (fake *FakeImpl) RecordSyscall(arg1 api.SecurityProfilesOperatorClient, arg2 *api.RecordSyscallRequest) (*api.EmptyResponse, error) {
+	fake.recordSyscallMutex.Lock()
+	ret, specificReturn := fake.recordSyscallReturnsOnCall[len(fake.recordSyscallArgsForCall)]
+	fake.recordSyscallArgsForCall = append(fake.recordSyscallArgsForCall, struct {
+		arg1 api.SecurityProfilesOperatorClient
+		arg2 *api.RecordSyscallRequest
+	}{arg1, arg2})
+	stub := fake.RecordSyscallStub
+	fakeReturns := fake.recordSyscallReturns
+	fake.recordInvocation("RecordSyscall", []interface{}{arg1, arg2})
+	fake.recordSyscallMutex.Unlock()
+	if stub != nil {
+		return stub(arg1, arg2)
+	}
+	if specificReturn {
+		return ret.result1, ret.result2
+	}
+	return fakeReturns.result1, fakeReturns.result2
+}
+
+func (fake *FakeImpl) RecordSyscallCallCount() int {
+	fake.recordSyscallMutex.RLock()
+	defer fake.recordSyscallMutex.RUnlock()
+	return len(fake.recordSyscallArgsForCall)
+}
+
+func (fake *FakeImpl) RecordSyscallCalls(stub func(api.SecurityProfilesOperatorClient, *api.RecordSyscallRequest) (*api.EmptyResponse, error)) {
+	fake.recordSyscallMutex.Lock()
+	defer fake.recordSyscallMutex.Unlock()
+	fake.RecordSyscallStub = stub
+}
+
+func (fake *FakeImpl) RecordSyscallArgsForCall(i int) (api.SecurityProfilesOperatorClient, *api.RecordSyscallRequest) {
+	fake.recordSyscallMutex.RLock()
+	defer fake.recordSyscallMutex.RUnlock()
+	argsForCall := fake.recordSyscallArgsForCall[i]
+	return argsForCall.arg1, argsForCall.arg2
+}
+
+func (fake *FakeImpl) RecordSyscallReturns(result1 *api.EmptyResponse, result2 error) {
+	fake.recordSyscallMutex.Lock()
+	defer fake.recordSyscallMutex.Unlock()
+	fake.RecordSyscallStub = nil
+	fake.recordSyscallReturns = struct {
+		result1 *api.EmptyResponse
+		result2 error
+	}{result1, result2}
+}
+
+func (fake *FakeImpl) RecordSyscallReturnsOnCall(i int, result1 *api.EmptyResponse, result2 error) {
+	fake.recordSyscallMutex.Lock()
+	defer fake.recordSyscallMutex.Unlock()
+	fake.RecordSyscallStub = nil
+	if fake.recordSyscallReturnsOnCall == nil {
+		fake.recordSyscallReturnsOnCall = make(map[int]struct {
+			result1 *api.EmptyResponse
+			result2 error
+		})
+	}
+	fake.recordSyscallReturnsOnCall[i] = struct {
+		result1 *api.EmptyResponse
+		result2 error
+	}{result1, result2}
+}
+
 func (fake *FakeImpl) TailFile(arg1 string, arg2 tail.Config) (*tail.Tail, error) {
 	fake.tailFileMutex.Lock()
 	ret, specificReturn := fake.tailFileReturnsOnCall[len(fake.tailFileArgsForCall)]
@@ -800,6 +877,8 @@ func (fake *FakeImpl) Invocations() map[string][][]interface{} {
 	defer fake.newForConfigMutex.RUnlock()
 	fake.openMutex.RLock()
 	defer fake.openMutex.RUnlock()
+	fake.recordSyscallMutex.RLock()
+	defer fake.recordSyscallMutex.RUnlock()
 	fake.tailFileMutex.RLock()
 	defer fake.tailFileMutex.RUnlock()
 	copiedInvocations := map[string][][]interface{}{}
diff --git a/internal/pkg/daemon/enricher/impl.go b/internal/pkg/daemon/enricher/impl.go
index 5b0ae2382..191996a67 100644
--- a/internal/pkg/daemon/enricher/impl.go
+++ b/internal/pkg/daemon/enricher/impl.go
@@ -48,7 +48,10 @@ type impl interface {
 	MetricsAuditInc(
 		client api.SecurityProfilesOperatorClient,
 		in *api.MetricsAuditRequest,
-		opts ...grpc.CallOption,
+	) (*api.EmptyResponse, error)
+	RecordSyscall(
+		client api.SecurityProfilesOperatorClient,
+		in *api.RecordSyscallRequest,
 	) (*api.EmptyResponse, error)
 }
 
@@ -99,7 +102,13 @@ func (d *defaultImpl) ListPods(
 func (d *defaultImpl) MetricsAuditInc(
 	client api.SecurityProfilesOperatorClient,
 	in *api.MetricsAuditRequest,
-	opts ...grpc.CallOption,
 ) (*api.EmptyResponse, error) {
-	return client.MetricsAuditInc(context.Background(), in, opts...)
+	return client.MetricsAuditInc(context.Background(), in)
+}
+
+func (d *defaultImpl) RecordSyscall(
+	client api.SecurityProfilesOperatorClient,
+	in *api.RecordSyscallRequest,
+) (*api.EmptyResponse, error) {
+	return client.RecordSyscall(context.Background(), in)
 }
diff --git a/internal/pkg/daemon/enricher/types.go b/internal/pkg/daemon/enricher/types.go
index 4659d7ce1..43a8292d7 100644
--- a/internal/pkg/daemon/enricher/types.go
+++ b/internal/pkg/daemon/enricher/types.go
@@ -41,6 +41,7 @@ type containerInfo struct {
 	containerName string
 	namespace     string
 	containerID   string
+	recordProfile string
 }
 
 // List of x86-x64 syscalls
diff --git a/internal/pkg/daemon/profilerecorder/profilerecorder.go b/internal/pkg/daemon/profilerecorder/profilerecorder.go
index d31e0e919..ed4d7dbd9 100644
--- a/internal/pkg/daemon/profilerecorder/profilerecorder.go
+++ b/internal/pkg/daemon/profilerecorder/profilerecorder.go
@@ -28,10 +28,12 @@ import (
 	"sync"
 	"time"
 
+	"github.com/containers/common/pkg/seccomp"
 	"github.com/crossplane/crossplane-runtime/pkg/event"
 	"github.com/crossplane/crossplane-runtime/pkg/resource"
 	"github.com/go-logr/logr"
 	"github.com/pkg/errors"
+	"google.golang.org/grpc"
 	corev1 "k8s.io/api/core/v1"
 	kerrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -46,9 +48,11 @@ import (
 
 	profilerecording1alpha1 "sigs.k8s.io/security-profiles-operator/api/profilerecording/v1alpha1"
 	"sigs.k8s.io/security-profiles-operator/api/seccompprofile/v1alpha1"
+	api "sigs.k8s.io/security-profiles-operator/api/server"
 	"sigs.k8s.io/security-profiles-operator/internal/pkg/config"
 	"sigs.k8s.io/security-profiles-operator/internal/pkg/controller"
 	"sigs.k8s.io/security-profiles-operator/internal/pkg/daemon/metrics"
+	"sigs.k8s.io/security-profiles-operator/internal/pkg/daemon/server"
 )
 
 const (
@@ -73,12 +77,18 @@ func NewController() controller.Controller {
 
 type RecorderReconciler struct {
 	client        client.Client
+	grpcClient    api.SecurityProfilesOperatorClient
 	log           logr.Logger
 	record        event.Recorder
 	nodeAddresses []string
 	podsToWatch   sync.Map
 }
 
+type podToWatch struct {
+	recorder profilerecording1alpha1.ProfileRecorder
+	profiles []string
+}
+
 // Name returns the name of the controller.
 func (r *RecorderReconciler) Name() string {
 	return "recorder-spod"
@@ -126,6 +136,13 @@ func (r *RecorderReconciler) Setup(
 		return errors.New("Unable to get node's internal Address")
 	}
 
+	r.log.Info("Connecting to local GRPC server")
+	conn, err := grpc.Dial(server.Addr(), grpc.WithInsecure())
+	if err != nil {
+		return errors.Wrap(err, "connecting to local GRPC server")
+	}
+
+	r.grpcClient = api.NewSecurityProfilesOperatorClient(conn)
 	r.client = mgr.GetClient()
 	r.nodeAddresses = nodeAddresses
 	r.record = event.NewAPIRecorder(mgr.GetEventRecorderFor(name))
@@ -169,7 +186,8 @@ func (r *RecorderReconciler) isPodWithTraceAnnotation(obj runtime.Object) bool {
 	}
 
 	for key := range p.Annotations {
-		if strings.HasPrefix(key, config.SeccompProfileRecordAnnotationKey+"/") {
+		if strings.HasPrefix(key, config.SeccompProfileRecordHookAnnotationKey) ||
+			strings.HasPrefix(key, config.SeccompProfileRecordLogsAnnotationKey) {
 			return true
 		}
 	}
@@ -203,19 +221,46 @@ func (r *RecorderReconciler) Reconcile(_ context.Context, req reconcile.Request)
 			return reconcile.Result{}, nil
 		}
 
-		outputFiles, err := parseAnnotations(pod.Annotations)
+		hookProfiles, err := parseHookAnnotations(pod.Annotations)
+		if err != nil {
+			// Malformed annotations could be set by users directly, which is
+			// why we are ignoring them.
+			logger.Info("Ignoring because unable to parse hook annotation", "error", err)
+			r.record.Event(pod, event.Warning(reasonAnnotationParsing, err))
+			return reconcile.Result{}, nil
+		}
+
+		logProfiles, err := parseLogAnnotations(pod.Annotations)
 		if err != nil {
 			// Malformed annotations could be set by users directly, which is
 			// why we are ignoring them.
-			logger.Info("Ignoring because unable to parse annotation", "error", err)
+			logger.Info("Ignoring because unable to parse log annotation", "error", err)
 			r.record.Event(pod, event.Warning(reasonAnnotationParsing, err))
 			return reconcile.Result{}, nil
 		}
 
+		var profiles []string
+		var recorder profilerecording1alpha1.ProfileRecorder
+		if len(hookProfiles) > 0 { // nolint: gocritic
+			profiles = hookProfiles
+			recorder = profilerecording1alpha1.ProfileRecorderHook
+		} else if len(logProfiles) > 0 {
+			profiles = logProfiles
+			recorder = profilerecording1alpha1.ProfileRecorderLogs
+		} else {
+			logger.Info("Neither hook nor log annotations found on pod")
+			return reconcile.Result{}, nil
+		}
+
 		logger.Info(fmt.Sprintf(
-			"Recording seccomp profiles: %s", strings.Join(outputFiles, ", "),
+			"Recording seccomp profiles for pod %s: %s",
+			req.NamespacedName.String(),
+			strings.Join(profiles, ", "),
 		))
-		r.podsToWatch.Store(req.NamespacedName.String(), outputFiles)
+		r.podsToWatch.Store(
+			req.NamespacedName.String(),
+			podToWatch{recorder, profiles},
+		)
 		r.record.Event(pod, event.Normal(reasonProfileRecording, "Recording seccomp profiles"))
 	}
 
@@ -228,50 +273,150 @@ func (r *RecorderReconciler) Reconcile(_ context.Context, req reconcile.Request)
 	return reconcile.Result{}, nil
 }
 
-func (r *RecorderReconciler) collectProfile(ctx context.Context, name types.NamespacedName) (res error) {
-	defer r.podsToWatch.Delete(name.String())
+func (r *RecorderReconciler) collectProfile(
+	ctx context.Context, name types.NamespacedName,
+) error {
+	n := name.String()
 
-	if value, ok := r.podsToWatch.Load(name.String()); ok {
-		for _, profilePath := range value.([]string) {
-			r.log.Info("Collecting profile", "path", profilePath)
+	value, ok := r.podsToWatch.Load(n)
+	if !ok {
+		return nil
+	}
 
-			data, err := ioutil.ReadFile(profilePath)
-			if err != nil {
-				r.log.Error(err, "Failed to read profile")
-				return errors.Wrap(err, "read profile")
-			}
+	podToWatch, ok := value.(podToWatch)
+	if !ok {
+		return errors.New("type assert pod to watch")
+	}
 
-			// Remove the file extension and timestamp
-			base := filepath.Base(profilePath)
-			lastIndex := strings.LastIndex(base, "-")
-			if lastIndex == -1 {
-				return errors.Errorf("malformed profile path: %s", profilePath)
-			}
-			profileName := base[:lastIndex]
+	if podToWatch.recorder == profilerecording1alpha1.ProfileRecorderHook {
+		if err := r.collectLocalProfiles(
+			ctx, name, podToWatch.profiles,
+		); err != nil {
+			return errors.Wrap(err, "collect local profile")
+		}
+	}
 
-			profile := &v1alpha1.SeccompProfile{
-				ObjectMeta: metav1.ObjectMeta{
-					Name:      profileName,
-					Namespace: name.Namespace,
-				},
-			}
-			res, err := controllerutil.CreateOrUpdate(ctx, r.client, profile,
-				func() error {
-					return errors.Wrap(
-						json.Unmarshal(data, &profile.Spec),
-						"unmarshal profile spec JSON",
-					)
-				},
+	if podToWatch.recorder == profilerecording1alpha1.ProfileRecorderLogs {
+		if err := r.collectLogProfiles(
+			ctx, name, podToWatch.profiles,
+		); err != nil {
+			return errors.Wrap(err, "collect log profile")
+		}
+	}
+
+	r.podsToWatch.Delete(n)
+	return nil
+}
+
+func (r *RecorderReconciler) collectLocalProfiles(
+	ctx context.Context,
+	name types.NamespacedName,
+	profiles []string,
+) error {
+	for _, profilePath := range profiles {
+		r.log.Info("Collecting profile", "path", profilePath)
+
+		data, err := ioutil.ReadFile(profilePath)
+		if err != nil {
+			r.log.Error(err, "Failed to read profile")
+			return errors.Wrap(err, "read profile")
+		}
+
+		// Remove the file extension and timestamp
+		profileName, err := extractProfileName(filepath.Base(profilePath))
+		if err != nil {
+			return errors.Wrap(err, "extract profile name")
+		}
+
+		profile := &v1alpha1.SeccompProfile{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      profileName,
+				Namespace: name.Namespace,
+			},
+		}
+		res, err := controllerutil.CreateOrUpdate(ctx, r.client, profile,
+			func() error {
+				return errors.Wrap(
+					json.Unmarshal(data, &profile.Spec),
+					"unmarshal profile spec JSON",
+				)
+			},
+		)
+		if err != nil {
+			r.log.Error(err, "Cannot create seccompprofile resource")
+			r.record.Event(profile, event.Warning(reasonProfileCreationFailed, err))
+			return errors.Wrap(err, "create seccompProfile resource")
+		}
+		r.log.Info("Created/updated profile", "action", res, "name", profileName)
+		r.record.Event(
+			profile,
+			event.Normal(reasonProfileCreated, "seccomp profile created"),
+		)
+	}
+
+	return nil
+}
+
+func (r *RecorderReconciler) collectLogProfiles(
+	ctx context.Context,
+	name types.NamespacedName,
+	profiles []string,
+) error {
+	for _, profileID := range profiles {
+		// Remove the timestamp
+		profileName, err := extractProfileName(profileID)
+		if err != nil {
+			return errors.Wrap(err, "extract profile name")
+		}
+
+		r.log.Info("Collecting profile", "name", profileName)
+
+		// Retrieve the syscalls for the recording
+		request := &api.SyscallsRequest{Profile: profileID}
+		response, err := r.grpcClient.Syscalls(ctx, request)
+		if err != nil {
+			return errors.Wrapf(
+				err, "retrieve syscalls for profile %s", profileID,
 			)
-			if err != nil {
-				r.log.Error(err, "Cannot create seccompprofile resource")
-				r.record.Event(profile, event.Warning(reasonProfileCreationFailed, err))
-				return errors.Wrap(err, "create seccompProfile resource")
-			}
-			r.log.Info("Created/updated profile", "action", res, "name", profileName)
-			r.record.Event(
-				profile,
-				event.Normal(reasonProfileCreated, "seccomp profile created"),
+		}
+
+		profileSpec := v1alpha1.SeccompProfileSpec{
+			DefaultAction: seccomp.ActErrno,
+			Syscalls: []*v1alpha1.Syscall{{
+				Action: seccomp.ActAllow,
+				Names:  response.GetSyscalls(),
+			}},
+		}
+
+		profile := &v1alpha1.SeccompProfile{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      profileName,
+				Namespace: name.Namespace,
+			},
+			Spec: profileSpec,
+		}
+
+		res, err := controllerutil.CreateOrUpdate(ctx, r.client, profile,
+			func() error {
+				profile.Spec = profileSpec
+				return nil
+			},
+		)
+		if err != nil {
+			r.log.Error(err, "Cannot create seccompprofile resource")
+			r.record.Event(profile, event.Warning(reasonProfileCreationFailed, err))
+			return errors.Wrap(err, "create seccompProfile resource")
+		}
+		r.log.Info("Created/updated profile", "action", res, "name", profileName)
+		r.record.Event(
+			profile,
+			event.Normal(reasonProfileCreated, "seccomp profile created"),
+		)
+
+		// Reset the syscalls for further recordings
+		if _, err := r.grpcClient.ResetSyscalls(ctx, request); err != nil {
+			return errors.Wrapf(
+				err, "reset syscalls for profile %s", profileID,
 			)
 		}
 	}
@@ -279,13 +424,21 @@ func (r *RecorderReconciler) collectProfile(ctx context.Context, name types.Name
 	return nil
 }
 
-// parseAnnotations parses the provided annotations and extracts the mandatory
-// output files.
-func parseAnnotations(annotations map[string]string) (res []string, err error) {
+func extractProfileName(s string) (string, error) {
+	lastIndex := strings.LastIndex(s, "-")
+	if lastIndex == -1 {
+		return "", errors.Errorf("malformed profile path: %s", s)
+	}
+	return s[:lastIndex], nil
+}
+
+// parseHookAnnotations parses the provided annotations and extracts the
+// mandatory output files for the hook recorder.
+func parseHookAnnotations(annotations map[string]string) (res []string, err error) {
 	const prefix = "of:"
 
 	for key, value := range annotations {
-		if !strings.HasPrefix(key, config.SeccompProfileRecordAnnotationKey+"/") {
+		if !strings.HasPrefix(key, config.SeccompProfileRecordHookAnnotationKey) {
 			continue
 		}
 
@@ -315,3 +468,22 @@ func parseAnnotations(annotations map[string]string) (res []string, err error) {
 
 	return res, nil
 }
+
+// parseLogAnnotations parses the provided annotations and extracts the
+// mandatory output profiles for the log recorder.
+func parseLogAnnotations(annotations map[string]string) (res []string, err error) {
+	for key, profile := range annotations {
+		if !strings.HasPrefix(key, config.SeccompProfileRecordLogsAnnotationKey) {
+			continue
+		}
+
+		if profile == "" {
+			return nil, errors.Wrap(errors.New(errInvalidAnnotation),
+				"providing output profile is mandatory")
+		}
+
+		res = append(res, profile)
+	}
+
+	return res, nil
+}
diff --git a/internal/pkg/daemon/server/server.go b/internal/pkg/daemon/server/server.go
index 59d7e40d6..216231c5f 100644
--- a/internal/pkg/daemon/server/server.go
+++ b/internal/pkg/daemon/server/server.go
@@ -19,10 +19,12 @@ package server
 import (
 	"context"
 	"net"
+	"sync"
 
 	"github.com/go-logr/logr"
 	"github.com/pkg/errors"
 	"google.golang.org/grpc"
+	"k8s.io/apimachinery/pkg/util/sets"
 
 	api "sigs.k8s.io/security-profiles-operator/api/server"
 	"sigs.k8s.io/security-profiles-operator/internal/pkg/daemon/metrics"
@@ -35,8 +37,9 @@ const (
 // Server is the main structure of this package.
 type Server struct {
 	api.UnimplementedSecurityProfilesOperatorServer
-	logger  logr.Logger
-	metrics *metrics.Metrics
+	logger   logr.Logger
+	metrics  *metrics.Metrics
+	syscalls sync.Map
 }
 
 // New creates a new Server instance.
@@ -81,7 +84,6 @@ func (s *Server) MetricsAuditInc(
 	ctx context.Context, r *api.MetricsAuditRequest,
 ) (*api.EmptyResponse, error) {
 	if r.GetType() == api.MetricsAuditRequest_SECCOMP {
-		s.logger.Info("Updating seccomp audit metrics")
 		s.metrics.IncSeccompProfileAudit(
 			r.GetNode(),
 			r.GetNamespace(),
@@ -91,7 +93,6 @@ func (s *Server) MetricsAuditInc(
 			r.GetSyscall(),
 		)
 	} else if r.GetType() == api.MetricsAuditRequest_SELINUX {
-		s.logger.Info("Updating selinux audit metrics")
 		s.metrics.IncSelinuxProfileAudit(
 			r.GetNode(),
 			r.GetNamespace(),
@@ -103,3 +104,35 @@ func (s *Server) MetricsAuditInc(
 	}
 	return &api.EmptyResponse{}, nil
 }
+
+// RecordSyscall traces a single syscall for a provided profile.
+func (s *Server) RecordSyscall(
+	ctx context.Context, r *api.RecordSyscallRequest,
+) (*api.EmptyResponse, error) {
+	syscalls, _ := s.syscalls.LoadOrStore(r.GetProfile(), sets.NewString())
+	syscalls.(sets.String).Insert(r.GetSyscall())
+	return &api.EmptyResponse{}, nil
+}
+
+// Syscalls returns the syscalls for a provided profile.
+func (s *Server) Syscalls(
+	ctx context.Context, r *api.SyscallsRequest,
+) (*api.SyscallsResponse, error) {
+	syscalls, ok := s.syscalls.Load(r.GetProfile())
+	if !ok {
+		return nil, errors.Errorf(
+			"no syscalls recorded for profile: %v", r.GetProfile(),
+		)
+	}
+	return &api.SyscallsResponse{
+		Syscalls: syscalls.(sets.String).List(),
+	}, nil
+}
+
+// ResetSyscalls removes the syscalls for a provided profile.
+func (s *Server) ResetSyscalls(
+	ctx context.Context, r *api.SyscallsRequest,
+) (*api.EmptyResponse, error) {
+	s.syscalls.Delete(r.GetProfile())
+	return &api.EmptyResponse{}, nil
+}
diff --git a/internal/pkg/manager/spod/bindata/default_profiles.go b/internal/pkg/manager/spod/bindata/default_profiles.go
index 3f26afb86..129f338ec 100644
--- a/internal/pkg/manager/spod/bindata/default_profiles.go
+++ b/internal/pkg/manager/spod/bindata/default_profiles.go
@@ -32,14 +32,24 @@ var (
 
 // DefaultProfiles returns the default profiles deployed by the operator.
 func DefaultProfiles() []*v1alpha1.SeccompProfile {
+	namespace := config.GetOperatorNamespace()
+	labels := map[string]string{"app": config.OperatorName}
 	return []*v1alpha1.SeccompProfile{
+		{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      config.LogEnricherProfile,
+				Namespace: namespace,
+				Labels:    labels,
+			},
+			Spec: v1alpha1.SeccompProfileSpec{
+				DefaultAction: seccomp.ActLog,
+			},
+		},
 		{
 			ObjectMeta: metav1.ObjectMeta{
 				Name:      "nginx-1.19.1",
-				Namespace: config.GetOperatorNamespace(),
-				Labels: map[string]string{
-					"app": config.OperatorName,
-				},
+				Namespace: namespace,
+				Labels:    labels,
 			},
 			Spec: v1alpha1.SeccompProfileSpec{
 				DefaultAction: seccomp.ActErrno,
diff --git a/internal/pkg/webhooks/recording/recording.go b/internal/pkg/webhooks/recording/recording.go
index 60de139f3..8adde4c13 100644
--- a/internal/pkg/webhooks/recording/recording.go
+++ b/internal/pkg/webhooks/recording/recording.go
@@ -137,7 +137,10 @@ func (p *podSeccompRecorder) Handle(
 		}
 
 		if selector.Matches(podLabels) {
-			podChanged = p.addAnnotations(pod, &item)
+			podChanged, err = p.updatePod(pod, &item)
+			if err != nil {
+				return admission.Errored(http.StatusInternalServerError, err)
+			}
 		}
 
 		if podChanged {
@@ -160,13 +163,18 @@ func (p *podSeccompRecorder) Handle(
 	return admission.PatchResponseFromRaw(req.Object.Raw, marshaledPod)
 }
 
-func (p *podSeccompRecorder) addAnnotations(
+func (p *podSeccompRecorder) updatePod(
 	pod *corev1.Pod,
 	profileRecording *profilerecordingv1alpha1.ProfileRecording,
-) (podChanged bool) {
-	ctrs := []corev1.Container{}
-	ctrs = append(ctrs, pod.Spec.InitContainers...)
-	ctrs = append(ctrs, pod.Spec.Containers...)
+) (podChanged bool, err error) {
+	// Collect containers as references to not copy them during modification
+	ctrs := []*corev1.Container{}
+	for i := range pod.Spec.InitContainers {
+		ctrs = append(ctrs, &pod.Spec.InitContainers[i])
+	}
+	for i := range pod.Spec.Containers {
+		ctrs = append(ctrs, &pod.Spec.Containers[i])
+	}
 
 	// Handle replicas by tracking them
 	replica := ""
@@ -177,22 +185,43 @@ func (p *podSeccompRecorder) addAnnotations(
 	}
 
 	for i := range ctrs {
-		ctr := &ctrs[i]
-		key := fmt.Sprintf("%s/%s", config.SeccompProfileRecordAnnotationKey, ctr.Name)
+		ctr := ctrs[i]
 
 		ctrName := ctr.Name
 		if replica != "" {
 			ctrName += replica
 		}
 
-		value := fmt.Sprintf(
-			"of:%s/%s-%s-%d.json",
-			config.ProfileRecordingOutputPath,
-			profileRecording.GetName(),
-			ctrName,
-			time.Now().Unix(),
-		)
+		// Distinguish the profile recorders by using different annotation prefixes
+		var annotationPrefix, value string
+		switch profileRecording.Spec.Recorder {
+		case profilerecordingv1alpha1.ProfileRecorderHook:
+			annotationPrefix = config.SeccompProfileRecordHookAnnotationKey
+			value = fmt.Sprintf(
+				"of:%s/%s-%s-%d.json",
+				config.ProfileRecordingOutputPath,
+				profileRecording.GetName(),
+				ctrName,
+				time.Now().Unix(),
+			)
+
+		case profilerecordingv1alpha1.ProfileRecorderLogs:
+			annotationPrefix = config.SeccompProfileRecordLogsAnnotationKey
+			value = fmt.Sprintf(
+				"%s-%s-%d",
+				profileRecording.GetName(),
+				ctrName,
+				time.Now().Unix(),
+			)
+			p.updateSecurityContext(ctr)
 
+		default:
+			return false, errors.Errorf(
+				"invalid recorder: %s", profileRecording.Spec.Recorder,
+			)
+		}
+
+		key := annotationPrefix + ctr.Name
 		existingValue, ok := pod.GetAnnotations()[key]
 		if !ok {
 			if pod.Annotations == nil {
@@ -220,7 +249,32 @@ func (p *podSeccompRecorder) addAnnotations(
 		}
 	}
 
-	return podChanged
+	return podChanged, nil
+}
+
+func (p *podSeccompRecorder) updateSecurityContext(
+	ctr *corev1.Container,
+) {
+	if ctr.SecurityContext == nil {
+		ctr.SecurityContext = &corev1.SecurityContext{}
+	}
+
+	if ctr.SecurityContext.SeccompProfile == nil {
+		ctr.SecurityContext.SeccompProfile = &corev1.SeccompProfile{}
+	}
+
+	ctr.SecurityContext.SeccompProfile.Type = corev1.SeccompProfileTypeLocalhost
+	profile := fmt.Sprintf(
+		"operator/%s/%s.json",
+		config.GetOperatorNamespace(),
+		config.LogEnricherProfile,
+	)
+	ctr.SecurityContext.SeccompProfile.LocalhostProfile = &profile
+
+	p.log.Info(fmt.Sprintf(
+		"set SecurityContext for container %s: %+v",
+		ctr.Name, ctr.SecurityContext,
+	))
 }
 
 func (p *podSeccompRecorder) cleanupReplicas(podName string) {


Test patch:
diff --git a/internal/pkg/daemon/enricher/enricher_test.go b/internal/pkg/daemon/enricher/enricher_test.go
index c8e745ca2..6784ebe39 100644
--- a/internal/pkg/daemon/enricher/enricher_test.go
+++ b/internal/pkg/daemon/enricher/enricher_test.go
@@ -97,7 +97,7 @@ func TestRun(t *testing.T) {
 					// Wait for MetricsAuditIncCallCount to be called
 				}
 
-				_, res, _ := mock.MetricsAuditIncArgsForCall(0)
+				_, res := mock.MetricsAuditIncArgsForCall(0)
 				require.Equal(t, node, res.Node)
 				require.Equal(t, namespace, res.Namespace)
 				require.Equal(t, pod, res.Pod)
diff --git a/internal/pkg/webhooks/recording/recording_test.go b/internal/pkg/webhooks/recording/recording_test.go
index 5ae5f2c91..fb059fd99 100644
--- a/internal/pkg/webhooks/recording/recording_test.go
+++ b/internal/pkg/webhooks/recording/recording_test.go
@@ -94,7 +94,8 @@ func TestHandle(t *testing.T) {
 					Items: []v1alpha1.ProfileRecording{
 						{
 							Spec: v1alpha1.ProfileRecordingSpec{
-								Kind: v1alpha1.ProfileRecordingKindSeccompProfile,
+								Kind:     v1alpha1.ProfileRecordingKindSeccompProfile,
+								Recorder: v1alpha1.ProfileRecorderHook,
 							},
 						},
 					},
@@ -190,7 +191,8 @@ func TestHandle(t *testing.T) {
 					Items: []v1alpha1.ProfileRecording{
 						{
 							Spec: v1alpha1.ProfileRecordingSpec{
-								Kind: v1alpha1.ProfileRecordingKindSeccompProfile,
+								Kind:     v1alpha1.ProfileRecordingKindSeccompProfile,
+								Recorder: v1alpha1.ProfileRecorderHook,
 							},
 						},
 					},
diff --git a/test/e2e_test.go b/test/e2e_test.go
index 0c8886e25..81b590c4f 100644
--- a/test/e2e_test.go
+++ b/test/e2e_test.go
@@ -124,7 +124,8 @@ func (e *e2e) TestSecurityProfilesOperator() {
 	})
 
 	e.Run("cluster-wide: Seccomp: Verify profile recording", func() {
-		e.testCaseProfileRecordingStaticPod()
+		e.testCaseProfileRecordingStaticPodHook()
+		e.testCaseProfileRecordingStaticPodLogs()
 		e.testCaseProfileRecordingKubectlRun()
 		e.testCaseProfileRecordingMultiContainer()
 		e.testCaseProfileRecordingDeployment()
diff --git a/test/tc_profilerecordings_test.go b/test/tc_profilerecordings_test.go
index ceda9e13b..48a4dac48 100644
--- a/test/tc_profilerecordings_test.go
+++ b/test/tc_profilerecordings_test.go
@@ -19,19 +19,29 @@ package e2e_test
 import (
 	"io/ioutil"
 	"os"
+	"time"
 )
 
 const (
-	exampleRecordingPath = "examples/profilerecording.yaml"
-	recordingName        = "test-recording"
+	exampleRecordingHookPath = "examples/profilerecording-hook.yaml"
+	exampleRecordingLogsPath = "examples/profilerecording-logs.yaml"
+	recordingName            = "test-recording"
 )
 
-func (e *e2e) testCaseProfileRecordingStaticPod() {
+func (e *e2e) testCaseProfileRecordingStaticPodHook() {
 	e.profileRecordingTestCase()
+	e.profileRecordingStaticPod(exampleRecordingHookPath, 0)
+}
+
+func (e *e2e) testCaseProfileRecordingStaticPodLogs() {
+	e.profileRecordingTestCase()
+	e.logEnricherOnlyTestCase()
+	e.profileRecordingStaticPod(exampleRecordingLogsPath, 10*time.Second)
+}
 
+func (e *e2e) profileRecordingStaticPod(recording string, sleepTime time.Duration) {
 	e.logf("Creating recording for static pod test")
-	e.kubectl("create", "-f", exampleRecordingPath)
-	defer e.kubectl("delete", "-f", exampleRecordingPath)
+	e.kubectl("create", "-f", recording)
 
 	e.logf("Creating test pod")
 	const testPod = `
@@ -49,7 +59,6 @@ spec:
 `
 	testPodFile, err := ioutil.TempFile(os.TempDir(), "recording-pod*.yaml")
 	e.Nil(err)
-	defer os.Remove(testPodFile.Name())
 	_, err = testPodFile.Write([]byte(testPod))
 	e.Nil(err)
 	err = testPodFile.Close()
@@ -60,20 +69,25 @@ spec:
 	const podName = "recording"
 	e.retryGet("pod", podName)
 	e.waitFor("condition=ready", "pod", podName)
+
+	time.Sleep(sleepTime)
 	e.kubectl("delete", "pod", podName)
 
 	resourceName := recordingName + "-nginx"
 	profile := e.retryGetSeccompProfile(resourceName)
-	defer e.kubectl("delete", "sp", resourceName)
-	e.Contains(profile, "mkdir")
+	e.Contains(profile, "close")
+
+	e.kubectl("delete", "-f", recording)
+	e.Nil(os.Remove(testPodFile.Name()))
+	e.kubectl("delete", "sp", resourceName)
 }
 
 func (e *e2e) testCaseProfileRecordingKubectlRun() {
 	e.profileRecordingTestCase()
 
 	e.logf("Creating recording for kubectl run test")
-	e.kubectl("create", "-f", exampleRecordingPath)
-	defer e.kubectl("delete", "-f", exampleRecordingPath)
+	e.kubectl("create", "-f", exampleRecordingHookPath)
+	defer e.kubectl("delete", "-f", exampleRecordingHookPath)
 
 	e.logf("Creating test pod")
 	e.kubectlRun("--labels=app=alpine", "fedora", "--", "echo", "test")
@@ -89,8 +103,8 @@ func (e *e2e) testCaseProfileRecordingMultiContainer() {
 	e.profileRecordingTestCase()
 
 	e.logf("Creating recording for multi container test")
-	e.kubectl("create", "-f", exampleRecordingPath)
-	defer e.kubectl("delete", "-f", exampleRecordingPath)
+	e.kubectl("create", "-f", exampleRecordingHookPath)
+	defer e.kubectl("delete", "-f", exampleRecordingHookPath)
 
 	e.logf("Creating test pod")
 	const testPod = `
@@ -134,8 +148,8 @@ func (e *e2e) testCaseProfileRecordingDeployment() {
 	e.profileRecordingTestCase()
 
 	e.logf("Creating recording for deployment test")
-	e.kubectl("create", "-f", exampleRecordingPath)
-	defer e.kubectl("delete", "-f", exampleRecordingPath)
+	e.kubectl("create", "-f", exampleRecordingHookPath)
+	defer e.kubectl("delete", "-f", exampleRecordingHookPath)
 
 	e.logf("Creating test deployment")
 	const testDeployment = `