├── .github └── workflows │ └── build.yml ├── .gitignore ├── .mergify.yml ├── .metwork-framework └── README.md ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── docker ├── Dockerfile └── entrypoint.sh ├── installer ├── install.sh └── uninstall.sh ├── sonar-project.properties └── src ├── Makefile ├── control.c ├── control.h ├── log_proxy.c ├── log_proxy_wrapper.c ├── options.h ├── out.c ├── out.h ├── test_log_proxy.c ├── util.c ├── util.h └── valgrind.sh /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: {} 5 | pull_request: {} 6 | release: 7 | types: 8 | - published 9 | 10 | jobs: 11 | prepare: 12 | runs-on: ubuntu-22.04 13 | timeout-minutes: 10 14 | steps: 15 | # we block concurrent executions because of concurrency issues 16 | # on docker build image 17 | - name: 'Block Concurrent Executions' 18 | uses: softprops/turnstyle@v2.3.0 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | - uses: actions/checkout@v4 22 | name: checkout repository 23 | # only for push or release 24 | - name: make docker buildimage 25 | uses: elgohr/Publish-Docker-Github-Action@v5 26 | if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'push' 27 | with: 28 | username: "${{ secrets.DOCKER_USERNAME }}" 29 | password: "${{ secrets.DOCKER_PASSWORD }}" 30 | name: metwork/logproxy-centos6-buildimage 31 | workdir: docker 32 | cache: true 33 | tags: "temporary" 34 | build: 35 | runs-on: ubuntu-22.04 36 | needs: prepare 37 | steps: 38 | - name: checkout repository 39 | uses: actions/checkout@v4 40 | - name: debug env 41 | run: env |grep GITHUB 42 | - name: compute tag name 43 | id: vars 44 | run: | 45 | TMPREF=${GITHUB_REF#refs/*/} 46 | if [[ "$TMPREF" == */merge ]]; then echo "tag="`echo pr${TMPREF} |awk -F '/' '{print $1;}'`>> ${GITHUB_OUTPUT}; else echo "tag="${TMPREF} >> ${GITHUB_OUTPUT}; fi 47 | #if [[ "$TMPREF" == */merge ]]; then echo ::set-output name=tag::`echo pr${TMPREF} |awk -F '/' '{print $1;}'`; else echo ::set-output name=tag::${TMPREF}; fi 48 | - name: "Install system deps" 49 | run: | 50 | sudo apt update 51 | sudo apt -y install valgrind 52 | - name: "Basic build and test" 53 | run: | 54 | make DEBUG=yes 55 | make leak 56 | make clean 57 | make 58 | # only for releases or push 59 | - name: release 60 | uses: docker://metwork/logproxy-centos6-buildimage:temporary 61 | if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'push' 62 | # only for releases or push 63 | - name: make tar.gz 64 | if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'push' 65 | run: | 66 | mkdir log_proxy-linux64-${{ steps.vars.outputs.tag }} 67 | cp release/usr/local/bin/* log_proxy-linux64-${{ steps.vars.outputs.tag }}/ 68 | tar -cvf log_proxy-linux64-${{ steps.vars.outputs.tag }}.tar log_proxy-linux64-${{ steps.vars.outputs.tag }} 69 | gzip log_proxy-linux64-${{ steps.vars.outputs.tag }}.tar 70 | # only for releases or push 71 | - name: upload artifact 72 | if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'push' 73 | uses: actions/upload-artifact@v4 74 | with: 75 | name: log_proxy-linux64-${{ steps.vars.outputs.tag }} 76 | path: ./log_proxy-linux64-${{ steps.vars.outputs.tag }} 77 | # only for releases 78 | - name: upload release asset 79 | id: upload-release-asset 80 | if: startsWith(github.ref, 'refs/tags/v') && github.event_name == 'release' 81 | uses: actions/upload-release-asset@v1 82 | env: 83 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 84 | with: 85 | upload_url: ${{ github.event.release.upload_url }} 86 | asset_path: ./log_proxy-linux64-${{ steps.vars.outputs.tag }}.tar.gz 87 | asset_name: log_proxy-linux64-${{ steps.vars.outputs.tag }}.tar.gz 88 | asset_content_type: application/gzip 89 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Object files 5 | *.o 6 | *.ko 7 | *.obj 8 | *.elf 9 | 10 | # Linker output 11 | *.ilk 12 | *.map 13 | *.exp 14 | 15 | # Precompiled Headers 16 | *.gch 17 | *.pch 18 | 19 | # Libraries 20 | *.lib 21 | *.a 22 | *.la 23 | *.lo 24 | 25 | # Tests results 26 | app.info 27 | *.gcda 28 | *.gcno 29 | *.control 30 | coverage 31 | 32 | # Shared objects (inc. Windows DLLs) 33 | *.dll 34 | *.so 35 | *.so.* 36 | *.dylib 37 | 38 | # Executables 39 | *.exe 40 | *.out 41 | *.app 42 | *.i*86 43 | *.x86_64 44 | *.hex 45 | 46 | # Debug files 47 | *.dSYM/ 48 | *.su 49 | *.idb 50 | *.pdb 51 | 52 | # Kernel Module Compile Results 53 | *.mod* 54 | *.cmd 55 | .tmp_versions/ 56 | modules.order 57 | Module.symvers 58 | Mkfile.old 59 | dkms.conf 60 | log_proxy 61 | log_proxy_wrapper 62 | test_log_proxy 63 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | # automatically generated from https://github.com/metwork-framework/github_organization_management/blob/master/common_files/mergify.yml) 2 | 3 | pull_request_rules: 4 | 5 | - name: approved merges 6 | actions: 7 | merge: 8 | method: squash 9 | commit_message_template: |- 10 | {{ title }} (#{{ number }}) 11 | 12 | {{ body }} 13 | delete_head_branch: {} 14 | conditions: 15 | 16 | - -merged 17 | - -closed 18 | - "#approved-reviews-by>=1" 19 | - -title~=(WIP|wip) 20 | - "label!=Status: Blocked" 21 | - "title~=^(build|chore|ci|docs|style|refactor|perf|test|fix|fea\ 22 | t|fix|feat|feat!|fix!)(\\([a-z]+\\))?: .*$" 23 | 24 | - name: change title 25 | actions: 26 | comment: 27 | message: | 28 | For mergify to merge automatically this PR, 29 | 30 | => you have to change the PR title to match conventional commit specification: 31 | 32 | ``` 33 | regex: ^(build|chore|ci|docs|style|refactor|perf|test|fix|feat|fix|feat|feat!|fix!)(\([a-z]+\))?: .*$ 34 | ``` 35 | 36 | As the title will be used for the commit message (and so for changelog entry if `feat:` or `fix:`). 37 | conditions: 38 | 39 | - -merged 40 | - -closed 41 | - "#approved-reviews-by>=1" 42 | - -title~=(WIP|wip) 43 | - "label!=Status: Blocked" 44 | - "-title~=^(build|chore|ci|docs|style|refactor|perf|test|fix|fe\ 45 | at|fix|feat|feat!|fix!)(\\([a-z]+\\))?: .*$" 46 | 47 | - name: buildbot auto merges 48 | actions: 49 | review: 50 | type: APPROVE 51 | message: automatic approve because author=metworkbot 52 | conditions: 53 | 54 | - -merged 55 | - -closed 56 | - author=metworkbot 57 | - -title~=(WIP|wip) 58 | - "label!=Status: Blocked" 59 | - name: mergenow auto merges 60 | actions: 61 | review: 62 | type: APPROVE 63 | message: "automatic approve because label=Status: Merge Now" 64 | conditions: 65 | 66 | - -merged 67 | - -closed 68 | - -title~=(WIP|wip) 69 | - "label=Status: Merge Now" 70 | - name: wip 71 | actions: 72 | label: 73 | add: 74 | - "Status: In Progress" 75 | remove: 76 | - "Status: Revision Needed" 77 | - "Status: Pending" 78 | - "Status: Review Needed" 79 | - "Status: Accepted" 80 | - "Status: Blocked" 81 | - "Status: NotReproduced" 82 | - "Status: Merged" 83 | - "Status: Merge Now" 84 | conditions: 85 | 86 | - -merged 87 | - -closed 88 | - title~=(WIP|wip) 89 | - "label!=Status: Blocked" 90 | - name: review needed 91 | actions: 92 | label: 93 | add: 94 | - "Status: Review Needed" 95 | remove: 96 | - "Status: Revision Needed" 97 | - "Status: Pending" 98 | - "Status: In Progress" 99 | - "Status: Accepted" 100 | - "Status: Blocked" 101 | - "Status: NotReproduced" 102 | - "Status: Merged" 103 | - "Status: Merge Now" 104 | conditions: 105 | 106 | - -merged 107 | - -closed 108 | - -title~=(WIP|wip) 109 | - "label!=Status: Blocked" 110 | - "label!=Status: Merge Now" 111 | - "#approved-reviews-by=0" 112 | - author!=metworkbot 113 | - name: revision needed1 114 | actions: 115 | label: 116 | add: 117 | - "Status: Revision Needed" 118 | remove: 119 | - "Status: In Progress" 120 | - "Status: Pending" 121 | - "Status: Review Needed" 122 | - "Status: Accepted" 123 | - "Status: Blocked" 124 | - "Status: NotReproduced" 125 | - "Status: Merged" 126 | - "Status: Merge Now" 127 | conditions: 128 | 129 | - -merged 130 | - -closed 131 | - "#changes-requested-reviews-by>=1" 132 | - "label!=Status: Blocked" 133 | - name: revision needed2 134 | actions: 135 | label: 136 | add: 137 | - "Status: Revision Needed" 138 | remove: 139 | - "Status: In Progress" 140 | - "Status: Pending" 141 | - "Status: Review Needed" 142 | - "Status: Accepted" 143 | - "Status: Blocked" 144 | - "Status: NotReproduced" 145 | - "Status: Merged" 146 | - "Status: Merge Now" 147 | conditions: 148 | 149 | - -merged 150 | - -closed 151 | - conflict 152 | - "label!=Status: Blocked" 153 | 154 | - name: closed 155 | actions: 156 | label: 157 | add: 158 | - "Status: Closed" 159 | remove: 160 | - "Status: Revision Needed" 161 | - "Status: Pending" 162 | - "Status: Merged" 163 | - "Status: Review Needed" 164 | - "Status: Accepted" 165 | - "Status: Blocked" 166 | - "Status: In Progress" 167 | - "Status: NotReproduced" 168 | - "Status: Merge Now" 169 | conditions: 170 | - closed 171 | - -merged 172 | - name: merged 173 | actions: 174 | label: 175 | add: 176 | - "Status: Merged" 177 | remove: 178 | - "Status: Revision Needed" 179 | - "Status: Pending" 180 | - "Status: Closed" 181 | - "Status: Review Needed" 182 | - "Status: Accepted" 183 | - "Status: Blocked" 184 | - "Status: In Progress" 185 | - "Status: NotReproduced" 186 | - "Status: Merge Now" 187 | conditions: 188 | - merged 189 | queue_rules: [] 190 | -------------------------------------------------------------------------------- /.metwork-framework/README.md: -------------------------------------------------------------------------------- 1 | ## What is it? 2 | 3 | **log_proxy** is a tiny C utility for log rotation for apps that write their logs to stdout. 4 | 5 | This is very useful, specially for [12-factor apps that write their logs to stdout](https://12factor.net/logs). 6 | 7 | It can be used to avoid loosing some logs if you use `logrotate` with `copytruncate` feature or to prevent a log file from filling your hard disk. 8 | 9 | ## Features 10 | 11 | - [x] usable as a pipe (`myapp myapp_arg1 myapp_arg2 |log_proxy /log/myapp.log`) 12 | - [x] configurable log rotation suffix with `strftime` placeholders (for example: `.%Y%m%d%H%M%S`) 13 | - [x] can limit the number of rotated files (and delete oldest) 14 | - [x] can rotate files depending on their size (in bytes) 15 | - [x] can rotate files depending on their age (in seconds) 16 | - [x] does not need a specific log directory for a given app (you can have one directory with plenty of different log files from different apps) 17 | - [x] several instances of the same app can log to the same file without issue (example: `myapp arg1 |log_proxy --use-locks /log/myapp.log` and `myapp arg2 |log_proxy --use-locks /log/myapp.log` can run at the same time) 18 | - [ ] configurable action (a command to execute) to run after each log rotation 19 | - [ ] rock solid (it's perfectly stable in our use case but we are waiting for other success stories to check this feature) 20 | - [x] option to add a timestamp before each log line, thanks to [mk-fg](https://github.com/mk-fg) 21 | - [x] really fast 22 | - [x] do not eat a lot of memory 23 | - [x] configurable with CLI options as well with env variables 24 | - [x] usable as a wrapper to capture stdout and stderr (`log_proxy_wrapper --stdout=/log/myapp.stdout --stderr=/log/myapp.stderr -- myapp myapp_arg1 myapp_arg2`) 25 | - [x] usable as a wrapper to capture stdout and stderr in the same file (`log_proxy_wrapper --stdout=/log/myapp.log --stderr=STDOUT -- myapp myapp_arg1 myapp_arg2`) 26 | - [x] very few dependencies (only `glib2` is required) 27 | - [x] very easy to build (event on old distributions like `CentOS 6`) 28 | 29 | ## How to install? 30 | 31 | We provide Linux 64 bits binaries in [releases section](https://github.com/metwork-framework/log_proxy/releases). There is virtually no requirement (you just need a Linux 64 bits distribution more recent than CentOS 6 (2011!)). 32 | 33 | Of course, you can also build the tool by yourself (see at the end of this document). 34 | 35 | To install the binary distribution: 36 | 37 | ``` 38 | # As root user (or with sudo) 39 | bash -c "$(curl -fsSLk https://raw.githubusercontent.com/metwork-framework/log_proxy/master/installer/install.sh)" 40 | ``` 41 | 42 | Notes: 43 | 44 | - if you are very concerned about the security of your system and if you don't want to execute 45 | a remote `root` script on your system, please review the [very small install script](https://raw.githubusercontent.com/metwork-framework/log_proxy/master/installer/install.sh) 46 | (it's just about downloading and installing two statically compiled binaries in `/usr/local/bin/`) 47 | - our binary distribution won't work on [Alpine Linux](https://alpinelinux.org/) because of `glibc` replacement but [@tomalok](https://github.com/tomalok) is maintaining a [log_proxy Alpine Linux package](https://pkgs.alpinelinux.org/packages?name=log_proxy). 48 | 49 | ## How to uninstall? 50 | 51 | ``` 52 | # As root user (or with sudo) 53 | rm -f /usr/local/bin/log_proxy 54 | rm -f /usr/local/bin/log_proxy_wrapper 55 | ``` 56 | 57 | ## Why this tool? 58 | 59 | ### Why not using `logrotate` with `copytruncate` feature? 60 | 61 | If you use `myapp myapp_arg1 myapp_arg2 >/log/myapp.log 2>&1`for example and if you can't stop easily your app (because it's a critical thing), you can configure `logrotate` with `copytruncate` feature to do the log rotation of `/log/myapp.log` but: 62 | 63 | - you may loose a few lines during log rotation (1) 64 | - the rotation is mainly time-based, so you can fill your storage if your app suddently start to be very very verbose 65 | 66 | (1), see https://unix.stackexchange.com/questions/475524/how-copytruncate-actually-works 67 | 68 | > Please note also that copyrotate has an inherent race condition, in that it's possible that the writer will append a line to the logfile just after logrotate finished the copy and before it has issued the truncate operation. That race condition would cause it to lose those lines of log forever. That's why rotating logs using copytruncate is usually not recommended, unless it's the only possible way to do it. 69 | 70 | ### Why developing another tool? 71 | 72 | After reading: https://superuser.com/questions/291368/log-rotation-of-stdout and http://zpz.github.io/blog/log-rotation-of-stdout/, we reviewed plenty of existing tools (`multilog`, `rotatelogs`, `piper`...). 73 | 74 | But none of them was ok with our needed features: 75 | 76 | - configurable log rotation on size **AND** age 77 | - no dedicated log directory for an app 78 | - (and) several instances of the same app can log to the same log file without issue 79 | 80 | The [piper tool](https://github.com/gongled/piper) was the more close but does not support the last feature (several instances to the same log file). 81 | 82 | ## Usage 83 | 84 | ### As a filter 85 | 86 | ```console 87 | your_app your_app_arg1 your_app_arg2 2>&1 |log_proxy --rotation-size=1000000 my_log_file_max_1MB.log 88 | ``` 89 | 90 | Full help: 91 | 92 | ```console 93 | $ ./log_proxy --help 94 | Usage: 95 | log_proxy [OPTION?] LOGNAME - log proxy 96 | 97 | Help Options: 98 | -h, --help Show help options 99 | 100 | Application Options: 101 | -s, --rotation-size maximum size (in bytes) for a log file before rotation (0 => no maximum, default: content of environment variable LOGPROXY_ROTATION_SIZE or 104857600 (100MB)) 102 | -t, --rotation-time maximum lifetime (in seconds) for a log file before rotation (0 => no maximum, default: content of environment variable LOGPROXY_ROTATION_TIME or 86400 (24H)) 103 | -S, --rotation-suffix strftime based suffix to append to rotated log files (default: content of environment variable LOGPROXY_ROTATION_SUFFIX or .%%Y%%m%%d%%H%%M%%S) 104 | -d, --log-directory directory to store log files (default: content of environment variable LOGPROXY_LOG_DIRECTORY or current directory), directory is created if missing 105 | -n, --rotated-files maximum number of rotated files to keep including main one (0 => no cleaning, default: content of environment variable LOGPROXY_ROTATED_FILES or 5) 106 | -T, --timestamps strftime prefix to prepend to every output line (default: content of environment variable LOGPROXY_TIMESTAMPS or none) 107 | -c, --chmod if set, chmod the logfile to this value, '0700' for example (default: content of environment variable LOGPROXY_CHMOD or NULL) 108 | -o, --chown if set, try (if you don't have sufficient privileges, it will fail silently) to change the owner of the logfile to the given user value 109 | -g, --chgrp if set, try (if you don't have sufficient privileges, it will fail silently) to change the group of the logfile to the given group value 110 | -m, --use-locks use locks to append to main log file (useful if several process writes to the same file) 111 | -f, --fifo if set, read lines on this fifo instead of stdin 112 | -r, --rm-fifo-at-exit if set, drop fifo at then end of the program (you have to use --fifo option of course) 113 | 114 | Optional environment variables to override defaults: 115 | LOGPROXY_ROTATION_SIZE 116 | LOGPROXY_ROTATION_TIME 117 | LOGPROXY_ROTATION_SUFFIX 118 | LOGPROXY_LOG_DIRECTORY 119 | LOGPROXY_ROTATED_FILES 120 | LOGPROXY_TIMESTAMPS 121 | 122 | Example for rotation-size option : 123 | - If log_proxy is run with the option --rotation-size on command line, rotation-size will take the provided value 124 | - If the option --rotation-size is not provided on command line : 125 | - If the environment variable LOGPROXY_ROTATION_SIZE is set, rotation-size will take this value 126 | - If the environment variable LOGPROXY_ROTATION_SIZE is not set, rotation-size will take the default value 104857600 127 | ``` 128 | 129 | ## As a wrapper 130 | 131 | ```console 132 | log_proxy_wrapper --rotation-size=1000000 --stdout=my_log_file_max_1MB.log --stderr=STDOUT -- your_app your_app_arg1 your_app_arg2 133 | ``` 134 | 135 | Full help: 136 | 137 | ```console 138 | $ ./log_proxy_wrapper --help 139 | Usage: 140 | log_proxy_wrapper [OPTION?] -- COMMAND [COMMAND_ARG1] [COMMAND_ARG2] [...] - log proxy 141 | 142 | Help Options: 143 | -h, --help Show help options 144 | 145 | Application Options: 146 | -s, --rotation-size maximum size (in bytes) for a log file before rotation (0 => no maximum, default: content of environment variable LOGPROXY_ROTATION_SIZE or 104857600 (100MB)) 147 | -t, --rotation-time maximum lifetime (in seconds) for a log file before rotation (0 => no maximum, default: content of environment variable LOGPROXY_ROTATION_TIME or 86400 (24H)) 148 | -S, --rotation-suffix strftime based suffix to append to rotated log files (default: content of environment variable LOGPROXY_ROTATION_SUFFIX or .%%Y%%m%%d%%H%%M%%S) 149 | -d, --log-directory directory to store log files (default: content of environment variable LOGPROXY_LOG_DIRECTORY or current directory), directory is created if missing 150 | -n, --rotated-files maximum number of rotated files to keep including main one (0 => no cleaning, default: content of environment variable LOGPROXY_ROTATED_FILES or 5) 151 | -T, --timestamps strftime prefix to prepend to every output line (default: content of environment variable LOGPROXY_TIMESTAMPS or none) 152 | -c, --chmod if set, chmod the logfile to this value, '0700' for example (default: content of environment variable LOGPROXY_CHMOD or NULL) 153 | -o, --chown if set, try (if you don't have sufficient privileges, it will fail silently) to change the owner of the logfile to the given user value 154 | -g, --chgrp if set, try (if you don't have sufficient privileges, it will fail silently) to change the group of the logfile to the given group value 155 | -m, --use-locks use locks to append to main log file (useful if several process writes to the same file) 156 | -O, --stdout stdout file path (NULL string (default) can be used to redirect to /dev/null) 157 | -E, --stderr stderr file path (STDOUT string (default) can be used to redirect to the same file than stdout) 158 | -F, --fifo-tmp-dir directory where to store tmp FIFO for log_proxy (default: content of environment variable TMPDIR if set, /tmp if not) 159 | ``` 160 | 161 | ## How to build? 162 | 163 | ### Requirements 164 | 165 | A Linux/Unix distribution with standard development tools (`git`, `gcc`, `make`, `pkg-config`) and `glib2` library with `devel` support (provided for example in CentOS 6 in the `glib2-devel` standard package). 166 | 167 | ### Build and install 168 | 169 | ```console 170 | git clone https://github.com/metwork-framework/log_proxy # or download/unpack a zip with the github interface 171 | cd log_proxy 172 | make 173 | ``` 174 | 175 | Then as `root` user or prefixed with `sudo`: 176 | 177 | ```console 178 | make install 179 | ``` 180 | 181 | This will install `log_proxy` and `log_proxy_wrapper` in `/usr/local/bin`, by default. 182 |  183 | `make install` also supports `PREFIX=...` for installing (for example) into a `/usr` directory other than the one in `/usr/local`, and `DESTDIR=...` for installing into `$DESTDIR/$PREFIX/bin` which is useful when making packages. 184 |  185 |  186 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## v0.7.4 (2024-11-05) 4 | 5 | ### Bug Fixes 6 | 7 | - break out in case of write problem (fix #44) 8 | 9 | ## v0.7.3 (2024-02-12) 10 | 11 | ### Bug Fixes 12 | 13 | - add more examples and correct some confusing wording in cli options' descriptions (#43) 14 | 15 | ## v0.7.2 (2024-02-09) 16 | 17 | ### Bug Fixes 18 | 19 | - pass missing options -c, -o and -g from wrapper to logger process 20 | 21 | ## v0.7.1 (2024-02-09) 22 | 23 | ### Bug Fixes 24 | 25 | - print more descriptive error for strftime() failure on timestamp-prefix (#41) 26 | 27 | ## v0.7.0 (2024-02-09) 28 | 29 | ### Bug Fixes 30 | 31 | - build log_proxy command in wrapper as an argv array, don't use gshell parser 32 | - cleanup glib includes, add some missing libc ones 33 | 34 | ## v0.6.0 (2024-02-09) 35 | 36 | ### New Features 37 | 38 | - add -T/--timestamps (env LOGPROXY_TIMESTAMPS) option to prepend strftime to each line 39 | 40 | ### Bug Fixes 41 | 42 | - pass -d/--log-directory command-line option from wrapper to logger process 43 | - fix signal_handler for SIGINT 44 | 45 | ## v0.5.2 (2022-02-21) 46 | 47 | ### New Features 48 | 49 | - add optional environment variable LOGPROXY_CHMOD to change logf… (#32) 50 | - add optional environment variable LOGPROXY_CHMOD to change logf… (#33) 51 | 52 | ## v0.5.1 (2021-10-14) 53 | 54 | ### Bug Fixes 55 | 56 | - centos8 memory fix (#31) 57 | 58 | ## v0.5.0 (2021-08-16) 59 | 60 | ### Bug Fixes 61 | 62 | - potential fix for malloc deadlock in some corner cases (#30) 63 | 64 | ## v0.4.4 (2021-01-21) 65 | 66 | ### Bug Fixes 67 | 68 | - return a !=0 status code if we can't launch the program 69 | 70 | ## v0.4.3 (2021-01-07) 71 | 72 | - No interesting change 73 | 74 | ## v0.4.2 (2020-12-28) 75 | 76 | ### Bug Fixes 77 | 78 | - fix installer cleaning 79 | 80 | ## v0.4.1 (2020-12-28) 81 | 82 | ### Bug Fixes 83 | 84 | - fix missing options in log_proxy_wrapper (#26) 85 | 86 | ## v0.4.0 (2020-12-06) 87 | 88 | ### New Features 89 | 90 | - add a chmod/chown/chgrp option (#22) 91 | 92 | ## v0.3.1 (2020-07-27) 93 | 94 | ### Bug Fixes 95 | 96 | - fix --fifo-tmp-dir documentation 97 | 98 | ## v0.3.0 (2020-07-22) 99 | 100 | ### New Features 101 | 102 | - add fifo-tmp-dir option to log_proxy_wrapper 103 | 104 | ## v0.2.3 (2020-07-10) 105 | 106 | - No interesting change 107 | 108 | ## v0.2.2 (2020-07-10) 109 | 110 | - No interesting change 111 | 112 | ## v0.2.1 (2020-07-10) 113 | 114 | - No interesting change 115 | 116 | ## v0.2.0 (2020-07-10) 117 | 118 | ### New Features 119 | 120 | - add option for static compiling 121 | - releases are now static builds 122 | - add install script 123 | 124 | ## v0.1.1 (2020-05-04) 125 | 126 | - No interesting change 127 | 128 | ## v0.1.0 (2020-04-29) 129 | 130 | ### Bug Fixes 131 | 132 | - fix a potential deadlock in some corner cases 133 | 134 | ## v0.0.9 (2020-04-16) 135 | 136 | - No interesting change 137 | 138 | ## v0.0.8 (2020-03-23) 139 | 140 | ### New Features 141 | 142 | - use github action 143 | 144 | ## v0.0.7 (2020-03-23) 145 | 146 | - No interesting change 147 | 148 | ## v0.0.6 (2020-03-18) 149 | 150 | ### New Features 151 | 152 | - force rotation-size to 90% of rlimit-fsize (if set) 153 | 154 | ## v0.0.5 (2020-02-24) 155 | 156 | ### New Features 157 | 158 | - prefix control files names by "." (dot) 159 | 160 | ## v0.0.4 (2020-01-28) 161 | 162 | - No interesting change 163 | 164 | ## v0.0.3 (2020-01-27) 165 | 166 | ### New Features 167 | 168 | - add possibility to specify a log directory 169 | 170 | ## v0.0.2 (2020-01-03) 171 | 172 | ### Bug Fixes 173 | 174 | - misuse of the g_build_path function 175 | 176 | ## v0.0.1 (2019-12-26) 177 | 178 | ### New Features 179 | 180 | - log_proxy first version 181 | - add a mode to read log lines from fifo instead of stdin 182 | - first try with log_proxy_wrapper 183 | - add optional environment variables to override default values 184 | - add other tests on control.c 185 | 186 | ### Bug Fixes 187 | 188 | - post test fixes 189 | - don't leak fifo if SIGTERM or SIGINT 190 | - fix initialization value and GOptionArg type for long parameters 191 | 192 | 193 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | [//]: # (automatically generated from https://github.com/metwork-framework/github_organization_management/blob/master/common_files/CODE_OF_CONDUCT.md) 4 | 5 | ## Our Pledge 6 | 7 | In the interest of fostering an open and welcoming environment, we as 8 | contributors and maintainers pledge to making participation in our project and 9 | our community a harassment-free experience for everyone, regardless of age, body 10 | size, disability, ethnicity, sex characteristics, gender identity and expression, 11 | level of experience, education, socio-economic status, nationality, personal 12 | appearance, race, religion, or sexual identity and orientation. 13 | 14 | ## Our Standards 15 | 16 | Examples of behavior that contributes to creating a positive environment 17 | include: 18 | 19 | * Using welcoming and inclusive language 20 | * Being respectful of differing viewpoints and experiences 21 | * Gracefully accepting constructive criticism 22 | * Focusing on what is best for the community 23 | * Showing empathy towards other community members 24 | 25 | Examples of unacceptable behavior by participants include: 26 | 27 | * The use of sexualized language or imagery and unwelcome sexual attention or 28 | advances 29 | * Trolling, insulting/derogatory comments, and personal or political attacks 30 | * Public or private harassment 31 | * Publishing others' private information, such as a physical or electronic 32 | address, without explicit permission 33 | * Other conduct which could reasonably be considered inappropriate in a 34 | professional setting 35 | 36 | ## Our Responsibilities 37 | 38 | Project maintainers are responsible for clarifying the standards of acceptable 39 | behavior and are expected to take appropriate and fair corrective action in 40 | response to any instances of unacceptable behavior. 41 | 42 | Project maintainers have the right and responsibility to remove, edit, or 43 | reject comments, commits, code, wiki edits, issues, and other contributions 44 | that are not aligned to this Code of Conduct, or to ban temporarily or 45 | permanently any contributor for other behaviors that they deem inappropriate, 46 | threatening, offensive, or harmful. 47 | 48 | ## Scope 49 | 50 | This Code of Conduct applies both within project spaces and in public spaces 51 | when an individual is representing the project or its community. Examples of 52 | representing a project or community include using an official project e-mail 53 | address, posting via an official social media account, or acting as an appointed 54 | representative at an online or offline event. Representation of a project may be 55 | further defined and clarified by project maintainers. 56 | 57 | ## Enforcement 58 | 59 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 60 | reported by contacting the project team at `team@metwork-framework.org`. All 61 | complaints will be reviewed and investigated and will result in a response that 62 | is deemed necessary and appropriate to the circumstances. The project team is 63 | obligated to maintain confidentiality with regard to the reporter of an incident. 64 | Further details of specific enforcement policies may be posted separately. 65 | 66 | Project maintainers who do not follow or enforce the Code of Conduct in good 67 | faith may face temporary or permanent repercussions as determined by other 68 | members of the project's leadership. 69 | 70 | ## Attribution 71 | 72 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 73 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 74 | 75 | [homepage]: https://www.contributor-covenant.org 76 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing guide 2 | 3 | [//]: # (automatically generated from https://github.com/metwork-framework/github_organization_management/blob/master/common_files/CONTRIBUTING.md) 4 | 5 | **WARNING: work in progress** 6 | 7 | 8 | 9 | 10 | 11 | ## Version numbering 12 | 13 | We follow the [semantic versionning specification](https://semver.org/). 14 | 15 | ### Summary (see above specification for more details) 16 | 17 | Given a version number `MAJOR.MINOR.PATCH`, we increment the: 18 | 19 | - `MAJOR` version when we make incompatible API changes, 20 | - `MINOR` version when we add functionality in a backwards-compatible manner, and 21 | - `PATCH` version when we make backwards-compatible bug fixes. 22 | 23 | ## Commit Message Guidelines 24 | 25 | Inspired by Angular project and [conventional commits initiative](https://www.conventionalcommits.org), 26 | we have very precise rules over how our git commit messages can be formatted. This leads to more readable messages that are 27 | easy to follow when looking through the project history. But also, we use the git commit messages to generate the project 28 | changelog. 29 | 30 | So we follow the [conventional commits initiative](https://www.conventionalcommits.org) specification. 31 | 32 | ### Summary (see above specification for more details) 33 | 34 | Each commit message consists of a `header`, a `body` and a `footer`. The `header` has a special format that includes a `type`, 35 | a `scope` and a `description`. The commit message should be structured as follows: 36 | 37 | ``` 38 | [optional scope]: 39 | 40 | [optional body] 41 | 42 | [optional footer] 43 | ``` 44 | 45 | The commit message contains the following structural elements, to communicate intent to the consumers of the project: 46 | 47 | - `fix`: a commit of the type fix patches a bug in your codebase (this correlates with `PATCH` in semantic versioning). 48 | - `feat`: a commit of the type feat introduces a new feature to the codebase (this correlates with `MINOR` in semantic versioning). 49 | - `BREAKING CHANGE`: a commit that has the text `BREAKING CHANGE:` at the beginning of its optional body or footer section 50 | introduces a breaking API change (correlating with `MAJOR` in semantic versioning). 51 | A breaking change can be part of commits of any type. e.g., a `fix:`, `feat:` & `chore:` types would all be valid, 52 | in addition to any other type. 53 | Others: commit types other than fix: and feat: are allowed, for example commitlint-config-conventional (based on the the Angular convention) recommends chore:, docs:, style:, refactor:, perf:, test:, and others. We also recommend improvement for commits that improve a current implementation without adding a new feature or fixing a bug. Notice these types are not mandated by the conventional commits specification, and have no implicit effect in semantic versioning (unless they include a BREAKING CHANGE, which is NOT recommended). 54 | A scope may be provided to a commit’s type, to provide additional contextual information and is contained within parenthesis, e.g., feat(parser): add ability to parse arrays. 55 | 56 | ### Examples 57 | 58 | #### Commit message with description and breaking change in body 59 | 60 | ``` 61 | feat: allow provided config object to extend other configs 62 | 63 | BREAKING CHANGE: `extends` key in config file is now used for extending other config files 64 | ``` 65 | 66 | #### Commit message with no body 67 | 68 | ``` 69 | docs: correct spelling of CHANGELOG 70 | ``` 71 | 72 | #### Commit message with scope 73 | 74 | ``` 75 | feat(lang): added polish language 76 | ``` 77 | 78 | #### Commit message for a fix using an (optional) issue number. 79 | 80 | ``` 81 | fix: minor typos in code 82 | 83 | see the issue for details on the typos fixed 84 | 85 | fixes issue #12 86 | ``` 87 | 88 | ### Revert 89 | 90 | If the commit reverts a previous commit, it should begin with `revert:`, followed by the header of the reverted commit. 91 | In the body it should say: `This reverts commit .`, where the `hash` is the SHA of the commit being reverted. 92 | 93 | ### Type 94 | 95 | Must be one of the following: 96 | 97 | - `build`: Changes that affect the build or CI system (`chore` is also accepted for compatibility) 98 | - `docs`: Documentation only changes 99 | - `feat`: A new feature 100 | - `fix`: A bug fix 101 | - `perf`: A code change that improves performance 102 | - `refactor`: A code change that neither fixes a bug nor adds a feature 103 | - `style`: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) 104 | - `test`: Adding missing tests or correcting existing tests 105 | 106 | ### Scope 107 | 108 | The scope is not used for the moment. Please don't use scopes in commit messages. 109 | 110 | ### Description 111 | 112 | The description contains a succinct description of the change: 113 | 114 | ``` 115 | use the imperative, present tense: "change" not "changed" nor "changes" 116 | don't capitalize the first letter 117 | no dot (.) at the end 118 | ``` 119 | 120 | ### Body 121 | 122 | Just as in the subject, use the imperative, present tense: "change" not "changed" nor "changes". The body should include 123 | the motivation for the change and contrast this with previous behavior. 124 | 125 | ### Footer 126 | 127 | The footer should contain any information about `Breaking Changes` and is also the place to reference GitHub issues 128 | that this commit Closes. 129 | 130 | `Breaking Changes` should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit 131 | message is then used for this. 132 | 133 | 134 | 135 | 136 | 137 | ## Pull-requests and issues labels 138 | 139 | We use a consistent labelling scheme inspired by [sensible-github-labels](https://github.com/Relequestual/sensible-github-labels). 140 | 141 | ### Type 142 | 143 | - `Type: Bug`: it's about a bug 144 | - `Type: Enhancement`: it's about a new feature 145 | - `Type: Question`: it's just a question 146 | - `Type: Maintenance`: it's about a better way to implement an existing feature (refactor, performances improvement...) 147 | 148 | ### Priority 149 | 150 | - `Priority: Critical`: This should be dealt with ASAP. Not fixing this issue would be a serious error. 151 | - `Priority: High`: After critical issues are fixed, these should be dealt with before any further issues. 152 | - `Priority: Medium`: (implicit, does not exist as a label) This issue may be useful, and needs some attention. 153 | - `Priority: Low` : This issue can probably be picked up by anyone looking to contribute to the project, as an entry fix. 154 | 155 | ### Status 156 | 157 | - `Status: Pending`: The issue is new, this is the triage status. 158 | - `Status: Closed`: The issue/pr is closed (because the corresponding change is merged or because the corresponding change was abandoned/rejected) 159 | - `Status: Accepted`: It's clear what the subject of the issue is about, what the resolution should be, and we want this :-) 160 | - `Status: Blocked`: There is another issue that needs to be resolved first, or a specific person is required to comment or reply to progress. There may also be some external blocker. 161 | - `Status: In Progress`: This issue is being worked on, and has someone assigned. 162 | - `Status: Review Needed`: The PR must be reviewed by a team member. 163 | - `Status: Revision Needed`: Submitter of PR needs to revise the PR related to the issue. 164 | 165 | ### Labels management by `MetworkBot` 166 | 167 | We have a bot to do some automatic labelling: 168 | 169 | - [x] When a pr is opended, it adds the `Status: Pending` label 170 | - [x] When an issue is opened, it adds the `Status: Pending` label (if no other `Status: *` label was given initialy) 171 | - [x] When a pr is closed, it removes every `Status: *` labels and adds `Status: Closed` 172 | - [x] When an issue is closed, it removes every `Status: *` labels and adds `Status: Closed` 173 | - [x] When a pr is reopened, it removes the `Status: Closed` label and adds `Status: Review Needed` 174 | - [x] When an issue is reopened, it removes the `Status: Closed` label and adds `Status: Pending` 175 | - [ ] When a new `Priority: *` label is set, old `Priority: *` labels are removed (if necessary) 176 | - [ ] When a new `Status: *` label is set, old `Status: *` labels are removed (if necessary) 177 | - [x] When a pr is not "good" (because of bad statuses for example), the label `Status: Revision Needed` is set 178 | - [x] When a pr is "good" (statuses all green), the label `Status: Review Needed` is set 179 | 180 | 181 | 182 | 183 | 184 | ## Code of Conduct 185 | 186 | The MetWork community must follow the Code of Conduct described in [this document](CODE_OF_CONDUCT.md). 187 | 188 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, MetWork Framework 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all clean install test leak coverage release 2 | 3 | all: 4 | cd src && $(MAKE) all 5 | 6 | clean: 7 | cd src && $(MAKE) clean 8 | rm -Rf release 9 | 10 | install: 11 | cd src && $(MAKE) install 12 | 13 | test: 14 | cd src && $(MAKE) test 15 | 16 | leak: 17 | cd src && $(MAKE) leak 18 | 19 | coverage: 20 | cd src && $(MAKE) coverage 21 | 22 | release: clean 23 | cd src && $(MAKE) STATIC=yes DESTDIR=$(shell pwd)/release install 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # log_proxy 2 | 3 | [//]: # (automatically generated from https://github.com/metwork-framework/github_organization_management/blob/master/common_files/README.md) 4 | 5 | **Status (master branch)** 6 | 7 | [![GitHub CI](https://github.com/metwork-framework/log_proxy/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/metwork-framework/log_proxy/actions?query=workflow%3ACI+branch%3Amaster) 8 | [![Maintenance](https://raw.githubusercontent.com/metwork-framework/resources/master/badges/maintained.svg)](https://github.com/metwork-framework/resources/blob/master/badges/maintained.svg) 9 | 10 | 11 | 12 | 13 | ## What is it? 14 | 15 | **log_proxy** is a tiny C utility for log rotation for apps that write their logs to stdout. 16 | 17 | This is very useful, specially for [12-factor apps that write their logs to stdout](https://12factor.net/logs). 18 | 19 | It can be used to avoid loosing some logs if you use `logrotate` with `copytruncate` feature or to prevent a log file from filling your hard disk. 20 | 21 | ## Features 22 | 23 | - [x] usable as a pipe (`myapp myapp_arg1 myapp_arg2 |log_proxy /log/myapp.log`) 24 | - [x] configurable log rotation suffix with `strftime` placeholders (for example: `.%Y%m%d%H%M%S`) 25 | - [x] can limit the number of rotated files (and delete oldest) 26 | - [x] can rotate files depending on their size (in bytes) 27 | - [x] can rotate files depending on their age (in seconds) 28 | - [x] does not need a specific log directory for a given app (you can have one directory with plenty of different log files from different apps) 29 | - [x] several instances of the same app can log to the same file without issue (example: `myapp arg1 |log_proxy --use-locks /log/myapp.log` and `myapp arg2 |log_proxy --use-locks /log/myapp.log` can run at the same time) 30 | - [ ] configurable action (a command to execute) to run after each log rotation 31 | - [ ] rock solid (it's perfectly stable in our use case but we are waiting for other success stories to check this feature) 32 | - [x] option to add a timestamp before each log line, thanks to [mk-fg](https://github.com/mk-fg) 33 | - [x] really fast 34 | - [x] do not eat a lot of memory 35 | - [x] configurable with CLI options as well with env variables 36 | - [x] usable as a wrapper to capture stdout and stderr (`log_proxy_wrapper --stdout=/log/myapp.stdout --stderr=/log/myapp.stderr -- myapp myapp_arg1 myapp_arg2`) 37 | - [x] usable as a wrapper to capture stdout and stderr in the same file (`log_proxy_wrapper --stdout=/log/myapp.log --stderr=STDOUT -- myapp myapp_arg1 myapp_arg2`) 38 | - [x] very few dependencies (only `glib2` is required) 39 | - [x] very easy to build (event on old distributions like `CentOS 6`) 40 | 41 | ## How to install? 42 | 43 | We provide Linux 64 bits binaries in [releases section](https://github.com/metwork-framework/log_proxy/releases). There is virtually no requirement (you just need a Linux 64 bits distribution more recent than CentOS 6 (2011!)). 44 | 45 | Of course, you can also build the tool by yourself (see at the end of this document). 46 | 47 | To install the binary distribution: 48 | 49 | ``` 50 | # As root user (or with sudo) 51 | bash -c "$(curl -fsSLk https://raw.githubusercontent.com/metwork-framework/log_proxy/master/installer/install.sh)" 52 | ``` 53 | 54 | Notes: 55 | 56 | - if you are very concerned about the security of your system and if you don't want to execute 57 | a remote `root` script on your system, please review the [very small install script](https://raw.githubusercontent.com/metwork-framework/log_proxy/master/installer/install.sh) 58 | (it's just about downloading and installing two statically compiled binaries in `/usr/local/bin/`) 59 | - our binary distribution won't work on [Alpine Linux](https://alpinelinux.org/) because of `glibc` replacement but [@tomalok](https://github.com/tomalok) is maintaining a [log_proxy Alpine Linux package](https://pkgs.alpinelinux.org/packages?name=log_proxy). 60 | 61 | ## How to uninstall? 62 | 63 | ``` 64 | # As root user (or with sudo) 65 | rm -f /usr/local/bin/log_proxy 66 | rm -f /usr/local/bin/log_proxy_wrapper 67 | ``` 68 | 69 | ## Why this tool? 70 | 71 | ### Why not using `logrotate` with `copytruncate` feature? 72 | 73 | If you use `myapp myapp_arg1 myapp_arg2 >/log/myapp.log 2>&1`for example and if you can't stop easily your app (because it's a critical thing), you can configure `logrotate` with `copytruncate` feature to do the log rotation of `/log/myapp.log` but: 74 | 75 | - you may loose a few lines during log rotation (1) 76 | - the rotation is mainly time-based, so you can fill your storage if your app suddently start to be very very verbose 77 | 78 | (1), see https://unix.stackexchange.com/questions/475524/how-copytruncate-actually-works 79 | 80 | > Please note also that copyrotate has an inherent race condition, in that it's possible that the writer will append a line to the logfile just after logrotate finished the copy and before it has issued the truncate operation. That race condition would cause it to lose those lines of log forever. That's why rotating logs using copytruncate is usually not recommended, unless it's the only possible way to do it. 81 | 82 | ### Why developing another tool? 83 | 84 | After reading: https://superuser.com/questions/291368/log-rotation-of-stdout and http://zpz.github.io/blog/log-rotation-of-stdout/, we reviewed plenty of existing tools (`multilog`, `rotatelogs`, `piper`...). 85 | 86 | But none of them was ok with our needed features: 87 | 88 | - configurable log rotation on size **AND** age 89 | - no dedicated log directory for an app 90 | - (and) several instances of the same app can log to the same log file without issue 91 | 92 | The [piper tool](https://github.com/gongled/piper) was the more close but does not support the last feature (several instances to the same log file). 93 | 94 | ## Usage 95 | 96 | ### As a filter 97 | 98 | ```console 99 | your_app your_app_arg1 your_app_arg2 2>&1 |log_proxy --rotation-size=1000000 my_log_file_max_1MB.log 100 | ``` 101 | 102 | Full help: 103 | 104 | ```console 105 | $ ./log_proxy --help 106 | Usage: 107 | log_proxy [OPTION?] LOGNAME - log proxy 108 | 109 | Help Options: 110 | -h, --help Show help options 111 | 112 | Application Options: 113 | -s, --rotation-size maximum size (in bytes) for a log file before rotation (0 => no maximum, default: content of environment variable LOGPROXY_ROTATION_SIZE or 104857600 (100MB)) 114 | -t, --rotation-time maximum lifetime (in seconds) for a log file before rotation (0 => no maximum, default: content of environment variable LOGPROXY_ROTATION_TIME or 86400 (24H)) 115 | -S, --rotation-suffix strftime based suffix to append to rotated log files (default: content of environment variable LOGPROXY_ROTATION_SUFFIX or .%%Y%%m%%d%%H%%M%%S) 116 | -d, --log-directory directory to store log files (default: content of environment variable LOGPROXY_LOG_DIRECTORY or current directory), directory is created if missing 117 | -n, --rotated-files maximum number of rotated files to keep including main one (0 => no cleaning, default: content of environment variable LOGPROXY_ROTATED_FILES or 5) 118 | -T, --timestamps strftime prefix to prepend to every output line (default: content of environment variable LOGPROXY_TIMESTAMPS or none) 119 | -c, --chmod if set, chmod the logfile to this value, '0700' for example (default: content of environment variable LOGPROXY_CHMOD or NULL) 120 | -o, --chown if set, try (if you don't have sufficient privileges, it will fail silently) to change the owner of the logfile to the given user value 121 | -g, --chgrp if set, try (if you don't have sufficient privileges, it will fail silently) to change the group of the logfile to the given group value 122 | -m, --use-locks use locks to append to main log file (useful if several process writes to the same file) 123 | -f, --fifo if set, read lines on this fifo instead of stdin 124 | -r, --rm-fifo-at-exit if set, drop fifo at then end of the program (you have to use --fifo option of course) 125 | 126 | Optional environment variables to override defaults: 127 | LOGPROXY_ROTATION_SIZE 128 | LOGPROXY_ROTATION_TIME 129 | LOGPROXY_ROTATION_SUFFIX 130 | LOGPROXY_LOG_DIRECTORY 131 | LOGPROXY_ROTATED_FILES 132 | LOGPROXY_TIMESTAMPS 133 | 134 | Example for rotation-size option : 135 | - If log_proxy is run with the option --rotation-size on command line, rotation-size will take the provided value 136 | - If the option --rotation-size is not provided on command line : 137 | - If the environment variable LOGPROXY_ROTATION_SIZE is set, rotation-size will take this value 138 | - If the environment variable LOGPROXY_ROTATION_SIZE is not set, rotation-size will take the default value 104857600 139 | ``` 140 | 141 | ## As a wrapper 142 | 143 | ```console 144 | log_proxy_wrapper --rotation-size=1000000 --stdout=my_log_file_max_1MB.log --stderr=STDOUT -- your_app your_app_arg1 your_app_arg2 145 | ``` 146 | 147 | Full help: 148 | 149 | ```console 150 | $ ./log_proxy_wrapper --help 151 | Usage: 152 | log_proxy_wrapper [OPTION?] -- COMMAND [COMMAND_ARG1] [COMMAND_ARG2] [...] - log proxy 153 | 154 | Help Options: 155 | -h, --help Show help options 156 | 157 | Application Options: 158 | -s, --rotation-size maximum size (in bytes) for a log file before rotation (0 => no maximum, default: content of environment variable LOGPROXY_ROTATION_SIZE or 104857600 (100MB)) 159 | -t, --rotation-time maximum lifetime (in seconds) for a log file before rotation (0 => no maximum, default: content of environment variable LOGPROXY_ROTATION_TIME or 86400 (24H)) 160 | -S, --rotation-suffix strftime based suffix to append to rotated log files (default: content of environment variable LOGPROXY_ROTATION_SUFFIX or .%%Y%%m%%d%%H%%M%%S) 161 | -d, --log-directory directory to store log files (default: content of environment variable LOGPROXY_LOG_DIRECTORY or current directory), directory is created if missing 162 | -n, --rotated-files maximum number of rotated files to keep including main one (0 => no cleaning, default: content of environment variable LOGPROXY_ROTATED_FILES or 5) 163 | -T, --timestamps strftime prefix to prepend to every output line (default: content of environment variable LOGPROXY_TIMESTAMPS or none) 164 | -c, --chmod if set, chmod the logfile to this value, '0700' for example (default: content of environment variable LOGPROXY_CHMOD or NULL) 165 | -o, --chown if set, try (if you don't have sufficient privileges, it will fail silently) to change the owner of the logfile to the given user value 166 | -g, --chgrp if set, try (if you don't have sufficient privileges, it will fail silently) to change the group of the logfile to the given group value 167 | -m, --use-locks use locks to append to main log file (useful if several process writes to the same file) 168 | -O, --stdout stdout file path (NULL string (default) can be used to redirect to /dev/null) 169 | -E, --stderr stderr file path (STDOUT string (default) can be used to redirect to the same file than stdout) 170 | -F, --fifo-tmp-dir directory where to store tmp FIFO for log_proxy (default: content of environment variable TMPDIR if set, /tmp if not) 171 | ``` 172 | 173 | ## How to build? 174 | 175 | ### Requirements 176 | 177 | A Linux/Unix distribution with standard development tools (`git`, `gcc`, `make`, `pkg-config`) and `glib2` library with `devel` support (provided for example in CentOS 6 in the `glib2-devel` standard package). 178 | 179 | ### Build and install 180 | 181 | ```console 182 | git clone https://github.com/metwork-framework/log_proxy # or download/unpack a zip with the github interface 183 | cd log_proxy 184 | make 185 | ``` 186 | 187 | Then as `root` user or prefixed with `sudo`: 188 | 189 | ```console 190 | make install 191 | ``` 192 | 193 | This will install `log_proxy` and `log_proxy_wrapper` in `/usr/local/bin`, by default. 194 |  195 | `make install` also supports `PREFIX=...` for installing (for example) into a `/usr` directory other than the one in `/usr/local`, and `DESTDIR=...` for installing into `$DESTDIR/$PREFIX/bin` which is useful when making packages. 196 |  197 |  198 | 199 | 200 | 201 | 202 | 203 | 204 | ## Contributing guide 205 | 206 | See [CONTRIBUTING.md](CONTRIBUTING.md) file. 207 | 208 | 209 | 210 | ## Code of Conduct 211 | 212 | See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) file. 213 | 214 | 215 | 216 | ## Sponsors 217 | 218 | *(If you are officially paid to work on MetWork Framework, please contact us to add your company logo here!)* 219 | 220 | [![logo](https://raw.githubusercontent.com/metwork-framework/resources/master/sponsors/meteofrance-small.jpeg)](http://www.meteofrance.com) 221 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM metwork/centos6 2 | MAINTAINER Fabien MARTY 3 | 4 | RUN yum -y install gcc make valgrind glib2-devel glib2-static glibc-static 5 | 6 | ADD entrypoint.sh /entrypoint.sh 7 | CMD /entrypoint.sh 8 | -------------------------------------------------------------------------------- /docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | make release 4 | -------------------------------------------------------------------------------- /installer/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # paranoid mode 4 | set -eu 5 | 6 | # some vars 7 | ORG=metwork-framework 8 | REPO=log_proxy 9 | CURL_OPTS="-fsSLk" 10 | PREFIX=/usr/local 11 | 12 | # Check if we are root (or if we used FORCE argument) 13 | IDU=$(id -u) 14 | if test "${IDU}" != "0"; then 15 | if test "${1:-}" != "FORCE"; then 16 | echo "ERROR: you must run this script as root user" 17 | echo " (or use FORCE argument if you know exactly what you are doing)" 18 | exit 1 19 | fi 20 | fi 21 | 22 | GITHUB_URL="https://api.github.com/repos/${ORG}/${REPO}/releases/latest" 23 | echo "Getting latest release on ${GITHUB_URL}..." 24 | DOWNLOAD_URL=$(curl "${CURL_OPTS}" "${GITHUB_URL}" |grep "browser_download_url.:..https.*tar.gz" |cut -d : -f 2,3 | tr -d \" |tr -d ' ') 25 | RELEASE=$(echo "${DOWNLOAD_URL}" |sed 's~.*/\(v[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\)/.*$~\1~g') 26 | if test "${RELEASE}" = ""; then 27 | echo "ERROR: can't get the latest release version. Please retry in a few minutes." 28 | exit 1 29 | fi 30 | echo "=> Found release: ${RELEASE}" 31 | echo "=> Found download url: ${DOWNLOAD_URL}" 32 | 33 | echo "Removing old releases..." 34 | rm -Rf /opt/log_proxy-linux64-v* >/dev/null 2>&1 35 | rm -Rf /opt/log_proxy >/dev/null 2>&1 36 | rm -Rf /opt/metwork-framework-log_proxy* >/dev/null 2>&1 37 | rm -f "${PREFIX}/bin/log_proxy" >/dev/null 2>&1 38 | rm -f "${PREFIX}/bin/log_proxy_wrapper" >/dev/null 2>&1 39 | 40 | cd "${TMPDIR:-/tmp}" || exit 1 41 | echo "Downloading ${DOWNLOAD_URL} into $(pwd)/${ORG}-${REPO}-${RELEASE}.tar.gz..." 42 | curl "${CURL_OPTS}" "${DOWNLOAD_URL}" >"${ORG}-${REPO}-${RELEASE}.tar.gz" 43 | echo "Installing..." 44 | zcat "${ORG}-${REPO}-${RELEASE}.tar.gz" |tar xf - 45 | mkdir -p "${PREFIX}/bin" 46 | for F in log_proxy log_proxy_wrapper; do 47 | cp -f "log_proxy-linux64-${RELEASE}/${F}" "${PREFIX}/bin/" 48 | chmod a+rx "${PREFIX}/bin/${F}" 49 | done 50 | echo "Cleaning..." 51 | rm -f "log_proxy-linux64-${RELEASE}.tar.gz" 52 | rm -Rf "log_proxy-linux64-${RELEASE}" 53 | echo "Done" 54 | -------------------------------------------------------------------------------- /installer/uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # paranoid mode 4 | set -eu 5 | 6 | # Check if we are root (or if we used FORCE argument) 7 | IDU=$(id -u) 8 | if test "${IDU}" != "0"; then 9 | if test "${1:-}" != "FORCE"; then 10 | echo "ERROR: you must run this script as root user" 11 | echo " (or use FORCE argument if you know exactly what you are doing)" 12 | exit 1 13 | fi 14 | fi 15 | 16 | echo "Uninstalling..." 17 | rm -f /usr/local/bin/log_proxy 18 | rm -f /usr/local/bin/log_proxy_wrapper 19 | echo "Done" 20 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.organization=metwork-framework 2 | sonar.projectKey=metwork-framework_log_proxy 3 | -------------------------------------------------------------------------------- /src/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: coverage leak test clean all 2 | 3 | DEBUG=no 4 | FORCE_RPATH= 5 | DESTDIR= 6 | PREFIX=/usr/local 7 | STATIC=no 8 | GCC?=gcc 9 | CFLAGS+=-D_XOPEN_SOURCE=700 10 | GCC_VERSION=`$(GCC) --version | head -1 | cut -d" " -f3 | cut -d"." -f1-3` 11 | 12 | ifeq ($(COVERAGE),yes) 13 | COVERAGE_CFLAGS=-fprofile-arcs -ftest-coverage 14 | else 15 | COVERAGE_CFLAGS= 16 | endif 17 | ifeq ($(STATIC),yes) 18 | _STATIC_EXTRA_OPT=--static 19 | _STATIC_OPT=-static 20 | else 21 | _STATIC_EXTRA_OPT= 22 | _STATIC_OPT= 23 | endif 24 | ifeq ($(FORCE_RPATH),) 25 | FORCE_RPATH_STR= 26 | else 27 | FORCE_RPATH_STR=-Wl,-rpath=~$(FORCE_RPATH)~ 28 | endif 29 | ifeq ($(DEBUG),yes) 30 | DEBUG_CFLAGS=-g -Werror 31 | else 32 | DEBUG_CFLAGS=-O2 -Os 33 | endif 34 | ifeq ($(shell expr $(GCC_VERSION) \< "8.0.0" ), 1) 35 | _CFLAGS=$(CFLAGS) -I. $(shell pkg-config --cflags $(_STATIC_EXTRA_OPT) glib-2.0 gthread-2.0) -fPIC -Wall -std=c99 -Wextra -pedantic -Wshadow -Wstrict-overflow -Wno-deprecated-declarations -fno-strict-aliasing -DG_LOG_DOMAIN=\"log_proxy\" $(DEBUG_CFLAGS) $(COVERAGE_CFLAGS) 36 | else 37 | _CFLAGS=$(CFLAGS) -I. $(shell pkg-config --cflags $(_STATIC_EXTRA_OPT) glib-2.0 gthread-2.0) -fPIC -Wall -std=c99 -Wextra -pedantic -Wshadow -Wstrict-overflow -Wno-deprecated-declarations -Wno-cast-function-type -fno-strict-aliasing -DG_LOG_DOMAIN=\"log_proxy\" $(DEBUG_CFLAGS) $(COVERAGE_CFLAGS) 38 | endif 39 | _LDFLAGS=$(LDFLAGS) -L. $(shell pkg-config --libs $(_STATIC_EXTRA_OPT) glib-2.0 gthread-2.0) $(shell echo '$(FORCE_RPATH_STR)' |sed 's/@/$$/g' |sed s/~/"'"/g) -lrt 40 | 41 | OBJECTS=util.o control.o out.o 42 | BINARIES=log_proxy log_proxy_wrapper 43 | TESTS=test_log_proxy 44 | LIBS= 45 | 46 | VALGRIND=./valgrind.sh 47 | 48 | all:: $(OBJECTS) $(BINARIES) $(TESTS) $(LIBS) 49 | 50 | clean:: 51 | rm -f $(OBJECTS) $(BINARIES) $(TESTS) core.* vgcore.* 52 | rm -Rf coverage 53 | rm -f app.info *.gcno *.gcda 54 | 55 | log_proxy: log_proxy.c $(OBJECTS) options.h 56 | $(GCC) $(_STATIC_OPT) $(_CFLAGS) -o $@ $^ $(_LDFLAGS) 57 | 58 | log_proxy_wrapper: log_proxy_wrapper.c $(OBJECTS) options.h 59 | $(GCC) $(_STATIC_OPT) $(_CFLAGS) -o $@ $^ $(_LDFLAGS) 60 | 61 | test_log_proxy: test_log_proxy.c $(OBJECTS) 62 | $(GCC) $(_STATIC_OPT) $(_CFLAGS) -o $@ $^ $(_LDFLAGS) 63 | 64 | control.o: control.c control.h 65 | $(GCC) -c -o $@ $(_CFLAGS) $< 66 | 67 | out.o: out.c out.h 68 | $(GCC) -c -o $@ $(_CFLAGS) $< 69 | 70 | util.o: util.c util.h 71 | $(GCC) -c -o $@ $(_CFLAGS) $< 72 | 73 | install: $(BINARIES) 74 | mkdir -p $(DESTDIR)$(PREFIX)/bin 75 | cp -f $(BINARIES) $(DESTDIR)$(PREFIX)/bin 76 | 77 | test: $(TESTS) 78 | ./test_log_proxy && echo "OK" 79 | 80 | leak: $(TESTS) 81 | $(VALGRIND) ./test_log_proxy && echo "OK" 82 | 83 | coverage: 84 | $(MAKE) clean 85 | $(MAKE) COVERAGE=yes test_log_proxy 86 | rm -Rf coverage/* app*.info && lcov --directory . --zerocounters 87 | ./test_log_proxy 88 | lcov --directory . --capture --output-file app.info 89 | if ! test -d coverage; then mkdir coverage; fi; genhtml --output-directory coverage app.info 90 | -------------------------------------------------------------------------------- /src/control.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "control.h" 8 | 9 | gchar *_get_control_file_path(const gchar *path) { 10 | gchar *dirpath = g_path_get_dirname(path); 11 | gchar *basename = g_path_get_basename(path); 12 | gchar *cfile = g_strdup_printf("%s/.%s.control", dirpath, basename); 13 | g_free(dirpath); 14 | g_free(basename); 15 | return(cfile); 16 | } 17 | 18 | /** 19 | * Init the control file with the given content. 20 | * 21 | * If the file already exists or if errors, FALSE is returned. 22 | * 23 | * @param path log file path. 24 | * @param content the content to put in the control file. 25 | * @return TRUE if ok, FALSE if the control file is already here. 26 | */ 27 | gboolean init_control_file(const gchar *path, const gchar *content) { 28 | gboolean res = FALSE; 29 | gchar *cfile = _get_control_file_path(path); 30 | int fd = open(cfile, O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); 31 | if (fd < 0) { 32 | g_free(cfile); 33 | return FALSE; 34 | } 35 | size_t content_size = strlen(content); 36 | ssize_t write_size = write(fd, content, content_size); 37 | if (write_size > 0) { 38 | if ((size_t) write_size == content_size) { 39 | res = TRUE; 40 | } 41 | } 42 | g_free(cfile); 43 | close(fd); 44 | return res; 45 | } 46 | 47 | /** 48 | * Lock the control file. 49 | * 50 | * If the returned value is >=0, we have an exclusive lock. 51 | * 52 | * @param path log file path. 53 | * @return a file descriptor (< 0 in case of errors) 54 | */ 55 | int lock_control_file(const gchar *path) { 56 | int fd = -1; 57 | while (TRUE) { 58 | gchar *cfile = _get_control_file_path(path); 59 | fd = open(cfile, O_CREAT | O_WRONLY, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); 60 | g_free(cfile); 61 | if (fd < 0) { 62 | if (errno == EINTR) { 63 | // try another time 64 | continue; 65 | } 66 | return -1; 67 | } else { 68 | break; 69 | } 70 | } 71 | while (TRUE) { 72 | int res = flock(fd, LOCK_EX); 73 | if (res < 0) { 74 | if (errno == EINTR) { 75 | // try another time 76 | continue; 77 | } 78 | return -1; 79 | } else { 80 | break; 81 | } 82 | } 83 | return fd; 84 | } 85 | 86 | /** 87 | * Unlock the control file. 88 | * 89 | * @param fd the file descriptor got with lock_control_file. 90 | */ 91 | void unlock_control_file(int fd) { 92 | while (TRUE) { 93 | int res = close(fd); 94 | if (res < 0) { 95 | if (errno == EINTR) { 96 | // try another time 97 | continue; 98 | } 99 | } 100 | break; 101 | } 102 | } 103 | 104 | /** 105 | * Get the control file content 106 | * 107 | * If there are some errors, NULL is returned. 108 | * 109 | * @param path log file path. 110 | * @return newly allocated string with the content (free with g_free). 111 | */ 112 | gchar *get_control_file_content(const gchar *path) { 113 | gchar *cfile = _get_control_file_path(path); 114 | gchar *contents = NULL; 115 | gboolean res = g_file_get_contents(cfile, &contents, NULL, NULL); 116 | g_free(cfile); 117 | if (res == TRUE) { 118 | return contents; 119 | } else { 120 | return NULL; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/control.h: -------------------------------------------------------------------------------- 1 | #ifndef CONTROL_H_ 2 | #define CONTROL_H_ 3 | 4 | #include 5 | 6 | gboolean init_control_file(const gchar *path, const gchar *content); 7 | int lock_control_file(const gchar *path); 8 | void unlock_control_file(int fd); 9 | gchar *get_control_file_content(const gchar *path); 10 | 11 | #endif /* CONTROL_H_ */ 12 | -------------------------------------------------------------------------------- /src/log_proxy.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include "out.h" 13 | #include "util.h" 14 | #include "control.h" 15 | #include "options.h" 16 | 17 | struct sigaction sigact; 18 | static gboolean first_iteration = TRUE; 19 | static GMutex *mutex = NULL; 20 | static GAsyncQueue *queue = NULL; 21 | static volatile gboolean stop_signal = FALSE; 22 | static GIOChannel *in = NULL; 23 | 24 | gint _list_compare(gconstpointer a, gconstpointer b) { 25 | gchar *ca = (gchar *) a; 26 | gchar *cb = (gchar *) b; 27 | return g_strcmp0(cb, ca); 28 | } 29 | 30 | #define UNUSED(x) (void)(x) 31 | 32 | void clean_too_old_files() { 33 | gchar *dirpath = g_path_get_dirname(log_file); 34 | GDir *dir = g_dir_open(dirpath, 0, NULL); 35 | if (dir == NULL) { 36 | g_warning("can't read dir: %s", dirpath); 37 | g_free(dirpath); 38 | return; 39 | } 40 | gchar *basename = g_path_get_basename(log_file); 41 | gchar *control = g_strdup_printf(".%s.control", basename); 42 | GList *list = NULL; 43 | while (TRUE) { 44 | const gchar *name = g_dir_read_name(dir); 45 | if (name == NULL) { 46 | break; 47 | } 48 | if (g_strcmp0(name, control) == 0) { 49 | continue; 50 | } 51 | if (g_strcmp0(name, basename) == 0) { 52 | continue; 53 | } 54 | if (g_str_has_prefix(name, basename)) { 55 | gchar *filepath = g_strdup_printf("%s/%s", dirpath, name); 56 | list = g_list_insert_sorted(list, filepath, 57 | (GCompareFunc) _list_compare); 58 | } 59 | } 60 | if ((gint) g_list_length(list) > rotated_files) { 61 | GList *list2 = g_list_nth(list, rotated_files - 1); 62 | GList *l; 63 | for (l = list2; l != NULL; l = l->next) { 64 | int res = g_unlink((const gchar*) l->data); 65 | if (res < 0) { 66 | g_warning("can't unlink: %s", (gchar*) l->data); 67 | } 68 | } 69 | } 70 | g_list_free_full(list, g_free); 71 | g_free(control); 72 | g_free(dirpath); 73 | g_free(basename); 74 | g_dir_close(dir); 75 | } 76 | 77 | gboolean rotate() { 78 | gboolean result = FALSE; 79 | gchar *rotated_file = compute_strftime_suffix(log_file, rotation_suffix); 80 | if (rotated_file == NULL) { 81 | return FALSE; 82 | } 83 | if (g_file_test(rotated_file, G_FILE_TEST_EXISTS) == TRUE) { 84 | gchar *new_rotated_file = g_strdup_printf("%s.%s", rotated_file, get_unique_hexa_identifier()); 85 | g_free(rotated_file); 86 | rotated_file = new_rotated_file; 87 | } 88 | int res = g_rename(log_file, rotated_file); 89 | if (res == 0) { 90 | result = TRUE; 91 | } else { 92 | g_warning("can't rotate %s => %s (%i)", log_file, rotated_file, errno); 93 | } 94 | g_free(rotated_file); 95 | return result; 96 | } 97 | 98 | void signal_handler(int signum) { 99 | if ((signum == SIGINT) || (signum == SIGTERM)) { 100 | stop_signal = TRUE; 101 | } 102 | } 103 | 104 | static void every_second() { 105 | int fd = lock_control_file(log_file); 106 | if (fd >= 0) { 107 | if (first_iteration) { 108 | // A little bit of cleaning for first iteration 109 | first_iteration = FALSE; 110 | clean_too_old_files(); 111 | } 112 | if (test_output_channel_rotated() == TRUE) { 113 | // another program rotated our log file 114 | // => let's reinit the output channel 115 | destroy_output_channel(); 116 | init_output_channel(log_file, use_locks, TRUE, chmod_str, chown_str, chgrp_str, timestamp_prefix); 117 | unlock_control_file(fd); 118 | return; 119 | } 120 | glong size = get_file_size(log_file); 121 | if (size < 0) { 122 | unlock_control_file(fd); 123 | return; 124 | } 125 | gboolean must_rotate = FALSE; 126 | if (rotation_size > 0) { 127 | if (size > rotation_size) { 128 | must_rotate = TRUE; 129 | } 130 | } 131 | if (rotation_time > 0) { 132 | if (get_output_channel_age() > rotation_time) { 133 | must_rotate = TRUE; 134 | } 135 | } 136 | if (must_rotate) { 137 | gboolean rotate_res = rotate(); 138 | if (rotated_files > 0) { 139 | clean_too_old_files(); 140 | } 141 | if (rotate_res == TRUE) { 142 | destroy_output_channel(); 143 | init_output_channel(log_file, use_locks, TRUE, chmod_str, chown_str, chgrp_str, timestamp_prefix); 144 | } 145 | } 146 | unlock_control_file(fd); 147 | } 148 | } 149 | 150 | gpointer stop_thread(gpointer data) { 151 | // we do this here and not in signal_handler to avoid malloc in signal 152 | // handlers 153 | UNUSED(data); 154 | while (stop_signal == FALSE) { 155 | sleep(1); 156 | } 157 | g_async_queue_push(queue, GINT_TO_POINTER(2)); 158 | return NULL; 159 | } 160 | 161 | gpointer management_thread(gpointer data) { 162 | gpointer qdata; 163 | GTimeVal tval; 164 | gint stop_flag = 0; 165 | UNUSED(data); 166 | while (TRUE) { 167 | g_get_current_time(&tval); 168 | g_time_val_add(&tval, 1000000); 169 | qdata = g_async_queue_timed_pop(queue, &tval); 170 | if (qdata != NULL) { 171 | stop_flag = GPOINTER_TO_INT(qdata); 172 | } 173 | g_mutex_lock(mutex); 174 | every_second(); 175 | g_mutex_unlock(mutex); 176 | if (qdata != NULL) { 177 | break; 178 | } 179 | } 180 | if (stop_flag == 2) { 181 | // in this case (sigterm), we prefer to keep the mutex 182 | g_mutex_lock(mutex); 183 | } 184 | destroy_output_channel(); 185 | if (rm_fifo_at_exit == TRUE) { 186 | if (fifo != NULL) { 187 | g_unlink(fifo); 188 | } 189 | } 190 | if (stop_flag == 2) { 191 | // we exit here for sigterm as main thread is blocked in reading 192 | exit(0); 193 | } 194 | return NULL; 195 | } 196 | 197 | void init_or_reinit_output_channel(const gchar *lg_file, gboolean us_locks) { 198 | int lock_fd = lock_control_file(lg_file); 199 | if (lock_fd < 0) { 200 | g_critical("can't lock control file for log_file=%s => exiting", lg_file); 201 | exit(2); 202 | } 203 | destroy_output_channel(); 204 | init_output_channel(lg_file, us_locks, FALSE, chmod_str, chown_str, chgrp_str, timestamp_prefix); 205 | unlock_control_file(lock_fd); 206 | } 207 | 208 | int main(int argc, char *argv[]) 209 | { 210 | GOptionContext *context; 211 | setlocale(LC_ALL, ""); 212 | context = g_option_context_new("LOGFILE - log proxy"); 213 | g_option_context_add_main_entries(context, entries, NULL); 214 | gchar *description = "Optional environment variables to override defaults: \n LOGPROXY_ROTATION_SIZE\n LOGPROXY_ROTATION_TIME\n LOGPROXY_ROTATION_SUFFIX\n LOGPROXY_LOG_DIRECTORY\n LOGPROXY_ROTATED_FILES\n LOGPROXY_TIMESTAMPS\n\nExample for rotation-size option:\n- If log_proxy is run with the option --rotation-size on the command line, rotation-size will take the provided value\n- If the option --rotation-size is not provided on command line :\n - If the environment variable LOGPROXY_ROTATION_SIZE is set, rotation-size will take this value\n - If the environment variable LOGPROXY_ROTATION_SIZE is not set, rotation-size will take the default value 104857600\n"; 215 | g_option_context_set_description(context, description); 216 | if (!g_option_context_parse(context, &argc, &argv, NULL)) { 217 | g_print("%s", g_option_context_get_help(context, TRUE, NULL)); 218 | exit(1); 219 | } 220 | if (argc < 2) { 221 | g_print("%s", g_option_context_get_help(context, TRUE, NULL)); 222 | exit(1); 223 | } 224 | g_thread_init(NULL); 225 | mutex = g_mutex_new(); 226 | queue = g_async_queue_new(); 227 | signal(SIGTERM, signal_handler); 228 | signal(SIGINT, signal_handler); 229 | set_default_values_from_env(); 230 | log_file = compute_file_path(log_directory, argv[1]); 231 | // Create log directory if not existing 232 | gchar *log_dir = g_path_get_dirname(log_file); 233 | if ( ! g_file_test(log_dir, G_FILE_TEST_IS_DIR) ) { 234 | if ( g_mkdir_with_parents(log_dir, 0755) == -1 ) { 235 | g_critical("Can't create directory %s => exit", log_dir); 236 | return 1; 237 | } 238 | } 239 | g_free(log_dir); 240 | if (fifo == NULL) { 241 | // We read from stdin 242 | in = g_io_channel_unix_new(fileno(stdin)); 243 | } else { 244 | GError *error = NULL; 245 | in = g_io_channel_new_file(fifo, "r", &error); 246 | if (in == NULL) { 247 | g_critical("Can't open %s => exit", fifo); 248 | return 1; 249 | } 250 | } 251 | g_io_channel_set_encoding(in, NULL, NULL); 252 | GIOStatus in_status = G_IO_STATUS_NORMAL; 253 | GString *in_buffer = g_string_new(NULL); 254 | init_or_reinit_output_channel(log_file, use_locks); 255 | g_thread_create(stop_thread, NULL, FALSE, NULL); 256 | GThread* management = g_thread_create(management_thread, NULL, TRUE, NULL); 257 | while ((in_status != G_IO_STATUS_EOF) && (in_status != G_IO_STATUS_ERROR)) { 258 | in_status = g_io_channel_read_line_string(in, in_buffer, NULL, NULL); 259 | if (in_status == G_IO_STATUS_NORMAL) { 260 | g_mutex_lock(mutex); 261 | while (TRUE) { 262 | gboolean write_status = write_output_channel(in_buffer); 263 | if (write_status == FALSE) { 264 | g_warning("error during write on: %s", log_file); 265 | init_or_reinit_output_channel(log_file, use_locks); 266 | break; 267 | } 268 | break; 269 | } 270 | g_mutex_unlock(mutex); 271 | } 272 | } 273 | // if we are here, the "in" input channel is closed 274 | signal(SIGINT, SIG_DFL); 275 | signal(SIGTERM, SIG_DFL); 276 | g_async_queue_push(queue, GINT_TO_POINTER(1)); 277 | g_thread_join(management); 278 | g_io_channel_shutdown(in, FALSE, NULL); 279 | g_io_channel_unref(in); 280 | g_string_free(in_buffer, TRUE); 281 | g_option_context_free(context); 282 | g_free(log_file); 283 | g_mutex_free(mutex); 284 | return 0; 285 | } 286 | -------------------------------------------------------------------------------- /src/log_proxy_wrapper.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "util.h" 9 | #include "options.h" 10 | 11 | static gchar *stdout_path = "NULL"; 12 | static gchar *stderr_path = "STDOUT"; 13 | static gchar *fifo_tmp_dir = NULL; 14 | static gchar *command = NULL; 15 | static gchar **command_args = NULL; 16 | static GOptionEntry new_entry1 = { "stdout", 'O', 0, G_OPTION_ARG_STRING, &stdout_path, "stdout file path (NULL string (default) can be used to redirect to /dev/null)", NULL }; 17 | static GOptionEntry new_entry2 = { "stderr", 'E', 0, G_OPTION_ARG_STRING, &stderr_path, "stderr file path (STDOUT string (default) can be used to redirect to the same file than stdout)", NULL }; 18 | static GOptionEntry new_entry3 = { "fifo-tmp-dir", 'F', 0, G_OPTION_ARG_STRING, &fifo_tmp_dir, "directory where to store tmp FIFO for log_proxy (default: content of environment variable TMPDIR if set, /tmp if not)", NULL }; 19 | 20 | GOptionEntry *change_options() 21 | { 22 | int number_of_options = sizeof(entries) / sizeof(entries[0]); 23 | // we remove 2 options and we add 3 new 24 | GOptionEntry *res = g_malloc(sizeof(GOptionEntry) * (number_of_options + 1)); 25 | for (int i = 0 ; i < number_of_options - 1; i++) { 26 | if (g_strcmp0(entries[i].long_name, "fifo") == 0) { 27 | continue; 28 | } 29 | if (g_strcmp0(entries[i].long_name, "rm-fifo-at-exit") == 0) { 30 | continue; 31 | } 32 | res[i] = entries[i]; 33 | } 34 | res[number_of_options - 3] = new_entry1; 35 | res[number_of_options - 2] = new_entry2; 36 | res[number_of_options - 1] = new_entry3; 37 | res[number_of_options] = entries[number_of_options - 1]; 38 | return res; 39 | } 40 | 41 | gchar *make_fifo(const gchar *label) { 42 | const gchar *tmpdir = fifo_tmp_dir; 43 | if ( tmpdir == NULL ) { 44 | tmpdir = g_getenv("TMPDIR"); 45 | if (tmpdir == NULL) { 46 | tmpdir = "/tmp"; 47 | } 48 | } 49 | gchar *uid = get_unique_hexa_identifier(); 50 | gchar *path = g_strdup_printf("%s/log_proxy_%s_%s.fifo", tmpdir, label, uid); 51 | int res = mkfifo(path, S_IRUSR | S_IWUSR); 52 | if (res != 0) { 53 | g_critical("can't create fifo: %s", path); 54 | exit(1); 55 | } 56 | g_free(uid); 57 | return path; 58 | } 59 | 60 | void spawn_logproxy_async(const gchar *fifo_path, const gchar *log_path) { 61 | gchar *rotation_size_str = g_strdup_printf("%li", rotation_size); 62 | gchar *rotation_time_str = g_strdup_printf("%li", rotation_time); 63 | gchar *rotated_files_str = g_strdup_printf("%i", rotated_files); 64 | gchar *argv[25] = { 65 | "log_proxy", 66 | "-s", rotation_size_str, 67 | "-t", rotation_time_str, 68 | "-S", rotation_suffix, 69 | "-d", log_directory, 70 | "-n", rotated_files_str, 71 | "-r", "-f", (gchar*) fifo_path 72 | }; 73 | int argc = 14; // initial number of fixed arguments above 74 | 75 | if (use_locks) { 76 | argv[argc++] = "--use-locks"; 77 | } 78 | if (timestamp_prefix != NULL) { 79 | argv[argc++] = "-T"; 80 | argv[argc++] = timestamp_prefix; 81 | } 82 | if (chmod_str != NULL) { 83 | argv[argc++] = "-c"; 84 | argv[argc++] = chmod_str; 85 | } 86 | if (chown_str != NULL) { 87 | argv[argc++] = "-o"; 88 | argv[argc++] = chown_str; 89 | } 90 | if (chgrp_str != NULL) { 91 | argv[argc++] = "-g"; 92 | argv[argc++] = chgrp_str; 93 | } 94 | argv[argc++] = (gchar*) log_path; 95 | argv[argc] = NULL; 96 | 97 | gboolean spawn_res = g_spawn_async( 98 | NULL, argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, NULL, NULL ); 99 | if (spawn_res == FALSE) { 100 | g_critical("can't spawn [ %s ] => exit", g_strjoinv(" ", argv)); 101 | exit(1); 102 | } 103 | 104 | g_free(rotation_size_str); 105 | g_free(rotation_time_str); 106 | g_free(rotated_files_str); 107 | } 108 | 109 | int main(int argc, char *argv[]) 110 | { 111 | GOptionContext *context; 112 | setlocale(LC_ALL, ""); 113 | log_file = NULL; // not used 114 | context = g_option_context_new("-- COMMAND [COMMAND_ARG1] [COMMAND_ARG2] [...] - log proxy"); 115 | GOptionEntry *new_entries = change_options(); 116 | g_option_context_add_main_entries(context, new_entries, NULL); 117 | if (!g_option_context_parse(context, &argc, &argv, NULL)) { 118 | g_print("%s", g_option_context_get_help(context, TRUE, NULL)); 119 | exit(1); 120 | } 121 | if (argc < 2) { 122 | g_print("%s", g_option_context_get_help(context, TRUE, NULL)); 123 | exit(1); 124 | } 125 | set_default_values_from_env(); 126 | if (g_strcmp0(stderr_path, "STDOUT") == 0) { 127 | stderr_path = stdout_path; 128 | } 129 | if (g_strcmp0(stdout_path, "NULL") == 0) { 130 | stdout_path = "/dev/null"; 131 | } 132 | if (g_strcmp0(stderr_path, "NULL") == 0) { 133 | stderr_path = "/dev/null"; 134 | } 135 | int index = 1; 136 | if (g_strcmp0(argv[index], "--") == 0) { 137 | if (argc < 3) { 138 | g_print("%s", g_option_context_get_help(context, TRUE, NULL)); 139 | exit(1); 140 | } 141 | index++; 142 | } 143 | command = g_strdup(argv[index]); 144 | int command_args_length = argc - index - 1; 145 | command_args = g_malloc(sizeof(gchar*) * (command_args_length + 2)); 146 | command_args[0] = command; 147 | for (int i = 0 ; i < command_args_length ; i++) { 148 | command_args[i + 1] = argv[index + i + 1]; 149 | } 150 | command_args[command_args_length + 1] = NULL; 151 | gchar *stdout_fifo = g_strdup("/dev/null"); 152 | gchar *stderr_fifo = g_strdup("/dev/null"); 153 | if (g_strcmp0(stdout_path, "/dev/null") != 0) { 154 | stdout_fifo = make_fifo("stdout"); 155 | spawn_logproxy_async(stdout_fifo, stdout_path); 156 | } else { 157 | stdout_fifo = g_strdup("/dev/null"); 158 | } 159 | if (g_strcmp0(stdout_path, stderr_path) != 0) { 160 | if (g_strcmp0(stdout_path, "/dev/null") != 0) { 161 | stderr_fifo = make_fifo("stderr"); 162 | spawn_logproxy_async(stderr_fifo, stderr_path); 163 | } else { 164 | stderr_fifo = "/dev/null"; 165 | } 166 | } else { 167 | stderr_fifo = g_strdup(stdout_fifo); 168 | } 169 | int bak_stdout = dup(1); 170 | int bak_stderr = dup(2); 171 | int new_stdout = open(stdout_fifo, O_WRONLY); 172 | int new_stderr = open(stderr_fifo, O_WRONLY); 173 | dup2(new_stdout, 1); 174 | dup2(new_stderr, 2); 175 | close(new_stdout); 176 | close(new_stderr); 177 | execvp(command, command_args); 178 | // if we are here, there is an error 179 | fflush(stdout); 180 | fflush(stderr); 181 | dup2(bak_stdout, 1); 182 | dup2(bak_stderr, 2); 183 | close(bak_stdout); 184 | close(bak_stderr); 185 | g_critical("can't launch %s command with error: %i [%s]", command, errno, strerror(errno)); 186 | g_option_context_free(context); 187 | return 1; 188 | } 189 | -------------------------------------------------------------------------------- /src/options.h: -------------------------------------------------------------------------------- 1 | #ifndef OPTIONS_H_ 2 | #define OPTIONS_H_ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | static gchar *log_file = NULL; 11 | static glong rotation_size = -1; 12 | static glong rotation_time = -1; 13 | static gchar *rotation_suffix = NULL; 14 | static gchar *timestamp_prefix = NULL; 15 | static gchar *log_directory = NULL; 16 | static gchar *chmod_str = NULL; 17 | static gchar *chown_str = NULL; 18 | static gchar *chgrp_str = NULL; 19 | static gint rotated_files = -1; 20 | static gboolean rm_fifo_at_exit = FALSE; 21 | static gchar *fifo = NULL; 22 | static gboolean use_locks = FALSE; 23 | 24 | void set_default_values_from_env() 25 | { 26 | const gchar *env_val; 27 | struct rlimit rl; 28 | getrlimit(RLIMIT_FSIZE, &rl); 29 | if ( rotation_size == -1 ) { 30 | env_val = g_getenv("LOGPROXY_ROTATION_SIZE"); 31 | if ( env_val != NULL ) { 32 | rotation_size = atoi(env_val); 33 | } 34 | else { 35 | rotation_size = 104857600; 36 | } 37 | } 38 | if ( (long long int) rl.rlim_max != -1 ) { 39 | long long int val_max = (long long int) rl.rlim_max * 90 / 100; 40 | if ( rotation_size > (int) val_max ) { 41 | rotation_size = (int) val_max; 42 | } 43 | } 44 | 45 | if ( rotation_time == -1 ) { 46 | env_val = g_getenv("LOGPROXY_ROTATION_TIME"); 47 | if ( env_val != NULL ) { 48 | rotation_time = atoi(env_val); 49 | } 50 | else { 51 | rotation_time = 86400; 52 | } 53 | } 54 | 55 | if ( rotation_suffix == NULL ) { 56 | env_val = g_getenv("LOGPROXY_ROTATION_SUFFIX"); 57 | if ( env_val != NULL ) { 58 | rotation_suffix = (gchar *)env_val; 59 | } 60 | else { 61 | rotation_suffix = ".%Y%m%d%H%M%S"; 62 | } 63 | } 64 | 65 | if ( rotated_files == -1 ) { 66 | env_val = g_getenv("LOGPROXY_ROTATED_FILES"); 67 | if ( env_val != NULL ) { 68 | rotated_files = atoi(env_val); 69 | } 70 | else { 71 | rotated_files = 5; 72 | } 73 | } 74 | 75 | if ( timestamp_prefix == NULL ) { 76 | env_val = g_getenv("LOGPROXY_TIMESTAMPS"); 77 | if ( env_val != NULL ) { 78 | timestamp_prefix = (gchar *)env_val; 79 | } 80 | } 81 | if ( timestamp_prefix != NULL && strlen(timestamp_prefix) == 0 ) { 82 | timestamp_prefix = NULL; 83 | } 84 | 85 | if ( log_directory == NULL ) { 86 | env_val = g_getenv("LOGPROXY_LOG_DIRECTORY"); 87 | if ( env_val != NULL ) { 88 | log_directory = (gchar *)env_val; 89 | } 90 | else { 91 | log_directory = g_get_current_dir(); 92 | } 93 | } 94 | 95 | if ( chmod_str == NULL ) { 96 | env_val = g_getenv("LOGPROXY_CHMOD"); 97 | if ( env_val != NULL ) { 98 | chmod_str = (gchar *)env_val; 99 | } 100 | } 101 | } 102 | 103 | static GOptionEntry entries[] = { 104 | { "rotation-size", 's', 0, G_OPTION_ARG_INT64, &rotation_size, "maximum size (in bytes) for a log file before rotation (0 => no maximum, default: content of environment variable LOGPROXY_ROTATION_SIZE or 104857600 = 100MB)", NULL }, 105 | { "rotation-time", 't', 0, G_OPTION_ARG_INT64, &rotation_time, "maximum lifetime (in seconds) for a log file before rotation (0 => no maximum, default: content of environment variable LOGPROXY_ROTATION_TIME or 86400 = 24 hours)", NULL }, 106 | { "rotation-suffix", 'S', 0, G_OPTION_ARG_STRING, &rotation_suffix, "strftime based suffix to append to rotated log files (default: content of environment variable LOGPROXY_ROTATION_SUFFIX or .%Y%m%d%H%M%S)", NULL }, 107 | { "log-directory", 'd', 0, G_OPTION_ARG_STRING, &log_directory, "directory to store log files (default: content of environment variable LOGPROXY_LOG_DIRECTORY or current directory), directory is created if missing", NULL }, 108 | { "rotated-files", 'n', 0, G_OPTION_ARG_INT, &rotated_files, "maximum number of rotated files to keep including main one (0 => no cleaning, default: content of environment variable LOGPROXY_ROTATED_FILES or 5)", NULL }, 109 | { "timestamps", 'T', 0, G_OPTION_ARG_STRING, ×tamp_prefix, "strftime prefix to prepend to every output line, for example '[%F %T] ' (default: content of environment variable LOGPROXY_TIMESTAMPS or none)", NULL }, 110 | { "chmod", 'c', 0, G_OPTION_ARG_STRING, &chmod_str, "if set, change mode of log files to this octal value, '0600' for example (default: content of environment variable LOGPROXY_CHMOD or don't change mode)", NULL }, 111 | { "chown", 'o', 0, G_OPTION_ARG_STRING, &chown_str, "if set, try (if you don't have sufficient privileges, it will fail silently) to change the owner of the logfile to the given user value", NULL }, 112 | { "chgrp", 'g', 0, G_OPTION_ARG_STRING, &chgrp_str, "if set, try (if you don't have sufficient privileges, it will fail silently) to change the group of the logfile to the given group value", NULL }, 113 | { "use-locks", 'm', 0, G_OPTION_ARG_NONE, &use_locks, "use locks to append to main log file (useful if several process writes to the same file)", NULL }, 114 | { "fifo", 'f', 0, G_OPTION_ARG_STRING, &fifo, "if set, read lines on this fifo instead of stdin", NULL }, 115 | { "rm-fifo-at-exit", 'r', 0, G_OPTION_ARG_NONE, &rm_fifo_at_exit, "if set, drop fifo at then end of the program (you have to use --fifo option of course)", NULL }, 116 | { NULL } 117 | }; 118 | 119 | #endif /* OPTIONS_H_ */ 120 | -------------------------------------------------------------------------------- /src/out.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "util.h" 10 | #include "control.h" 11 | 12 | static GIOChannel *_out_channel = NULL; 13 | static gboolean _use_locks = FALSE; 14 | static gchar *_log_file = NULL; 15 | static glong _log_file_initial_timestamp = 0; 16 | static gchar *_timestamp_prefix = NULL; 17 | 18 | glong get_output_channel_age() { 19 | g_assert(_log_file_initial_timestamp > 0); 20 | return get_current_timestamp() - _log_file_initial_timestamp; 21 | } 22 | 23 | void destroy_output_channel() { 24 | if (_out_channel != NULL) { 25 | g_io_channel_shutdown(_out_channel, TRUE, NULL); 26 | g_io_channel_unref(_out_channel); 27 | _out_channel = NULL; 28 | } 29 | g_free(_log_file); 30 | } 31 | 32 | void init_output_channel(const gchar *log_file, gboolean use_locks, gboolean force_control_file, const gchar *chmod_str, const gchar *chown_str, const gchar *chgrp_str, const gchar *timestamp_prefix) { 33 | _log_file = g_strdup(log_file); 34 | _timestamp_prefix = g_strdup(timestamp_prefix); 35 | _use_locks = use_locks; 36 | create_empty(_log_file); 37 | _log_file_initial_timestamp = -1; 38 | gchar *content = NULL; 39 | if (force_control_file == FALSE) { 40 | content = get_control_file_content(_log_file); 41 | if (content != NULL) { 42 | _log_file_initial_timestamp = atol(content); 43 | g_free(content); 44 | } 45 | } 46 | if (_log_file_initial_timestamp <= 0) { 47 | _log_file_initial_timestamp = get_current_timestamp(); 48 | content = g_strdup_printf("%li\n", (long int) _log_file_initial_timestamp); 49 | init_control_file(_log_file, content); 50 | g_free(content); 51 | } 52 | GError *error = NULL; 53 | while (TRUE) { 54 | _out_channel = g_io_channel_new_file(_log_file, "a", &error); 55 | if (chmod_str != NULL) { 56 | mode_t chmod_mode_t = strtol(chmod_str, NULL, 8); 57 | chmod(_log_file, chmod_mode_t); 58 | } 59 | uid_t uid = -1; 60 | gid_t gid = -1; 61 | if (chown_str != NULL) { 62 | uid = user_id_from_name(chown_str); 63 | } 64 | if (chgrp_str != NULL) { 65 | gid = group_id_from_name(chgrp_str); 66 | } 67 | if ((uid > 0) || (gid > 0)) { 68 | chown(_log_file, uid, gid); 69 | } 70 | if (error != NULL) { 71 | g_warning("error during open output channel: %s => waiting 1s and try again...", error->message); 72 | g_error_free(error); 73 | sleep(1); 74 | continue; 75 | } 76 | g_io_channel_set_encoding(_out_channel, NULL, NULL); 77 | g_io_channel_set_buffered(_out_channel, FALSE); 78 | break; 79 | } 80 | } 81 | 82 | gboolean test_output_channel_rotated() { 83 | g_assert(_out_channel != NULL); 84 | int fd = g_io_channel_unix_get_fd(_out_channel); 85 | glong fd_inode = get_fd_inode(fd); 86 | glong log_file_inode = get_file_inode(_log_file); 87 | return (fd_inode != log_file_inode); 88 | } 89 | 90 | gboolean write_output_channel(GString *buffer) { 91 | g_assert(_out_channel != NULL); 92 | GIOStatus write_status; 93 | GError *error = NULL; 94 | gsize written; 95 | 96 | if ( _timestamp_prefix != NULL ) { 97 | GDateTime *dt = g_date_time_new_now_local(); 98 | gchar *prefix = g_date_time_format(dt, _timestamp_prefix); 99 | g_date_time_unref(dt); 100 | if ( prefix != NULL ) { 101 | g_string_prepend(buffer, prefix); 102 | g_free(prefix); 103 | } else { 104 | g_critical("strftime failed for timestamp-prefix: %s", _timestamp_prefix); 105 | } 106 | } 107 | 108 | while (TRUE) { 109 | if (_use_locks) { 110 | int res = flock(g_io_channel_unix_get_fd(_out_channel), LOCK_EX); 111 | if (res < 0) { 112 | continue; 113 | } 114 | } 115 | 116 | write_status = g_io_channel_write_chars(_out_channel, buffer->str, 117 | buffer->len, &written, &error); 118 | if (_use_locks) { 119 | while (TRUE) { 120 | int res2 = flock(g_io_channel_unix_get_fd(_out_channel), LOCK_UN); 121 | if (res2 < 0) { 122 | if (errno == EINTR) { 123 | // try again 124 | continue; 125 | } 126 | } 127 | break; 128 | } 129 | } 130 | if (write_status == G_IO_STATUS_NORMAL) { 131 | break; 132 | } else if (write_status == G_IO_STATUS_AGAIN) { 133 | continue; 134 | } else { 135 | return FALSE; 136 | } 137 | } 138 | return TRUE; 139 | } 140 | -------------------------------------------------------------------------------- /src/out.h: -------------------------------------------------------------------------------- 1 | #ifndef OUT_H_ 2 | #define OUT_H_ 3 | 4 | #include 5 | 6 | void init_output_channel(const gchar *log_file, gboolean use_locks, gboolean force_control_file, const gchar *chmod_str, const gchar *chown_str, const gchar *chgrp_str, const gchar *timestamp_prefix); 7 | void destroy_output_channel(); 8 | gboolean write_output_channel(GString *buffer); 9 | glong get_output_channel_age(); 10 | gboolean test_output_channel_rotated(); 11 | 12 | #endif /* OUT_H_ */ 13 | -------------------------------------------------------------------------------- /src/test_log_proxy.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "util.h" 10 | #include "control.h" 11 | 12 | //tests on util.c 13 | void test_get_current_timestamp() 14 | { 15 | glong t1 = get_current_timestamp(); 16 | g_assert(t1 > 1576518428); //December 16, 2019 17 | sleep(1); 18 | glong t2 = get_current_timestamp(); 19 | g_assert(t2-t1 <= 2); 20 | g_assert(t2-t1 >= 1); 21 | } 22 | 23 | void test_get_unique_hexa_identifier() 24 | { 25 | gchar *hexa1=get_unique_hexa_identifier(); 26 | g_assert(strlen(hexa1) >= 16); //should be 32 27 | gchar *hexa2=get_unique_hexa_identifier(); 28 | g_assert(hexa1 != hexa2); 29 | g_free(hexa1); 30 | g_free(hexa2); 31 | } 32 | 33 | void test_get_file_size() 34 | { 35 | g_unlink("example_file"); 36 | GError *err = NULL; 37 | g_file_set_contents("example_file", "hello\n", 6, &err); 38 | g_assert(get_file_size("example_file") == 6); 39 | g_unlink("example_file"); 40 | g_assert(get_file_size("example_file") == -1); 41 | } 42 | 43 | void test_get_file_inode() 44 | { 45 | g_creat("example_file", O_RDWR); 46 | g_assert(get_file_inode("example_file") > 0); 47 | g_unlink("example_file"); 48 | g_assert(get_file_inode("example_file") == -1); 49 | } 50 | 51 | void test_get_fd_inode() 52 | { 53 | int fd = g_open("example_file", O_CREAT, 0600); 54 | g_assert(get_fd_inode(fd) > 0); 55 | close(fd); 56 | g_remove("example_file"); 57 | g_assert(get_fd_inode(fd) == -1); 58 | } 59 | 60 | void test_compute_strftime_suffix() 61 | { 62 | gchar *suffix = compute_strftime_suffix("example_file", ".%Y%m%d%H%M%S"); 63 | g_assert(strlen(suffix) == 27); 64 | g_assert_cmpstr(suffix, >, "example_file.20191219000000"); //newer than December 19, 2019 65 | g_assert_cmpstr("example_file.21191219000000", >, suffix); //older than December 19, 2119 66 | g_free(suffix); 67 | } 68 | 69 | void test_create_empty() 70 | { 71 | g_unlink("example_file"); 72 | g_assert(create_empty("example_file")); 73 | g_assert(get_file_size("example_file") == 0); 74 | GError *err = NULL; 75 | g_file_set_contents("example_file", "hello\n", 6, &err); 76 | g_assert(create_empty("example_file")); 77 | g_assert(get_file_size("example_file") == 6); 78 | g_unlink("example_file"); 79 | } 80 | 81 | //tests on control.c 82 | void test_manage_control_file() 83 | { 84 | g_unlink(".log_file.control"); 85 | // check init control file 86 | g_assert(init_control_file("log_file", "start")); 87 | // check content 88 | gchar* content = get_control_file_content("log_file"); 89 | g_assert_cmpstr(content, ==, "start"); 90 | g_free(content); 91 | // lock control file (blocking) 92 | int fd1 = lock_control_file("log_file"); 93 | g_assert(fd1 >= 0); 94 | // check inode 95 | int fd2 = g_open(".log_file.control", O_RDONLY); 96 | g_assert_cmpint(get_fd_inode(fd1), ==, get_fd_inode(fd2)); 97 | // unlock control file 98 | unlock_control_file(fd1); 99 | // check control file can be locked again 100 | fd1 = lock_control_file("log_file"); 101 | g_assert(fd1 >= 0); 102 | unlock_control_file(fd1); 103 | } 104 | 105 | #if GLIB_CHECK_VERSION(2,38,0) 106 | void test_blocked_control_file() 107 | { 108 | if (g_test_subprocess()) { 109 | //lock control file 110 | int fd1 = lock_control_file("log_file"); 111 | g_assert(fd1 >= 0); 112 | //try to lock locked control file (should be blocked) 113 | lock_control_file("log_file"); 114 | return; 115 | } 116 | g_test_trap_subprocess(NULL, 2000000, 0); //execute test in subprocess with 3 seconds timeout 117 | // check it failed (control file was blocked) 118 | g_test_trap_assert_failed(); 119 | } 120 | #endif 121 | 122 | #if GLIB_CHECK_VERSION(2,32,0) 123 | void *thread_lock_control_file(void *pointer) 124 | { 125 | glong fd1 = -1; 126 | fd1 = lock_control_file("log_file"); 127 | g_assert(fd1 >= 0); 128 | g_thread_exit((gpointer)fd1); 129 | return(pointer); 130 | } 131 | 132 | void test_lock_unlock_control_file() 133 | { 134 | //lock control file 135 | int fd2 = lock_control_file("log_file"); 136 | g_assert(fd2 >= 0); 137 | //run thread trying to get lock 138 | GThread *thread = g_thread_new("thread", (GThreadFunc)thread_lock_control_file, NULL); 139 | //sleep a while 140 | sleep(1); 141 | //unlock control file 142 | unlock_control_file(fd2); 143 | //check thread has obtain lock 144 | gpointer val = g_thread_join(thread); 145 | g_assert((glong)val > 0); 146 | } 147 | #endif 148 | 149 | //tests on out.c 150 | int main(int argc, char *argv[]) 151 | { 152 | g_test_init (&argc, &argv, NULL); 153 | setlocale(LC_ALL, ""); 154 | g_test_add_func("/log_proxy/test_get_current_timestamp", test_get_current_timestamp); 155 | g_test_add_func("/log_proxy/test_get_unique_hexa_identifier", test_get_unique_hexa_identifier); 156 | g_test_add_func("/log_proxy/test_get_file_size", test_get_file_size); 157 | g_test_add_func("/log_proxy/test_get_fd_inode", test_get_fd_inode); 158 | g_test_add_func("/log_proxy/test_get_file_inode", test_get_file_inode); 159 | g_test_add_func("/log_proxy/test_compute_strftime_suffix", test_compute_strftime_suffix); 160 | g_test_add_func("/log_proxy/test_create_empty", test_create_empty); 161 | g_test_add_func("/log_proxy/test_manage_control_file", test_manage_control_file); 162 | #if GLIB_CHECK_VERSION(2,38,0) 163 | g_test_add_func("/log_proxy/test_blocked_control_file", test_blocked_control_file); 164 | #endif 165 | #if GLIB_CHECK_VERSION(2,32,0) 166 | g_test_add_func("/log_proxy/test_lock_unlock_control_file", test_lock_unlock_control_file); 167 | #endif 168 | int res = g_test_run(); 169 | return res; 170 | } 171 | -------------------------------------------------------------------------------- /src/util.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include "util.h" 11 | 12 | static GRand *__rand = NULL; 13 | 14 | GRand *__get_grand() 15 | { 16 | if (__rand == NULL) { 17 | __rand = g_rand_new(); 18 | } 19 | return __rand; 20 | } 21 | 22 | /** 23 | * Return the current timestamp as a long. 24 | * 25 | * @return the current timestamp. 26 | */ 27 | glong get_current_timestamp() 28 | { 29 | GTimeVal gtv; 30 | g_get_current_time(>v); 31 | return gtv.tv_sec; 32 | } 33 | 34 | /** 35 | * Return a unique hexa identifier 36 | * 37 | * @return newly allocated string containing a unique id (free with g_free). 38 | */ 39 | gchar *get_unique_hexa_identifier() 40 | { 41 | GRand *rnd = __get_grand(); 42 | gchar *res = g_malloc(sizeof(gchar) * 33); 43 | guint32 ri; 44 | int i; 45 | for (i = 0 ; i < 4 ; i++) { 46 | ri = g_rand_int(rnd); 47 | sprintf(res + (i * 8) * sizeof(gchar), "%08x", ri); 48 | } 49 | return res; 50 | } 51 | 52 | 53 | /** 54 | * Return the size (in bytes) of the given file path. 55 | * 56 | * @param file_path the file path. 57 | * @return the size (in bytes), -1 means error. 58 | */ 59 | glong get_file_size(const gchar *file_path) { 60 | GStatBuf buf; 61 | int res = g_stat(file_path, &buf); 62 | if (res != 0) { 63 | return -1; 64 | } 65 | return buf.st_size; 66 | } 67 | 68 | /** 69 | * Return the inode of the given file path. 70 | * 71 | * @param file_path the file path. 72 | * @return the file inode, -1 means error. 73 | */ 74 | glong get_file_inode(const gchar *file_path) { 75 | GStatBuf buf; 76 | int res = g_stat(file_path, &buf); 77 | if (res != 0) { 78 | return -1; 79 | } 80 | return buf.st_ino; 81 | } 82 | 83 | /** 84 | * Return the inode of the given file descriptor. 85 | * 86 | * @param fd file descriptor. 87 | * @return the file inode, -1 means error. 88 | */ 89 | glong get_fd_inode(int fd) { 90 | struct stat buf; 91 | int res = fstat(fd, &buf); 92 | if (res != 0) { 93 | return -1; 94 | } 95 | return buf.st_ino; 96 | } 97 | 98 | /** 99 | * Compute and append a strftime suffix. 100 | * 101 | * @param str original string. 102 | * @param strftime_suffix suffix to append to previous string (with strftime placeholders). 103 | * @return newly allocated string (free it with g_free) with resolved and appended suffix. 104 | */ 105 | gchar *compute_strftime_suffix(const gchar *str, const gchar *strftime_suffix) { 106 | time_t t; 107 | struct tm *tmp; 108 | t = time(NULL); 109 | tmp = localtime(&t); 110 | g_assert(tmp != NULL); 111 | char outstr[200]; 112 | if (strftime(outstr, sizeof(outstr), strftime_suffix, tmp) == 0) { 113 | g_critical("problem with strftime on %s", strftime_suffix); 114 | return NULL; 115 | } 116 | return g_strdup_printf("%s%s", str, outstr); 117 | } 118 | 119 | /** 120 | * Compute absolute file path from directory path and file name 121 | * 122 | * @param directory absolute or relative directory path 123 | * @param file_name file name (absolute or relative, if absolute directory is ignored) 124 | * @return newly allocated string (free it with g_free) with absolute path to the file 125 | */ 126 | gchar *compute_file_path(const gchar *directory, const gchar *file_name) { 127 | if ( file_name[0] == '/' ) { 128 | return g_strdup_printf("%s", file_name); 129 | } 130 | else { 131 | if ( directory[0] == '/' ) { 132 | return g_strdup_printf("%s/%s", directory, file_name); 133 | } 134 | else { 135 | return g_strdup_printf("%s/%s/%s", g_get_current_dir(), directory, 136 | file_name); 137 | } 138 | } 139 | } 140 | 141 | /** 142 | * Create an empty file if it does not exist 143 | * 144 | * Note: if the file already exists, nothing is done and TRUE is returned. 145 | * 146 | * @param file_path file path. 147 | * @return TRUE if ok, FALSE if errors. 148 | */ 149 | gboolean create_empty(const gchar *file_path) { 150 | int fd2, close_res; 151 | while (TRUE) { 152 | fd2 = open(file_path, O_CREAT | O_EXCL, 0600); 153 | if (fd2 < 0) { 154 | if (errno == EEXIST) { 155 | return TRUE; 156 | } 157 | if (errno == EINTR) { 158 | continue; 159 | } 160 | g_warning("can't create %s (errno: %i)", file_path, errno); 161 | return FALSE; 162 | } else { 163 | break; 164 | } 165 | } 166 | while (TRUE) { 167 | close_res = close(fd2); 168 | if (close_res < 0) { 169 | if (errno == EINTR) { 170 | continue; 171 | } 172 | g_warning("can't close %s (errno: %i)", file_path, errno); 173 | return FALSE; 174 | } else { 175 | break; 176 | } 177 | } 178 | return TRUE; 179 | } 180 | 181 | uid_t user_id_from_name(const gchar *name) { 182 | struct passwd *pwd; 183 | uid_t u; 184 | char *endptr; 185 | if (name == NULL || *name == '\0') { 186 | return -1; 187 | } 188 | u = strtol(name, &endptr, 10); 189 | if (*endptr == '\0') { 190 | // allow a numeric string 191 | return u; 192 | } 193 | pwd = getpwnam(name); 194 | if (pwd == NULL) { 195 | return -1; 196 | } 197 | return pwd->pw_uid; 198 | } 199 | 200 | gid_t group_id_from_name(const gchar *name) { 201 | struct group *grp; 202 | gid_t g; 203 | char *endptr; 204 | if (name == NULL || *name == '\0') { 205 | return -1; 206 | } 207 | g = strtol(name, &endptr, 10); 208 | if (*endptr == '\0') { 209 | // allow a numeric string 210 | return g; 211 | } 212 | grp = getgrnam(name); 213 | if (grp == NULL) { 214 | return -1; 215 | } 216 | return grp->gr_gid; 217 | } 218 | -------------------------------------------------------------------------------- /src/util.h: -------------------------------------------------------------------------------- 1 | #ifndef UTIL_H_ 2 | #define UTIL_H_ 3 | 4 | #include 5 | #include 6 | 7 | glong get_file_size(const gchar *file_path); 8 | glong get_current_timestamp(); 9 | gchar *compute_strftime_suffix(const gchar *str, const gchar *strftime_suffix); 10 | gchar *get_unique_hexa_identifier(); 11 | glong get_file_inode(const gchar *file_path); 12 | glong get_fd_inode(int fd); 13 | gboolean create_empty(const gchar *file_path); 14 | gchar *compute_file_path(const gchar *directory, const gchar *file_name); 15 | uid_t user_id_from_name(const gchar *name); 16 | gid_t group_id_from_name(const gchar *name); 17 | 18 | #endif /* UTIL_H_ */ 19 | -------------------------------------------------------------------------------- /src/valgrind.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function usage() 4 | { 5 | echo "usage: valgrind.sh /path/program/test [OPTIONS]" 6 | } 7 | 8 | if test $# -lt 1; then 9 | usage 10 | exit 11 | fi 12 | 13 | which valgrind >/dev/null 2>&1 14 | if test $? -ne 0; then 15 | echo "VALGRIND NOT FOUND" 16 | exit 1 17 | fi 18 | 19 | if test -f valgrind.suppressions; then 20 | SUPPS="--suppressions=valgrind.suppressions" 21 | else 22 | SUPPS="" 23 | fi 24 | 25 | echo "MEMORY ERRORS TESTS..." 26 | valgrind --error-exitcode=1 ${SUPPS} "$@" 27 | if test $? -eq 0; then 28 | echo "OK" 29 | else 30 | echo '=> MEMORY ERRORS DETECTED' 31 | exit 1 32 | fi 33 | 34 | echo "TEST DES FUITES MEMOIRES..." 35 | valgrind --tool=memcheck --leak-check=full --show-possibly-lost=yes ${SUPPS} "$@" 2>&1 |tee /tmp/valgrind.out 36 | N=$(cat /tmp/valgrind.out |grep -c "definitely lost: 0 bytes in 0 blocks") 37 | if test "${N}" -gt 0; then 38 | echo "OK" 39 | else 40 | echo "=> MEMORY LEAKS DETECTED" 41 | exit 1 42 | fi 43 | --------------------------------------------------------------------------------